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

feat: drop the use of node attribute "first_nbr" in PlanarEmbedding #7202

Merged
merged 15 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
8 changes: 4 additions & 4 deletions networkx/algorithms/planar_drawing.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ def triangulate_face(embedding, v1, v2):
v1, v2, v3 = v2, v3, v4
else:
# Add edge for triangulation
embedding.add_half_edge_cw(v1, v3, v2)
embedding.add_half_edge_ccw(v3, v1, v2)
embedding.add_half_edge(v1, v3, ccw=v2)
embedding.add_half_edge(v3, v1, cw=v2)
v1, v2, v3 = v1, v3, v4
# Get next node
_, v4 = embedding.next_face_half_edge(v2, v3)
Expand Down Expand Up @@ -445,8 +445,8 @@ def make_bi_connected(embedding, starting_node, outgoing_node, edges_counted):
# cycle is not completed yet
if v2 in face_set:
# v2 encountered twice: Add edge to ensure 2-connectedness
embedding.add_half_edge_cw(v1, v3, v2)
embedding.add_half_edge_ccw(v3, v1, v2)
embedding.add_half_edge(v1, v3, ccw=v2)
embedding.add_half_edge(v3, v1, cw=v2)
edges_counted.add((v2, v3))
edges_counted.add((v3, v1))
v2 = v1
Expand Down
207 changes: 128 additions & 79 deletions networkx/algorithms/planarity.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ def lr_planarity(self):
# initialize the embedding
previous_node = None
for w in self.ordered_adjs[v]:
self.embedding.add_half_edge_cw(v, w, previous_node)
self.embedding.add_half_edge(v, w, ccw=previous_node)
previous_node = w

# Free no longer used variables
Expand Down Expand Up @@ -436,7 +436,7 @@ def lr_planarity_recursive(self):
# initialize the embedding
previous_node = None
for w in self.ordered_adjs[v]:
self.embedding.add_half_edge_cw(v, w, previous_node)
self.embedding.add_half_edge(v, w, ccw=previous_node)
previous_node = w

# compute the complete embedding
Expand Down Expand Up @@ -714,9 +714,9 @@ def dfs_embedding(self, v):
break # handle next node in dfs_stack (i.e. w)
else: # back edge
if self.side[ei] == 1:
self.embedding.add_half_edge_cw(w, v, self.right_ref[w])
self.embedding.add_half_edge(w, v, ccw=self.right_ref[w])
else:
self.embedding.add_half_edge_ccw(w, v, self.left_ref[w])
self.embedding.add_half_edge(w, v, cw=self.left_ref[w])
self.left_ref[w] = v

def dfs_embedding_recursive(self, v):
Expand All @@ -731,10 +731,10 @@ def dfs_embedding_recursive(self, v):
else: # back edge
if self.side[ei] == 1:
# place v directly after right_ref[w] in embed. list of w
self.embedding.add_half_edge_cw(w, v, self.right_ref[w])
self.embedding.add_half_edge(w, v, ccw=self.right_ref[w])
else:
# place v directly before left_ref[w] in embed. list of w
self.embedding.add_half_edge_ccw(w, v, self.left_ref[w])
self.embedding.add_half_edge(w, v, cw=self.left_ref[w])
self.left_ref[w] = v

def sign(self, e):
Expand Down Expand Up @@ -791,15 +791,12 @@ class PlanarEmbedding(nx.DiGraph):
* Edges must go in both directions (because the edge attributes differ)
* Every edge must have a 'cw' and 'ccw' attribute which corresponds to a
correct planar embedding.
* A node with non zero degree must have a node attribute 'first_nbr'.

As long as a PlanarEmbedding is invalid only the following methods should
be called:

* :meth:`add_half_edge_ccw`
* :meth:`add_half_edge_cw`
* :meth:`add_half_edge`
* :meth:`connect_components`
* :meth:`add_half_edge_first`

Even though the graph is a subclass of nx.DiGraph, it can still be used
for algorithms that require undirected graphs, because the method
Expand All @@ -808,14 +805,14 @@ class PlanarEmbedding(nx.DiGraph):

**Half edges:**

In methods like `add_half_edge_ccw` the term "half-edge" is used, which is
In methods like `add_half_edge` the term "half-edge" is used, which is
a term that is used in `doubly connected edge lists
<https://en.wikipedia.org/wiki/Doubly_connected_edge_list>`_. It is used
to emphasize that the edge is only in one direction and there exists
another half-edge in the opposite direction.
While conventional edges always have two faces (including outer face) next
to them, it is possible to assign each half-edge *exactly one* face.
For a half-edge (u, v) that is orientated such that u is below v then the
For a half-edge (u, v) that is oriented such that u is below v then the
face that belongs to (u, v) is to the right of this half-edge.

See Also
Expand All @@ -833,23 +830,23 @@ class PlanarEmbedding(nx.DiGraph):
Create an embedding of a star graph (compare `nx.star_graph(3)`):

>>> G = nx.PlanarEmbedding()
>>> G.add_half_edge_cw(0, 1, None)
>>> G.add_half_edge_cw(0, 2, 1)
>>> G.add_half_edge_cw(0, 3, 2)
>>> G.add_half_edge_cw(1, 0, None)
>>> G.add_half_edge_cw(2, 0, None)
>>> G.add_half_edge_cw(3, 0, None)
>>> G.add_half_edge(0, 1)
>>> G.add_half_edge(0, 2, ccw=1)
>>> G.add_half_edge(0, 3, ccw=2)
>>> G.add_half_edge(1, 0)
>>> G.add_half_edge(2, 0)
>>> G.add_half_edge(3, 0)

Alternatively the same embedding can also be defined in counterclockwise
orientation. The following results in exactly the same PlanarEmbedding:

>>> G = nx.PlanarEmbedding()
>>> G.add_half_edge_ccw(0, 1, None)
>>> G.add_half_edge_ccw(0, 3, 1)
>>> G.add_half_edge_ccw(0, 2, 3)
>>> G.add_half_edge_ccw(1, 0, None)
>>> G.add_half_edge_ccw(2, 0, None)
>>> G.add_half_edge_ccw(3, 0, None)
>>> G.add_half_edge(0, 1)
>>> G.add_half_edge(0, 3, cw=1)
>>> G.add_half_edge(0, 2, cw=3)
>>> G.add_half_edge(1, 0)
>>> G.add_half_edge(2, 0)
>>> G.add_half_edge(3, 0)

After creating a graph, it is possible to validate that the PlanarEmbedding
object is correct:
Expand Down Expand Up @@ -894,8 +891,10 @@ def set_data(self, data):

"""
for v in data:
ref = None
for w in reversed(data[v]):
self.add_half_edge_first(v, w)
self.add_half_edge(v, w, cw=ref)
ref = w

def neighbors_cw_order(self, v):
"""Generator for the neighbors of v in clockwise order.
Expand All @@ -909,15 +908,90 @@ def neighbors_cw_order(self, v):
node

"""
if len(self[v]) == 0:
succs = self._succ[v]
if not succs:
# v has no neighbors
return
start_node = self.nodes[v]["first_nbr"]
start_node = next(reversed(succs))
yield start_node
current_node = self[v][start_node]["cw"]
current_node = succs[start_node]["cw"]
while start_node != current_node:
yield current_node
current_node = self[v][current_node]["cw"]
current_node = succs[current_node]["cw"]

def add_half_edge(self, start_node, end_node, cw=None, ccw=None):
mdealencar marked this conversation as resolved.
Show resolved Hide resolved
"""Adds a half-edge from start_node to end_node.

If the half-edge is not the first one out of start_node, a reference
node must be provided either in the clockwise (parameter cw) or in
the counterclockwise (parameter ccw) direction. Only one of cw/cww
parameter can be specified (or neither in the case of the first edge).
Note that specifying a reference in the clockwise (cw) direction means
inserting the new edge in the first counterclockwise position with
respect to the reference (and vice-versa).

Parameters
----------
start_node : node
Start node of inserted edge.
end_node : node
End node of inserted edge.
cw/ccw: node
End node of reference edge. Ommit or pass None if adding the first out-half-edge of start_node.
dschult marked this conversation as resolved.
Show resolved Hide resolved

Raises
------
NetworkXException
If the cw or ccw node is not a successor of start_node.
Or if start_node has successors, but neither cw or ccw is provided.
Or if both cw and ccw are specified.

See Also
--------
connect_components
"""

succs = self._succ.get(start_node)
if succs:
# there is already some edge out of start_node
leftmost_nbr = next(reversed(self._succ[start_node]))
if cw is not None:
if cw not in succs:
raise nx.NetworkXError("Invalid clockwise reference node.")
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)
succs[ref_ccw]["cw"] = end_node
succs[cw]["ccw"] = end_node
# when (cw == leftmost_nbr), the newly added neighbor is
# already at the end of dict self._succ[start_node] and
# takes the place of the former leftmost_nbr
move_leftmost_nbr_to_end = cw != leftmost_nbr
elif ccw is not 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)
succs[ref_cw]["ccw"] = end_node
succs[ccw]["cw"] = end_node
move_leftmost_nbr_to_end = True
else:
raise nx.NetworkXError(
"Node already has out-half-edge(s), either cw or ccw reference node required."
)
if move_leftmost_nbr_to_end:
# LRPlanarity (via self.add_half_edge_first()) requires that
# we keep track of the leftmost neighbor, which we accomplish
# by keeping it as the last key in dict self._succ[start_node]
data = succs[leftmost_nbr]
del succs[leftmost_nbr]
succs[leftmost_nbr] = data
rossbar marked this conversation as resolved.
Show resolved Hide resolved
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)

def check_structure(self):
"""Runs without exceptions if this object is valid.
Expand All @@ -927,7 +1001,6 @@ def check_structure(self):
* Edges go in both directions (because the edge attributes differ).
* Every edge has a 'cw' and 'ccw' attribute which corresponds to a
correct planar embedding.
* A node with a degree larger than 0 has a node attribute 'first_nbr'.

Running this method verifies that the underlying Graph must be planar.

Expand Down Expand Up @@ -1000,24 +1073,12 @@ def add_half_edge_ccw(self, start_node, end_node, reference_neighbor):

See Also
--------
add_half_edge
add_half_edge_cw
connect_components
add_half_edge_first

"""
if reference_neighbor is None:
# The start node has no neighbors
self.add_edge(start_node, end_node) # Add edge to graph
self[start_node][end_node]["cw"] = end_node
self[start_node][end_node]["ccw"] = end_node
self.nodes[start_node]["first_nbr"] = end_node
else:
ccw_reference = self[start_node][reference_neighbor]["ccw"]
self.add_half_edge_cw(start_node, end_node, ccw_reference)

if reference_neighbor == self.nodes[start_node].get("first_nbr", None):
# Update first neighbor
self.nodes[start_node]["first_nbr"] = end_node
self.add_half_edge(start_node, end_node, cw=reference_neighbor)

def add_half_edge_cw(self, start_node, end_node, reference_neighbor):
"""Adds a half-edge from start_node to end_node.
Expand All @@ -1041,31 +1102,11 @@ def add_half_edge_cw(self, start_node, end_node, reference_neighbor):

See Also
--------
add_half_edge
add_half_edge_ccw
connect_components
add_half_edge_first
"""
self.add_edge(start_node, end_node) # Add edge to graph

if reference_neighbor is None:
# The start node has no neighbors
self[start_node][end_node]["cw"] = end_node
self[start_node][end_node]["ccw"] = end_node
self.nodes[start_node]["first_nbr"] = end_node
return

if reference_neighbor not in self[start_node]:
raise nx.NetworkXException(
"Cannot add edge. Reference neighbor does not exist"
)

# Get half-edge at the other side
cw_reference = self[start_node][reference_neighbor]["cw"]
# Alter half-edge data structures
self[start_node][reference_neighbor]["cw"] = end_node
self[start_node][end_node]["cw"] = cw_reference
self[start_node][cw_reference]["ccw"] = end_node
self[start_node][end_node]["ccw"] = reference_neighbor
self.add_half_edge(start_node, end_node, ccw=reference_neighbor)

def connect_components(self, v, w):
"""Adds half-edges for (v, w) and (w, v) at some position.
Expand All @@ -1084,15 +1125,24 @@ def connect_components(self, v, w):

See Also
--------
add_half_edge_ccw
add_half_edge_cw
add_half_edge_first
add_half_edge
"""
self.add_half_edge_first(v, w)
self.add_half_edge_first(w, v)
if v in self._succ and self._succ[v]:
ref = next(reversed(self._succ[v]))
else:
ref = None
self.add_half_edge(v, w, cw=ref)
if w in self._succ and self._succ[w]:
ref = next(reversed(self._succ[w]))
else:
ref = None
self.add_half_edge(w, v, cw=ref)

def add_half_edge_first(self, start_node, end_node):
"""The added half-edge is inserted at the first position in the order.
"""Add a half-edge and set end_node as start_node's leftmost neighbor.

The new edge is inserted counterclockwise with respect to the current
leftmost neighbor, if there is one.

Parameters
----------
Expand All @@ -1101,15 +1151,14 @@ def add_half_edge_first(self, start_node, end_node):

See Also
--------
add_half_edge_ccw
add_half_edge_cw
add_half_edge
connect_components
"""
if start_node in self and "first_nbr" in self.nodes[start_node]:
reference = self.nodes[start_node]["first_nbr"]
else:
reference = None
self.add_half_edge_ccw(start_node, end_node, reference)
succs = self._succ.get(start_node)
# the leftmost neighbor is the last entry in the
# self._succ[start_node] dict
leftmost_nbr = next(reversed(succs)) if succs else None
self.add_half_edge(start_node, end_node, cw=leftmost_nbr)

def next_face_half_edge(self, v, w):
"""Returns the following half-edge left of a face.
Expand Down