aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/planarity.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/algorithms/planarity.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/planarity.py')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/planarity.py1402
1 files changed, 1402 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/planarity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/planarity.py
new file mode 100644
index 00000000..17d0bec5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/planarity.py
@@ -0,0 +1,1402 @@
+from collections import defaultdict
+
+import networkx as nx
+
+__all__ = ["check_planarity", "is_planar", "PlanarEmbedding"]
+
+
+@nx._dispatchable
+def is_planar(G):
+ """Returns True if and only if `G` is planar.
+
+ A graph is *planar* iff it can be drawn in a plane without
+ any edge intersections.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Returns
+ -------
+ bool
+ Whether the graph is planar.
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2)])
+ >>> nx.is_planar(G)
+ True
+ >>> nx.is_planar(nx.complete_graph(5))
+ False
+
+ See Also
+ --------
+ check_planarity :
+ Check if graph is planar *and* return a `PlanarEmbedding` instance if True.
+ """
+
+ return check_planarity(G, counterexample=False)[0]
+
+
+@nx._dispatchable(returns_graph=True)
+def check_planarity(G, counterexample=False):
+ """Check if a graph is planar and return a counterexample or an embedding.
+
+ A graph is planar iff it can be drawn in a plane without
+ any edge intersections.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ counterexample : bool
+ A Kuratowski subgraph (to proof non planarity) is only returned if set
+ to true.
+
+ Returns
+ -------
+ (is_planar, certificate) : (bool, NetworkX graph) tuple
+ is_planar is true if the graph is planar.
+ If the graph is planar `certificate` is a PlanarEmbedding
+ otherwise it is a Kuratowski subgraph.
+
+ Examples
+ --------
+ >>> G = nx.Graph([(0, 1), (0, 2)])
+ >>> is_planar, P = nx.check_planarity(G)
+ >>> print(is_planar)
+ True
+
+ When `G` is planar, a `PlanarEmbedding` instance is returned:
+
+ >>> P.get_data()
+ {0: [1, 2], 1: [0], 2: [0]}
+
+ Notes
+ -----
+ A (combinatorial) embedding consists of cyclic orderings of the incident
+ edges at each vertex. Given such an embedding there are multiple approaches
+ discussed in literature to drawing the graph (subject to various
+ constraints, e.g. integer coordinates), see e.g. [2].
+
+ The planarity check algorithm and extraction of the combinatorial embedding
+ is based on the Left-Right Planarity Test [1].
+
+ A counterexample is only generated if the corresponding parameter is set,
+ because the complexity of the counterexample generation is higher.
+
+ See also
+ --------
+ is_planar :
+ Check for planarity without creating a `PlanarEmbedding` or counterexample.
+
+ References
+ ----------
+ .. [1] Ulrik Brandes:
+ The Left-Right Planarity Test
+ 2009
+ http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.217.9208
+ .. [2] Takao Nishizeki, Md Saidur Rahman:
+ Planar graph drawing
+ Lecture Notes Series on Computing: Volume 12
+ 2004
+ """
+
+ planarity_state = LRPlanarity(G)
+ embedding = planarity_state.lr_planarity()
+ if embedding is None:
+ # graph is not planar
+ if counterexample:
+ return False, get_counterexample(G)
+ else:
+ return False, None
+ else:
+ # graph is planar
+ return True, embedding
+
+
+@nx._dispatchable(returns_graph=True)
+def check_planarity_recursive(G, counterexample=False):
+ """Recursive version of :meth:`check_planarity`."""
+ planarity_state = LRPlanarity(G)
+ embedding = planarity_state.lr_planarity_recursive()
+ if embedding is None:
+ # graph is not planar
+ if counterexample:
+ return False, get_counterexample_recursive(G)
+ else:
+ return False, None
+ else:
+ # graph is planar
+ return True, embedding
+
+
+@nx._dispatchable(returns_graph=True)
+def get_counterexample(G):
+ """Obtains a Kuratowski subgraph.
+
+ Raises nx.NetworkXException if G is planar.
+
+ The function removes edges such that the graph is still not planar.
+ At some point the removal of any edge would make the graph planar.
+ This subgraph must be a Kuratowski subgraph.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ Returns
+ -------
+ subgraph : NetworkX graph
+ A Kuratowski subgraph that proves that G is not planar.
+
+ """
+ # copy graph
+ G = nx.Graph(G)
+
+ if check_planarity(G)[0]:
+ raise nx.NetworkXException("G is planar - no counter example.")
+
+ # find Kuratowski subgraph
+ subgraph = nx.Graph()
+ for u in G:
+ nbrs = list(G[u])
+ for v in nbrs:
+ G.remove_edge(u, v)
+ if check_planarity(G)[0]:
+ G.add_edge(u, v)
+ subgraph.add_edge(u, v)
+
+ return subgraph
+
+
+@nx._dispatchable(returns_graph=True)
+def get_counterexample_recursive(G):
+ """Recursive version of :meth:`get_counterexample`."""
+
+ # copy graph
+ G = nx.Graph(G)
+
+ if check_planarity_recursive(G)[0]:
+ raise nx.NetworkXException("G is planar - no counter example.")
+
+ # find Kuratowski subgraph
+ subgraph = nx.Graph()
+ for u in G:
+ nbrs = list(G[u])
+ for v in nbrs:
+ G.remove_edge(u, v)
+ if check_planarity_recursive(G)[0]:
+ G.add_edge(u, v)
+ subgraph.add_edge(u, v)
+
+ return subgraph
+
+
+class Interval:
+ """Represents a set of return edges.
+
+ All return edges in an interval induce a same constraint on the contained
+ edges, which means that all edges must either have a left orientation or
+ all edges must have a right orientation.
+ """
+
+ def __init__(self, low=None, high=None):
+ self.low = low
+ self.high = high
+
+ def empty(self):
+ """Check if the interval is empty"""
+ return self.low is None and self.high is None
+
+ def copy(self):
+ """Returns a copy of this interval"""
+ return Interval(self.low, self.high)
+
+ def conflicting(self, b, planarity_state):
+ """Returns True if interval I conflicts with edge b"""
+ return (
+ not self.empty()
+ and planarity_state.lowpt[self.high] > planarity_state.lowpt[b]
+ )
+
+
+class ConflictPair:
+ """Represents a different constraint between two intervals.
+
+ The edges in the left interval must have a different orientation than
+ the one in the right interval.
+ """
+
+ def __init__(self, left=Interval(), right=Interval()):
+ self.left = left
+ self.right = right
+
+ def swap(self):
+ """Swap left and right intervals"""
+ temp = self.left
+ self.left = self.right
+ self.right = temp
+
+ def lowest(self, planarity_state):
+ """Returns the lowest lowpoint of a conflict pair"""
+ if self.left.empty():
+ return planarity_state.lowpt[self.right.low]
+ if self.right.empty():
+ return planarity_state.lowpt[self.left.low]
+ return min(
+ planarity_state.lowpt[self.left.low], planarity_state.lowpt[self.right.low]
+ )
+
+
+def top_of_stack(l):
+ """Returns the element on top of the stack."""
+ if not l:
+ return None
+ return l[-1]
+
+
+class LRPlanarity:
+ """A class to maintain the state during planarity check."""
+
+ __slots__ = [
+ "G",
+ "roots",
+ "height",
+ "lowpt",
+ "lowpt2",
+ "nesting_depth",
+ "parent_edge",
+ "DG",
+ "adjs",
+ "ordered_adjs",
+ "ref",
+ "side",
+ "S",
+ "stack_bottom",
+ "lowpt_edge",
+ "left_ref",
+ "right_ref",
+ "embedding",
+ ]
+
+ def __init__(self, G):
+ # copy G without adding self-loops
+ self.G = nx.Graph()
+ self.G.add_nodes_from(G.nodes)
+ for e in G.edges:
+ if e[0] != e[1]:
+ self.G.add_edge(e[0], e[1])
+
+ self.roots = []
+
+ # distance from tree root
+ self.height = defaultdict(lambda: None)
+
+ self.lowpt = {} # height of lowest return point of an edge
+ self.lowpt2 = {} # height of second lowest return point
+ self.nesting_depth = {} # for nesting order
+
+ # None -> missing edge
+ self.parent_edge = defaultdict(lambda: None)
+
+ # oriented DFS graph
+ self.DG = nx.DiGraph()
+ self.DG.add_nodes_from(G.nodes)
+
+ self.adjs = {}
+ self.ordered_adjs = {}
+
+ self.ref = defaultdict(lambda: None)
+ self.side = defaultdict(lambda: 1)
+
+ # stack of conflict pairs
+ self.S = []
+ self.stack_bottom = {}
+ self.lowpt_edge = {}
+
+ self.left_ref = {}
+ self.right_ref = {}
+
+ self.embedding = PlanarEmbedding()
+
+ def lr_planarity(self):
+ """Execute the LR planarity test.
+
+ Returns
+ -------
+ embedding : dict
+ If the graph is planar an embedding is returned. Otherwise None.
+ """
+ if self.G.order() > 2 and self.G.size() > 3 * self.G.order() - 6:
+ # graph is not planar
+ return None
+
+ # make adjacency lists for dfs
+ for v in self.G:
+ self.adjs[v] = list(self.G[v])
+
+ # orientation of the graph by depth first search traversal
+ for v in self.G:
+ if self.height[v] is None:
+ self.height[v] = 0
+ self.roots.append(v)
+ self.dfs_orientation(v)
+
+ # Free no longer used variables
+ self.G = None
+ self.lowpt2 = None
+ self.adjs = None
+
+ # testing
+ for v in self.DG: # sort the adjacency lists by nesting depth
+ # note: this sorting leads to non linear time
+ self.ordered_adjs[v] = sorted(
+ self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
+ )
+ for v in self.roots:
+ if not self.dfs_testing(v):
+ return None
+
+ # Free no longer used variables
+ self.height = None
+ self.lowpt = None
+ self.S = None
+ self.stack_bottom = None
+ self.lowpt_edge = None
+
+ for e in self.DG.edges:
+ self.nesting_depth[e] = self.sign(e) * self.nesting_depth[e]
+
+ self.embedding.add_nodes_from(self.DG.nodes)
+ for v in self.DG:
+ # sort the adjacency lists again
+ self.ordered_adjs[v] = sorted(
+ self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
+ )
+ # initialize the embedding
+ previous_node = None
+ for w in self.ordered_adjs[v]:
+ self.embedding.add_half_edge(v, w, ccw=previous_node)
+ previous_node = w
+
+ # Free no longer used variables
+ self.DG = None
+ self.nesting_depth = None
+ self.ref = None
+
+ # compute the complete embedding
+ for v in self.roots:
+ self.dfs_embedding(v)
+
+ # Free no longer used variables
+ self.roots = None
+ self.parent_edge = None
+ self.ordered_adjs = None
+ self.left_ref = None
+ self.right_ref = None
+ self.side = None
+
+ return self.embedding
+
+ def lr_planarity_recursive(self):
+ """Recursive version of :meth:`lr_planarity`."""
+ if self.G.order() > 2 and self.G.size() > 3 * self.G.order() - 6:
+ # graph is not planar
+ return None
+
+ # orientation of the graph by depth first search traversal
+ for v in self.G:
+ if self.height[v] is None:
+ self.height[v] = 0
+ self.roots.append(v)
+ self.dfs_orientation_recursive(v)
+
+ # Free no longer used variable
+ self.G = None
+
+ # testing
+ for v in self.DG: # sort the adjacency lists by nesting depth
+ # note: this sorting leads to non linear time
+ self.ordered_adjs[v] = sorted(
+ self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
+ )
+ for v in self.roots:
+ if not self.dfs_testing_recursive(v):
+ return None
+
+ for e in self.DG.edges:
+ self.nesting_depth[e] = self.sign_recursive(e) * self.nesting_depth[e]
+
+ self.embedding.add_nodes_from(self.DG.nodes)
+ for v in self.DG:
+ # sort the adjacency lists again
+ self.ordered_adjs[v] = sorted(
+ self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
+ )
+ # initialize the embedding
+ previous_node = None
+ for w in self.ordered_adjs[v]:
+ self.embedding.add_half_edge(v, w, ccw=previous_node)
+ previous_node = w
+
+ # compute the complete embedding
+ for v in self.roots:
+ self.dfs_embedding_recursive(v)
+
+ return self.embedding
+
+ def dfs_orientation(self, v):
+ """Orient the graph by DFS, compute lowpoints and nesting order."""
+ # the recursion stack
+ dfs_stack = [v]
+ # index of next edge to handle in adjacency list of each node
+ ind = defaultdict(lambda: 0)
+ # boolean to indicate whether to skip the initial work for an edge
+ skip_init = defaultdict(lambda: False)
+
+ while dfs_stack:
+ v = dfs_stack.pop()
+ e = self.parent_edge[v]
+
+ for w in self.adjs[v][ind[v] :]:
+ vw = (v, w)
+
+ if not skip_init[vw]:
+ if (v, w) in self.DG.edges or (w, v) in self.DG.edges:
+ ind[v] += 1
+ continue # the edge was already oriented
+
+ self.DG.add_edge(v, w) # orient the edge
+
+ self.lowpt[vw] = self.height[v]
+ self.lowpt2[vw] = self.height[v]
+ if self.height[w] is None: # (v, w) is a tree edge
+ self.parent_edge[w] = vw
+ self.height[w] = self.height[v] + 1
+
+ dfs_stack.append(v) # revisit v after finishing w
+ dfs_stack.append(w) # visit w next
+ skip_init[vw] = True # don't redo this block
+ break # handle next node in dfs_stack (i.e. w)
+ else: # (v, w) is a back edge
+ self.lowpt[vw] = self.height[w]
+
+ # determine nesting graph
+ self.nesting_depth[vw] = 2 * self.lowpt[vw]
+ if self.lowpt2[vw] < self.height[v]: # chordal
+ self.nesting_depth[vw] += 1
+
+ # update lowpoints of parent edge e
+ if e is not None:
+ if self.lowpt[vw] < self.lowpt[e]:
+ self.lowpt2[e] = min(self.lowpt[e], self.lowpt2[vw])
+ self.lowpt[e] = self.lowpt[vw]
+ elif self.lowpt[vw] > self.lowpt[e]:
+ self.lowpt2[e] = min(self.lowpt2[e], self.lowpt[vw])
+ else:
+ self.lowpt2[e] = min(self.lowpt2[e], self.lowpt2[vw])
+
+ ind[v] += 1
+
+ def dfs_orientation_recursive(self, v):
+ """Recursive version of :meth:`dfs_orientation`."""
+ e = self.parent_edge[v]
+ for w in self.G[v]:
+ if (v, w) in self.DG.edges or (w, v) in self.DG.edges:
+ continue # the edge was already oriented
+ vw = (v, w)
+ self.DG.add_edge(v, w) # orient the edge
+
+ self.lowpt[vw] = self.height[v]
+ self.lowpt2[vw] = self.height[v]
+ if self.height[w] is None: # (v, w) is a tree edge
+ self.parent_edge[w] = vw
+ self.height[w] = self.height[v] + 1
+ self.dfs_orientation_recursive(w)
+ else: # (v, w) is a back edge
+ self.lowpt[vw] = self.height[w]
+
+ # determine nesting graph
+ self.nesting_depth[vw] = 2 * self.lowpt[vw]
+ if self.lowpt2[vw] < self.height[v]: # chordal
+ self.nesting_depth[vw] += 1
+
+ # update lowpoints of parent edge e
+ if e is not None:
+ if self.lowpt[vw] < self.lowpt[e]:
+ self.lowpt2[e] = min(self.lowpt[e], self.lowpt2[vw])
+ self.lowpt[e] = self.lowpt[vw]
+ elif self.lowpt[vw] > self.lowpt[e]:
+ self.lowpt2[e] = min(self.lowpt2[e], self.lowpt[vw])
+ else:
+ self.lowpt2[e] = min(self.lowpt2[e], self.lowpt2[vw])
+
+ def dfs_testing(self, v):
+ """Test for LR partition."""
+ # the recursion stack
+ dfs_stack = [v]
+ # index of next edge to handle in adjacency list of each node
+ ind = defaultdict(lambda: 0)
+ # boolean to indicate whether to skip the initial work for an edge
+ skip_init = defaultdict(lambda: False)
+
+ while dfs_stack:
+ v = dfs_stack.pop()
+ e = self.parent_edge[v]
+ # to indicate whether to skip the final block after the for loop
+ skip_final = False
+
+ for w in self.ordered_adjs[v][ind[v] :]:
+ ei = (v, w)
+
+ if not skip_init[ei]:
+ self.stack_bottom[ei] = top_of_stack(self.S)
+
+ if ei == self.parent_edge[w]: # tree edge
+ dfs_stack.append(v) # revisit v after finishing w
+ dfs_stack.append(w) # visit w next
+ skip_init[ei] = True # don't redo this block
+ skip_final = True # skip final work after breaking
+ break # handle next node in dfs_stack (i.e. w)
+ else: # back edge
+ self.lowpt_edge[ei] = ei
+ self.S.append(ConflictPair(right=Interval(ei, ei)))
+
+ # integrate new return edges
+ if self.lowpt[ei] < self.height[v]:
+ if w == self.ordered_adjs[v][0]: # e_i has return edge
+ self.lowpt_edge[e] = self.lowpt_edge[ei]
+ else: # add constraints of e_i
+ if not self.add_constraints(ei, e):
+ # graph is not planar
+ return False
+
+ ind[v] += 1
+
+ if not skip_final:
+ # remove back edges returning to parent
+ if e is not None: # v isn't root
+ self.remove_back_edges(e)
+
+ return True
+
+ def dfs_testing_recursive(self, v):
+ """Recursive version of :meth:`dfs_testing`."""
+ e = self.parent_edge[v]
+ for w in self.ordered_adjs[v]:
+ ei = (v, w)
+ self.stack_bottom[ei] = top_of_stack(self.S)
+ if ei == self.parent_edge[w]: # tree edge
+ if not self.dfs_testing_recursive(w):
+ return False
+ else: # back edge
+ self.lowpt_edge[ei] = ei
+ self.S.append(ConflictPair(right=Interval(ei, ei)))
+
+ # integrate new return edges
+ if self.lowpt[ei] < self.height[v]:
+ if w == self.ordered_adjs[v][0]: # e_i has return edge
+ self.lowpt_edge[e] = self.lowpt_edge[ei]
+ else: # add constraints of e_i
+ if not self.add_constraints(ei, e):
+ # graph is not planar
+ return False
+
+ # remove back edges returning to parent
+ if e is not None: # v isn't root
+ self.remove_back_edges(e)
+ return True
+
+ def add_constraints(self, ei, e):
+ P = ConflictPair()
+ # merge return edges of e_i into P.right
+ while True:
+ Q = self.S.pop()
+ if not Q.left.empty():
+ Q.swap()
+ if not Q.left.empty(): # not planar
+ return False
+ if self.lowpt[Q.right.low] > self.lowpt[e]:
+ # merge intervals
+ if P.right.empty(): # topmost interval
+ P.right = Q.right.copy()
+ else:
+ self.ref[P.right.low] = Q.right.high
+ P.right.low = Q.right.low
+ else: # align
+ self.ref[Q.right.low] = self.lowpt_edge[e]
+ if top_of_stack(self.S) == self.stack_bottom[ei]:
+ break
+ # merge conflicting return edges of e_1,...,e_i-1 into P.L
+ while top_of_stack(self.S).left.conflicting(ei, self) or top_of_stack(
+ self.S
+ ).right.conflicting(ei, self):
+ Q = self.S.pop()
+ if Q.right.conflicting(ei, self):
+ Q.swap()
+ if Q.right.conflicting(ei, self): # not planar
+ return False
+ # merge interval below lowpt(e_i) into P.R
+ self.ref[P.right.low] = Q.right.high
+ if Q.right.low is not None:
+ P.right.low = Q.right.low
+
+ if P.left.empty(): # topmost interval
+ P.left = Q.left.copy()
+ else:
+ self.ref[P.left.low] = Q.left.high
+ P.left.low = Q.left.low
+
+ if not (P.left.empty() and P.right.empty()):
+ self.S.append(P)
+ return True
+
+ def remove_back_edges(self, e):
+ u = e[0]
+ # trim back edges ending at parent u
+ # drop entire conflict pairs
+ while self.S and top_of_stack(self.S).lowest(self) == self.height[u]:
+ P = self.S.pop()
+ if P.left.low is not None:
+ self.side[P.left.low] = -1
+
+ if self.S: # one more conflict pair to consider
+ P = self.S.pop()
+ # trim left interval
+ while P.left.high is not None and P.left.high[1] == u:
+ P.left.high = self.ref[P.left.high]
+ if P.left.high is None and P.left.low is not None:
+ # just emptied
+ self.ref[P.left.low] = P.right.low
+ self.side[P.left.low] = -1
+ P.left.low = None
+ # trim right interval
+ while P.right.high is not None and P.right.high[1] == u:
+ P.right.high = self.ref[P.right.high]
+ if P.right.high is None and P.right.low is not None:
+ # just emptied
+ self.ref[P.right.low] = P.left.low
+ self.side[P.right.low] = -1
+ P.right.low = None
+ self.S.append(P)
+
+ # side of e is side of a highest return edge
+ if self.lowpt[e] < self.height[u]: # e has return edge
+ hl = top_of_stack(self.S).left.high
+ hr = top_of_stack(self.S).right.high
+
+ if hl is not None and (hr is None or self.lowpt[hl] > self.lowpt[hr]):
+ self.ref[e] = hl
+ else:
+ self.ref[e] = hr
+
+ def dfs_embedding(self, v):
+ """Completes the embedding."""
+ # the recursion stack
+ dfs_stack = [v]
+ # index of next edge to handle in adjacency list of each node
+ ind = defaultdict(lambda: 0)
+
+ while dfs_stack:
+ v = dfs_stack.pop()
+
+ for w in self.ordered_adjs[v][ind[v] :]:
+ ind[v] += 1
+ ei = (v, w)
+
+ if ei == self.parent_edge[w]: # tree edge
+ self.embedding.add_half_edge_first(w, v)
+ self.left_ref[v] = w
+ self.right_ref[v] = w
+
+ dfs_stack.append(v) # revisit v after finishing w
+ dfs_stack.append(w) # visit w next
+ break # handle next node in dfs_stack (i.e. w)
+ else: # back edge
+ if self.side[ei] == 1:
+ self.embedding.add_half_edge(w, v, ccw=self.right_ref[w])
+ else:
+ self.embedding.add_half_edge(w, v, cw=self.left_ref[w])
+ self.left_ref[w] = v
+
+ def dfs_embedding_recursive(self, v):
+ """Recursive version of :meth:`dfs_embedding`."""
+ for w in self.ordered_adjs[v]:
+ ei = (v, w)
+ if ei == self.parent_edge[w]: # tree edge
+ self.embedding.add_half_edge_first(w, v)
+ self.left_ref[v] = w
+ self.right_ref[v] = w
+ self.dfs_embedding_recursive(w)
+ 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(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(w, v, cw=self.left_ref[w])
+ self.left_ref[w] = v
+
+ def sign(self, e):
+ """Resolve the relative side of an edge to the absolute side."""
+ # the recursion stack
+ dfs_stack = [e]
+ # dict to remember reference edges
+ old_ref = defaultdict(lambda: None)
+
+ while dfs_stack:
+ e = dfs_stack.pop()
+
+ if self.ref[e] is not None:
+ dfs_stack.append(e) # revisit e after finishing self.ref[e]
+ dfs_stack.append(self.ref[e]) # visit self.ref[e] next
+ old_ref[e] = self.ref[e] # remember value of self.ref[e]
+ self.ref[e] = None
+ else:
+ self.side[e] *= self.side[old_ref[e]]
+
+ return self.side[e]
+
+ def sign_recursive(self, e):
+ """Recursive version of :meth:`sign`."""
+ if self.ref[e] is not None:
+ self.side[e] = self.side[e] * self.sign_recursive(self.ref[e])
+ self.ref[e] = None
+ return self.side[e]
+
+
+class PlanarEmbedding(nx.DiGraph):
+ """Represents a planar graph with its planar embedding.
+
+ The planar embedding is given by a `combinatorial embedding
+ <https://en.wikipedia.org/wiki/Graph_embedding#Combinatorial_embedding>`_.
+
+ .. note:: `check_planarity` is the preferred way to check if a graph is planar.
+
+ **Neighbor ordering:**
+
+ In comparison to a usual graph structure, the embedding also stores the
+ order of all neighbors for every vertex.
+ The order of the neighbors can be given in clockwise (cw) direction or
+ counterclockwise (ccw) direction. This order is stored as edge attributes
+ in the underlying directed graph. For the edge (u, v) the edge attribute
+ 'cw' is set to the neighbor of u that follows immediately after v in
+ clockwise direction.
+
+ In order for a PlanarEmbedding to be valid it must fulfill multiple
+ conditions. It is possible to check if these conditions are fulfilled with
+ the method :meth:`check_structure`.
+ The conditions are:
+
+ * 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.
+
+ As long as a PlanarEmbedding is invalid only the following methods should
+ be called:
+
+ * :meth:`add_half_edge`
+ * :meth:`connect_components`
+
+ Even though the graph is a subclass of nx.DiGraph, it can still be used
+ for algorithms that require undirected graphs, because the method
+ :meth:`is_directed` is overridden. This is possible, because a valid
+ PlanarGraph must have edges in both directions.
+
+ **Half edges:**
+
+ 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 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
+ --------
+ is_planar :
+ Preferred way to check if an existing graph is planar.
+
+ check_planarity :
+ A convenient way to create a `PlanarEmbedding`. If not planar,
+ it returns a subgraph that shows this.
+
+ Examples
+ --------
+
+ Create an embedding of a star graph (compare `nx.star_graph(3)`):
+
+ >>> G = nx.PlanarEmbedding()
+ >>> 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(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:
+
+ >>> G.check_structure()
+
+ """
+
+ 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.
+
+ Returns
+ -------
+ embedding : dict
+ A dict mapping all nodes to a list of neighbors sorted in
+ clockwise order.
+
+ See Also
+ --------
+ set_data
+
+ """
+ embedding = {}
+ for v in self:
+ embedding[v] = list(self.neighbors_cw_order(v))
+ return embedding
+
+ def set_data(self, data):
+ """Inserts edges according to given sorted neighbor list.
+
+ The input format is the same as the output format of get_data().
+
+ Parameters
+ ----------
+ data : dict
+ A dict mapping all nodes to a list of neighbors sorted in
+ clockwise order.
+
+ See Also
+ --------
+ get_data
+
+ """
+ for v in data:
+ ref = None
+ for w in reversed(data[v]):
+ 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
+ nx._clear_cache(self)
+
+ 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.
+
+ Parameters
+ ----------
+ v : node
+
+ Yields
+ ------
+ node
+
+ """
+ succs = self._succ[v]
+ if not succs:
+ # v has no neighbors
+ return
+ start_node = next(reversed(succs))
+ yield start_node
+ current_node = succs[start_node]["cw"]
+ while start_node != current_node:
+ yield current_node
+ current_node = succs[current_node]["cw"]
+
+ def add_half_edge(self, start_node, end_node, *, cw=None, ccw=None):
+ """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`/`ccw`
+ 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.
+ Omit or pass `None` if adding the first out-half-edge of `start_node`.
+
+
+ Raises
+ ------
+ NetworkXException
+ If the `cw` or `ccw` node is not a successor of `start_node`.
+ If `start_node` has successors, but neither `cw` or `ccw` is provided.
+ 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"]
+ 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
+ # 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"]
+ 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
+ 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]
+ 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
+ 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.
+
+ Checks that the following properties are fulfilled:
+
+ * 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.
+
+ Running this method verifies that the underlying Graph must be planar.
+
+ Raises
+ ------
+ NetworkXException
+ This exception is raised with a short explanation if the
+ PlanarEmbedding is invalid.
+ """
+ # Check fundamental structure
+ for v in self:
+ try:
+ sorted_nbrs = set(self.neighbors_cw_order(v))
+ except KeyError as err:
+ msg = f"Bad embedding. Missing orientation for a neighbor of {v}"
+ raise nx.NetworkXException(msg) from err
+
+ unsorted_nbrs = set(self[v])
+ if sorted_nbrs != unsorted_nbrs:
+ msg = "Bad embedding. Edge orientations not set correctly."
+ raise nx.NetworkXException(msg)
+ for w in self[v]:
+ # Check if opposite half-edge exists
+ if not self.has_edge(w, v):
+ msg = "Bad embedding. Opposite half-edge is missing."
+ raise nx.NetworkXException(msg)
+
+ # Check planarity
+ counted_half_edges = set()
+ for component in nx.connected_components(self):
+ if len(component) == 1:
+ # Don't need to check single node component
+ continue
+ num_nodes = len(component)
+ num_half_edges = 0
+ num_faces = 0
+ for v in component:
+ for w in self.neighbors_cw_order(v):
+ num_half_edges += 1
+ if (v, w) not in counted_half_edges:
+ # We encountered a new face
+ num_faces += 1
+ # Mark all half-edges belonging to this face
+ self.traverse_face(v, w, counted_half_edges)
+ num_edges = num_half_edges // 2 # num_half_edges is even
+ if num_nodes - num_edges + num_faces != 2:
+ # The result does not match Euler's formula
+ msg = "Bad embedding. The graph does not match Euler's formula"
+ raise nx.NetworkXException(msg)
+
+ def add_half_edge_ccw(self, start_node, end_node, reference_neighbor):
+ """Adds a half-edge from start_node to end_node.
+
+ The half-edge is added counter clockwise next to the existing half-edge
+ (start_node, reference_neighbor).
+
+ Parameters
+ ----------
+ start_node : node
+ Start node of inserted edge.
+ end_node : node
+ End node of inserted edge.
+ reference_neighbor: node
+ End node of reference edge.
+
+ Raises
+ ------
+ NetworkXException
+ If the reference_neighbor does not exist.
+
+ See Also
+ --------
+ add_half_edge
+ add_half_edge_cw
+ connect_components
+
+ """
+ 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.
+
+ The half-edge is added clockwise next to the existing half-edge
+ (start_node, reference_neighbor).
+
+ Parameters
+ ----------
+ start_node : node
+ Start node of inserted edge.
+ end_node : node
+ End node of inserted edge.
+ reference_neighbor: node
+ End node of reference edge.
+
+ Raises
+ ------
+ NetworkXException
+ If the reference_neighbor does not exist.
+
+ See Also
+ --------
+ add_half_edge
+ add_half_edge_ccw
+ connect_components
+ """
+ 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
+ nx._clear_cache(self)
+
+ 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.
+
+ This method should only be called if v and w are in different
+ components, or it might break the embedding.
+ This especially means that if `connect_components(v, w)`
+ is called it is not allowed to call `connect_components(w, v)`
+ afterwards. The neighbor orientations in both directions are
+ all set correctly after the first call.
+
+ Parameters
+ ----------
+ v : node
+ w : node
+
+ See Also
+ --------
+ add_half_edge
+ """
+ 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):
+ """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
+ ----------
+ start_node : node
+ end_node : node
+
+ See Also
+ --------
+ add_half_edge
+ connect_components
+ """
+ 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.
+
+ Parameters
+ ----------
+ v : node
+ w : node
+
+ Returns
+ -------
+ half-edge : tuple
+ """
+ new_node = self[w][v]["ccw"]
+ return w, new_node
+
+ def traverse_face(self, v, w, mark_half_edges=None):
+ """Returns nodes on the face that belong to the half-edge (v, w).
+
+ The face that is traversed lies to the right of the half-edge (in an
+ orientation where v is below w).
+
+ Optionally it is possible to pass a set to which all encountered half
+ edges are added. Before calling this method, this set must not include
+ any half-edges that belong to the face.
+
+ Parameters
+ ----------
+ v : node
+ Start node of half-edge.
+ w : node
+ End node of half-edge.
+ mark_half_edges: set, optional
+ Set to which all encountered half-edges are added.
+
+ Returns
+ -------
+ face : list
+ A list of nodes that lie on this face.
+ """
+ if mark_half_edges is None:
+ mark_half_edges = set()
+
+ face_nodes = [v]
+ mark_half_edges.add((v, w))
+ prev_node = v
+ cur_node = w
+ # Last half-edge is (incoming_node, v)
+ incoming_node = self[v][w]["cw"]
+
+ while cur_node != v or prev_node != incoming_node:
+ face_nodes.append(cur_node)
+ prev_node, cur_node = self.next_face_half_edge(prev_node, cur_node)
+ if (prev_node, cur_node) in mark_half_edges:
+ raise nx.NetworkXException("Bad planar embedding. Impossible face.")
+ mark_half_edges.add((prev_node, cur_node))
+
+ return face_nodes
+
+ def is_directed(self):
+ """A valid PlanarEmbedding is undirected.
+
+ All reverse edges are contained, i.e. for every existing
+ half-edge (v, w) the half-edge in the opposite direction (w, v) is also
+ contained.
+ """
+ return False
+
+ def copy(self, as_view=False):
+ if as_view is True:
+ return nx.graphviews.generic_graph_view(self)
+ G = self.__class__()
+ G.graph.update(self.graph)
+ G.add_nodes_from((n, d.copy()) for n, d in self._node.items())
+ super(self.__class__, G).add_edges_from(
+ (u, v, datadict.copy())
+ for u, nbrs in self._adj.items()
+ for v, datadict in nbrs.items()
+ )
+ return G