Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PlanarEmbedding.remove_edge() now updates removed edge's neighbors #6798

Merged
merged 24 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0c8c64c
feat: drop the use of node attribute "first_nbr" in PlanarEmbedding (…
mdealencar Jan 2, 2024
525bcf7
feat: add method PlanarEmbedding.add_half_edge(), which superceeds th…
mdealencar Jan 3, 2024
16b7d83
style: code linting with black (planarity.py)
mdealencar Jan 3, 2024
76f6375
Merge branch 'networkx:main' into del_first_nbr
mdealencar Jan 3, 2024
e831f68
fix: deduplication of code in PlanarEmbedding.add_half_edge(), improv…
mdealencar Jan 4, 2024
17e2237
style: code linting with black (planarity.py)
mdealencar Jan 4, 2024
dbebb81
style: reformatting of add_half_edge_first()'s docstring (planarity.py)
mdealencar Jan 4, 2024
cbcea37
refactor: PlanarEmbedding: use local variable for successors and add …
mdealencar Jan 4, 2024
9d42ff8
fix: use `pytest.raises()` context in tests only for the exception-ra…
mdealencar Jan 4, 2024
3b1611b
Update networkx/algorithms/planarity.py
dschult Jan 5, 2024
34cd9f8
Update networkx/algorithms/planarity.py
dschult Jan 5, 2024
fd3c215
fix: PlanarEmbedding.remove_edge() now updates removed edge's neighbors
Jul 20, 2023
f4d8e16
feat: add test for PlanarEmbedding.remove_edge()
mdealencar Jul 22, 2023
6f208d8
feat: added PlanarEmbedding.remove_node() maintaining the structure v…
mdealencar Jul 22, 2023
3775aed
feat: disable inherited edge-adding methods of PlanarEmbedding (plana…
mdealencar Dec 21, 2023
5453be0
feat: add test of PlanarEmbedding's forbidden methods (test_planarity…
mdealencar Jan 2, 2024
f438481
fix: remove references to "first_nbr" (planarity.py)
mdealencar Jan 5, 2024
2f3a360
fix: PlanarEmbedding.add_half_edge(): explicitly call add_edge() meth…
mdealencar Jan 5, 2024
ca4170c
fix: work around calls to PlanarEmbedding.add_edge() in tests (test_p…
mdealencar Jan 5, 2024
4d9930f
doc: suggest `PlanarEmbedding.add_half_edge()` if `add_edge()` is cal…
mdealencar Jan 5, 2024
7f2f664
feat: add test for PlanarEmbedding's .remove_edge*() methods (test_pl…
mdealencar Jan 5, 2024
ea76c71
fix: change exception raised by forbidden methods in PlanarEmbedding …
mdealencar Jan 10, 2024
71b7607
Merge branch 'main' into main
mdealencar Jan 10, 2024
4f7afba
chore: code cleanup (test_planarity.py)
mdealencar Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
166 changes: 163 additions & 3 deletions networkx/algorithms/planarity.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,22 @@ class PlanarEmbedding(nx.DiGraph):

"""

def __init__(self, incoming_graph_data=None, **attr):
super().__init__(incoming_graph_data=incoming_graph_data, **attr)
self.add_edge = self.__forbidden
self.add_edges_from = self.__forbidden
self.add_weighted_edges_from = self.__forbidden

def __forbidden(self, *args, **kwargs):
"""Forbidden operation

Any edge additions to a PlanarEmbedding should be done using
method `add_half_edge`.
"""
raise NotImplementedError(
"Use `add_half_edge` method to add edges to a PlanarEmbedding."
)

def get_data(self):
"""Converts the adjacency structure into a better readable structure.

Expand Down Expand Up @@ -896,6 +912,75 @@ def set_data(self, data):
self.add_half_edge(v, w, cw=ref)
ref = w

def remove_node(self, n):
"""Remove node n.

Removes the node n and all adjacent edges, updating the
PlanarEmbedding to account for any resulting edge removal.
Attempting to remove a non-existent node will raise an exception.

Parameters
----------
n : node
A node in the graph

Raises
------
NetworkXError
If n is not in the graph.

See Also
--------
remove_nodes_from

"""
try:
for u in self._pred[n]:
succs_u = self._succ[u]
un_cw = succs_u[n]["cw"]
un_ccw = succs_u[n]["ccw"]
del succs_u[n]
del self._pred[u][n]
if n != un_cw:
succs_u[un_cw]["ccw"] = un_ccw
succs_u[un_ccw]["cw"] = un_cw
del self._node[n]
del self._succ[n]
del self._pred[n]
except KeyError as err: # NetworkXError if n not in self
raise nx.NetworkXError(
f"The node {n} is not in the planar embedding."
) from err

def remove_nodes_from(self, nodes):
"""Remove multiple nodes.

Parameters
----------
nodes : iterable container
A container of nodes (list, dict, set, etc.). If a node
in the container is not in the graph it is silently ignored.

See Also
--------
remove_node

Notes
-----
When removing nodes from an iterator over the graph you are changing,
a `RuntimeError` will be raised with message:
`RuntimeError: dictionary changed size during iteration`. This
happens when the graph's underlying dictionary is modified during
iteration. To avoid this error, evaluate the iterator into a separate
object, e.g. by using `list(iterator_of_nodes)`, and pass this
object to `G.remove_nodes_from`.

"""
for n in nodes:
if n in self._node:
self.remove_node(n)
# silently skip non-existing nodes

def neighbors_cw_order(self, v):
"""Generator for the neighbors of v in clockwise order.

Expand Down Expand Up @@ -940,6 +1025,7 @@ def add_half_edge(self, start_node, end_node, *, cw=None, ccw=None):
End node of reference edge.
Omit or pass `None` if adding the first out-half-edge of `start_node`.


Raises
------
NetworkXException
Expand All @@ -962,7 +1048,7 @@ def add_half_edge(self, start_node, end_node, *, cw=None, ccw=None):
if ccw is not None:
raise nx.NetworkXError("Only one of cw/ccw can be specified.")
ref_ccw = succs[cw]["ccw"]
self.add_edge(start_node, end_node, cw=cw, ccw=ref_ccw)
super().add_edge(start_node, end_node, cw=cw, ccw=ref_ccw)
succs[ref_ccw]["cw"] = end_node
succs[cw]["ccw"] = end_node
# when (cw == leftmost_nbr), the newly added neighbor is
Expand All @@ -973,7 +1059,7 @@ def add_half_edge(self, start_node, end_node, *, cw=None, ccw=None):
if ccw not in succs:
raise nx.NetworkXError("Invalid counterclockwise reference node.")
ref_cw = succs[ccw]["cw"]
self.add_edge(start_node, end_node, cw=ref_cw, ccw=ccw)
super().add_edge(start_node, end_node, cw=ref_cw, ccw=ccw)
succs[ref_cw]["ccw"] = end_node
succs[ccw]["cw"] = end_node
move_leftmost_nbr_to_end = True
Expand All @@ -986,11 +1072,12 @@ def add_half_edge(self, start_node, end_node, *, cw=None, ccw=None):
# we keep track of the leftmost neighbor, which we accomplish
# by keeping it as the last key in dict self._succ[start_node]
succs[leftmost_nbr] = succs.pop(leftmost_nbr)

else:
if cw is not None or ccw is not None:
raise nx.NetworkXError("Invalid reference node.")
# adding the first edge out of start_node
self.add_edge(start_node, end_node, ccw=end_node, cw=end_node)
super().add_edge(start_node, end_node, ccw=end_node, cw=end_node)

def check_structure(self):
"""Runs without exceptions if this object is valid.
Expand Down Expand Up @@ -1107,6 +1194,79 @@ def add_half_edge_cw(self, start_node, end_node, reference_neighbor):
"""
self.add_half_edge(start_node, end_node, ccw=reference_neighbor)

def remove_edge(self, u, v):
"""Remove the edge between u and v.

Parameters
----------
u, v : nodes
Remove the half-edges (u, v) and (v, u) and update the
edge ordering around the removed edge.

Raises
------
NetworkXError
If there is not an edge between u and v.

See Also
--------
remove_edges_from : remove a collection of edges
"""
try:
succs_u = self._succ[u]
succs_v = self._succ[v]
uv_cw = succs_u[v]["cw"]
uv_ccw = succs_u[v]["ccw"]
vu_cw = succs_v[u]["cw"]
vu_ccw = succs_v[u]["ccw"]
del succs_u[v]
del self._pred[v][u]
del succs_v[u]
del self._pred[u][v]
if v != uv_cw:
succs_u[uv_cw]["ccw"] = uv_ccw
succs_u[uv_ccw]["cw"] = uv_cw
if u != vu_cw:
succs_v[vu_cw]["ccw"] = vu_ccw
succs_v[vu_ccw]["cw"] = vu_cw
except KeyError as err:
raise nx.NetworkXError(
f"The edge {u}-{v} is not in the planar embedding."
) from err

def remove_edges_from(self, ebunch):
"""Remove all edges specified in ebunch.

Parameters
----------
ebunch: list or container of edge tuples
Each pair of half-edges between the nodes given in the tuples
will be removed from the graph. The nodes can be passed as:

- 2-tuples (u, v) half-edges (u, v) and (v, u).
- 3-tuples (u, v, k) where k is ignored.

See Also
--------
remove_edge : remove a single edge

Notes
-----
Will fail silently if an edge in ebunch is not in the graph.

Examples
--------
>>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
>>> ebunch = [(1, 2), (2, 3)]
>>> G.remove_edges_from(ebunch)
"""
for e in ebunch:
u, v = e[:2] # ignore edge data
# assuming that the PlanarEmbedding is valid, if the half_edge
# (u, v) is in the graph, then so is half_edge (v, u)
if u in self._succ and v in self._succ[u]:
self.remove_edge(u, v)

def connect_components(self, v, w):
"""Adds half-edges for (v, w) and (w, v) at some position.

Expand Down
77 changes: 67 additions & 10 deletions networkx/algorithms/tests/test_planarity.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,20 @@ def test_counterexample_planar_recursive(self):
G.add_node(1)
get_counterexample_recursive(G)

def test_edge_removal_from_planar_embedding(self):
# PlanarEmbedding.check_structure() must succeed after edge removal
edges = ((0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 2), (0, 3))
G = nx.Graph(edges)
cert, P = nx.check_planarity(G)
assert cert is True
P.remove_edge(0, 2)
self.check_graph(P, is_planar=True)
P.add_half_edge_ccw(1, 3, 2)
P.add_half_edge_cw(3, 1, 2)
self.check_graph(P, is_planar=True)
P.remove_edges_from(((0, 3), (1, 3)))
self.check_graph(P, is_planar=True)

rossbar marked this conversation as resolved.
Show resolved Hide resolved

def check_embedding(G, embedding):
"""Raises an exception if the combinatorial embedding is not correct
Expand Down Expand Up @@ -410,19 +424,51 @@ def test_get_data(self):
data_cmp = {0: [3, 2, 1], 1: [0], 2: [0], 3: [0]}
assert data == data_cmp

def test_missing_edge_orientation(self):
def test_edge_removal(self):
embedding = nx.PlanarEmbedding()
embedding.add_edge(1, 2)
embedding.add_edge(2, 1)
embedding.set_data(
{
1: [2, 5, 7],
2: [1, 3, 4, 5],
3: [2, 4],
4: [3, 6, 5, 2],
5: [7, 1, 2, 4],
6: [4, 7],
7: [6, 1, 5],
}
)
# remove_edges_from() calls remove_edge(), so both are tested here
embedding.remove_edges_from(((5, 4), (1, 5)))
embedding.check_structure()
embedding_expected = nx.PlanarEmbedding()
embedding_expected.set_data(
{
1: [2, 7],
2: [1, 3, 4, 5],
3: [2, 4],
4: [3, 6, 2],
5: [7, 2],
6: [4, 7],
7: [6, 1, 5],
}
)
assert nx.utils.graphs_equal(embedding, embedding_expected)

def test_missing_edge_orientation(self):
embedding = nx.PlanarEmbedding({1: {2: {}}, 2: {1: {}}})
with pytest.raises(nx.NetworkXException):
# Invalid structure because the orientation of the edge was not set
embedding.check_structure()

def test_invalid_edge_orientation(self):
embedding = nx.PlanarEmbedding()
embedding.add_half_edge(1, 2)
embedding.add_half_edge(2, 1)
embedding.add_edge(1, 3)
embedding = nx.PlanarEmbedding(
{
1: {2: {"cw": 2, "ccw": 2}},
2: {1: {"cw": 1, "ccw": 1}},
1: {3: {}},
3: {1: {}},
}
)
with pytest.raises(nx.NetworkXException):
embedding.check_structure()

Expand Down Expand Up @@ -461,12 +507,23 @@ def test_successful_face_traversal(self):
assert face == [1, 2]

def test_unsuccessful_face_traversal(self):
embedding = nx.PlanarEmbedding()
embedding.add_edge(1, 2, ccw=2, cw=3)
embedding.add_edge(2, 1, ccw=1, cw=3)
embedding = nx.PlanarEmbedding(
{1: {2: {"cw": 3, "ccw": 2}}, 2: {1: {"cw": 3, "ccw": 1}}}
)
with pytest.raises(nx.NetworkXException):
embedding.traverse_face(1, 2)

def test_forbidden_methods(self):
embedding = nx.PlanarEmbedding()
embedding.add_node(42) # no exception
embedding.add_nodes_from([(23, 24)]) # no exception
with pytest.raises(NotImplementedError):
embedding.add_edge(1, 3)
with pytest.raises(NotImplementedError):
embedding.add_edges_from([(0, 2), (1, 4)])
with pytest.raises(NotImplementedError):
embedding.add_weighted_edges_from([(0, 2, 350), (1, 4, 125)])

@staticmethod
def get_star_embedding(n):
embedding = nx.PlanarEmbedding()
Expand Down