Spaces:
Sleeping
Sleeping
from collections import defaultdict | |
import networkx as nx | |
__all__ = ["check_planarity", "is_planar", "PlanarEmbedding"] | |
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] | |
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 | |
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 | |
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 | |
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_cw(v, w, 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_cw(v, w, 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_cw(w, v, self.right_ref[w]) | |
else: | |
self.embedding.add_half_edge_ccw(w, v, 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_cw(w, v, 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.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. | |
* 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:`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 | |
: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_ccw` 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 | |
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_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) | |
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) | |
After creating a graph, it is possible to validate that the PlanarEmbedding | |
object is correct: | |
>>> G.check_structure() | |
""" | |
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: | |
for w in reversed(data[v]): | |
self.add_half_edge_first(v, w) | |
def neighbors_cw_order(self, v): | |
"""Generator for the neighbors of v in clockwise order. | |
Parameters | |
---------- | |
v : node | |
Yields | |
------ | |
node | |
""" | |
if len(self[v]) == 0: | |
# v has no neighbors | |
return | |
start_node = self.nodes[v]["first_nbr"] | |
yield start_node | |
current_node = self[v][start_node]["cw"] | |
while start_node != current_node: | |
yield current_node | |
current_node = self[v][current_node]["cw"] | |
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. | |
* 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. | |
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_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 | |
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_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 | |
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_ccw | |
add_half_edge_cw | |
add_half_edge_first | |
""" | |
self.add_half_edge_first(v, w) | |
self.add_half_edge_first(w, v) | |
def add_half_edge_first(self, start_node, end_node): | |
"""The added half-edge is inserted at the first position in the order. | |
Parameters | |
---------- | |
start_node : node | |
end_node : node | |
See Also | |
-------- | |
add_half_edge_ccw | |
add_half_edge_cw | |
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) | |
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 | |