| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463 |
- from collections import defaultdict
- from copy import deepcopy
- 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
- def to_undirected(self, reciprocal=False, as_view=False):
- """
- Returns a non-embedding undirected representation of the graph.
- This method strips the planar embedding information and provides
- a simple undirected graph representation. While creating the undirected graph,
- all edge attributes are retained except the ``"cw"`` and ``"ccw"`` attributes
- which are removed from the edge data. Those attributes are specific to
- the requirements of planar embeddings.
- Parameters
- ----------
- reciprocal : bool (optional)
- Not supported for PlanarEmbedding. This parameter raises an exception
- if used. All valid embeddings include reciprocal half-edges by definition,
- making this parameter unnecessary.
- as_view : bool (optional, default=False)
- Not supported for PlanarEmbedding. This parameter raises an exception
- if used.
- Returns
- -------
- G : Graph
- An undirected graph with the same name and nodes as the PlanarEmbedding.
- Edges are included with their data, except for the ``"cw"`` and ``"ccw"``
- attributes, which are omitted.
- Notes
- -----
- - If edges exist in both directions ``(u, v)`` and ``(v, u)`` in the PlanarEmbedding,
- attributes for the resulting undirected edge will be combined, excluding ``"cw"``
- and ``"ccw"``.
- - A deep copy is made of the other edge attributes as well as the
- node and graph attributes, ensuring independence of the resulting graph.
- - Subclass-specific data structures used in the original graph may not transfer
- to the undirected graph. The resulting graph will be of type ``nx.Graph``.
- """
- if reciprocal:
- raise ValueError(
- "'reciprocal=True' is not supported for PlanarEmbedding.\n"
- "All valid embeddings include reciprocal half-edges by definition,\n"
- "making this parameter unnecessary."
- )
- if as_view:
- raise ValueError("'as_view=True' is not supported for PlanarEmbedding.")
- graph_class = self.to_undirected_class()
- G = graph_class()
- G.graph.update(deepcopy(self.graph))
- G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items())
- G.add_edges_from(
- (u, v, {k: deepcopy(v) for k, v in d.items() if k not in {"cw", "ccw"}})
- for u, nbrs in self._adj.items()
- for v, d in nbrs.items()
- )
- return G
|