planarity.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174
  1. from collections import defaultdict
  2. import networkx as nx
  3. __all__ = ["check_planarity", "is_planar", "PlanarEmbedding"]
  4. def is_planar(G):
  5. """Returns True if and only if `G` is planar.
  6. A graph is *planar* iff it can be drawn in a plane without
  7. any edge intersections.
  8. Parameters
  9. ----------
  10. G : NetworkX graph
  11. Returns
  12. -------
  13. bool
  14. Whether the graph is planar.
  15. Examples
  16. --------
  17. >>> G = nx.Graph([(0, 1), (0, 2)])
  18. >>> nx.is_planar(G)
  19. True
  20. >>> nx.is_planar(nx.complete_graph(5))
  21. False
  22. See Also
  23. --------
  24. check_planarity :
  25. Check if graph is planar *and* return a `PlanarEmbedding` instance if True.
  26. """
  27. return check_planarity(G, counterexample=False)[0]
  28. def check_planarity(G, counterexample=False):
  29. """Check if a graph is planar and return a counterexample or an embedding.
  30. A graph is planar iff it can be drawn in a plane without
  31. any edge intersections.
  32. Parameters
  33. ----------
  34. G : NetworkX graph
  35. counterexample : bool
  36. A Kuratowski subgraph (to proof non planarity) is only returned if set
  37. to true.
  38. Returns
  39. -------
  40. (is_planar, certificate) : (bool, NetworkX graph) tuple
  41. is_planar is true if the graph is planar.
  42. If the graph is planar `certificate` is a PlanarEmbedding
  43. otherwise it is a Kuratowski subgraph.
  44. Examples
  45. --------
  46. >>> G = nx.Graph([(0, 1), (0, 2)])
  47. >>> is_planar, P = nx.check_planarity(G)
  48. >>> print(is_planar)
  49. True
  50. When `G` is planar, a `PlanarEmbedding` instance is returned:
  51. >>> P.get_data()
  52. {0: [1, 2], 1: [0], 2: [0]}
  53. Notes
  54. -----
  55. A (combinatorial) embedding consists of cyclic orderings of the incident
  56. edges at each vertex. Given such an embedding there are multiple approaches
  57. discussed in literature to drawing the graph (subject to various
  58. constraints, e.g. integer coordinates), see e.g. [2].
  59. The planarity check algorithm and extraction of the combinatorial embedding
  60. is based on the Left-Right Planarity Test [1].
  61. A counterexample is only generated if the corresponding parameter is set,
  62. because the complexity of the counterexample generation is higher.
  63. See also
  64. --------
  65. is_planar :
  66. Check for planarity without creating a `PlanarEmbedding` or counterexample.
  67. References
  68. ----------
  69. .. [1] Ulrik Brandes:
  70. The Left-Right Planarity Test
  71. 2009
  72. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.217.9208
  73. .. [2] Takao Nishizeki, Md Saidur Rahman:
  74. Planar graph drawing
  75. Lecture Notes Series on Computing: Volume 12
  76. 2004
  77. """
  78. planarity_state = LRPlanarity(G)
  79. embedding = planarity_state.lr_planarity()
  80. if embedding is None:
  81. # graph is not planar
  82. if counterexample:
  83. return False, get_counterexample(G)
  84. else:
  85. return False, None
  86. else:
  87. # graph is planar
  88. return True, embedding
  89. def check_planarity_recursive(G, counterexample=False):
  90. """Recursive version of :meth:`check_planarity`."""
  91. planarity_state = LRPlanarity(G)
  92. embedding = planarity_state.lr_planarity_recursive()
  93. if embedding is None:
  94. # graph is not planar
  95. if counterexample:
  96. return False, get_counterexample_recursive(G)
  97. else:
  98. return False, None
  99. else:
  100. # graph is planar
  101. return True, embedding
  102. def get_counterexample(G):
  103. """Obtains a Kuratowski subgraph.
  104. Raises nx.NetworkXException if G is planar.
  105. The function removes edges such that the graph is still not planar.
  106. At some point the removal of any edge would make the graph planar.
  107. This subgraph must be a Kuratowski subgraph.
  108. Parameters
  109. ----------
  110. G : NetworkX graph
  111. Returns
  112. -------
  113. subgraph : NetworkX graph
  114. A Kuratowski subgraph that proves that G is not planar.
  115. """
  116. # copy graph
  117. G = nx.Graph(G)
  118. if check_planarity(G)[0]:
  119. raise nx.NetworkXException("G is planar - no counter example.")
  120. # find Kuratowski subgraph
  121. subgraph = nx.Graph()
  122. for u in G:
  123. nbrs = list(G[u])
  124. for v in nbrs:
  125. G.remove_edge(u, v)
  126. if check_planarity(G)[0]:
  127. G.add_edge(u, v)
  128. subgraph.add_edge(u, v)
  129. return subgraph
  130. def get_counterexample_recursive(G):
  131. """Recursive version of :meth:`get_counterexample`."""
  132. # copy graph
  133. G = nx.Graph(G)
  134. if check_planarity_recursive(G)[0]:
  135. raise nx.NetworkXException("G is planar - no counter example.")
  136. # find Kuratowski subgraph
  137. subgraph = nx.Graph()
  138. for u in G:
  139. nbrs = list(G[u])
  140. for v in nbrs:
  141. G.remove_edge(u, v)
  142. if check_planarity_recursive(G)[0]:
  143. G.add_edge(u, v)
  144. subgraph.add_edge(u, v)
  145. return subgraph
  146. class Interval:
  147. """Represents a set of return edges.
  148. All return edges in an interval induce a same constraint on the contained
  149. edges, which means that all edges must either have a left orientation or
  150. all edges must have a right orientation.
  151. """
  152. def __init__(self, low=None, high=None):
  153. self.low = low
  154. self.high = high
  155. def empty(self):
  156. """Check if the interval is empty"""
  157. return self.low is None and self.high is None
  158. def copy(self):
  159. """Returns a copy of this interval"""
  160. return Interval(self.low, self.high)
  161. def conflicting(self, b, planarity_state):
  162. """Returns True if interval I conflicts with edge b"""
  163. return (
  164. not self.empty()
  165. and planarity_state.lowpt[self.high] > planarity_state.lowpt[b]
  166. )
  167. class ConflictPair:
  168. """Represents a different constraint between two intervals.
  169. The edges in the left interval must have a different orientation than
  170. the one in the right interval.
  171. """
  172. def __init__(self, left=Interval(), right=Interval()):
  173. self.left = left
  174. self.right = right
  175. def swap(self):
  176. """Swap left and right intervals"""
  177. temp = self.left
  178. self.left = self.right
  179. self.right = temp
  180. def lowest(self, planarity_state):
  181. """Returns the lowest lowpoint of a conflict pair"""
  182. if self.left.empty():
  183. return planarity_state.lowpt[self.right.low]
  184. if self.right.empty():
  185. return planarity_state.lowpt[self.left.low]
  186. return min(
  187. planarity_state.lowpt[self.left.low], planarity_state.lowpt[self.right.low]
  188. )
  189. def top_of_stack(l):
  190. """Returns the element on top of the stack."""
  191. if not l:
  192. return None
  193. return l[-1]
  194. class LRPlanarity:
  195. """A class to maintain the state during planarity check."""
  196. __slots__ = [
  197. "G",
  198. "roots",
  199. "height",
  200. "lowpt",
  201. "lowpt2",
  202. "nesting_depth",
  203. "parent_edge",
  204. "DG",
  205. "adjs",
  206. "ordered_adjs",
  207. "ref",
  208. "side",
  209. "S",
  210. "stack_bottom",
  211. "lowpt_edge",
  212. "left_ref",
  213. "right_ref",
  214. "embedding",
  215. ]
  216. def __init__(self, G):
  217. # copy G without adding self-loops
  218. self.G = nx.Graph()
  219. self.G.add_nodes_from(G.nodes)
  220. for e in G.edges:
  221. if e[0] != e[1]:
  222. self.G.add_edge(e[0], e[1])
  223. self.roots = []
  224. # distance from tree root
  225. self.height = defaultdict(lambda: None)
  226. self.lowpt = {} # height of lowest return point of an edge
  227. self.lowpt2 = {} # height of second lowest return point
  228. self.nesting_depth = {} # for nesting order
  229. # None -> missing edge
  230. self.parent_edge = defaultdict(lambda: None)
  231. # oriented DFS graph
  232. self.DG = nx.DiGraph()
  233. self.DG.add_nodes_from(G.nodes)
  234. self.adjs = {}
  235. self.ordered_adjs = {}
  236. self.ref = defaultdict(lambda: None)
  237. self.side = defaultdict(lambda: 1)
  238. # stack of conflict pairs
  239. self.S = []
  240. self.stack_bottom = {}
  241. self.lowpt_edge = {}
  242. self.left_ref = {}
  243. self.right_ref = {}
  244. self.embedding = PlanarEmbedding()
  245. def lr_planarity(self):
  246. """Execute the LR planarity test.
  247. Returns
  248. -------
  249. embedding : dict
  250. If the graph is planar an embedding is returned. Otherwise None.
  251. """
  252. if self.G.order() > 2 and self.G.size() > 3 * self.G.order() - 6:
  253. # graph is not planar
  254. return None
  255. # make adjacency lists for dfs
  256. for v in self.G:
  257. self.adjs[v] = list(self.G[v])
  258. # orientation of the graph by depth first search traversal
  259. for v in self.G:
  260. if self.height[v] is None:
  261. self.height[v] = 0
  262. self.roots.append(v)
  263. self.dfs_orientation(v)
  264. # Free no longer used variables
  265. self.G = None
  266. self.lowpt2 = None
  267. self.adjs = None
  268. # testing
  269. for v in self.DG: # sort the adjacency lists by nesting depth
  270. # note: this sorting leads to non linear time
  271. self.ordered_adjs[v] = sorted(
  272. self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
  273. )
  274. for v in self.roots:
  275. if not self.dfs_testing(v):
  276. return None
  277. # Free no longer used variables
  278. self.height = None
  279. self.lowpt = None
  280. self.S = None
  281. self.stack_bottom = None
  282. self.lowpt_edge = None
  283. for e in self.DG.edges:
  284. self.nesting_depth[e] = self.sign(e) * self.nesting_depth[e]
  285. self.embedding.add_nodes_from(self.DG.nodes)
  286. for v in self.DG:
  287. # sort the adjacency lists again
  288. self.ordered_adjs[v] = sorted(
  289. self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
  290. )
  291. # initialize the embedding
  292. previous_node = None
  293. for w in self.ordered_adjs[v]:
  294. self.embedding.add_half_edge_cw(v, w, previous_node)
  295. previous_node = w
  296. # Free no longer used variables
  297. self.DG = None
  298. self.nesting_depth = None
  299. self.ref = None
  300. # compute the complete embedding
  301. for v in self.roots:
  302. self.dfs_embedding(v)
  303. # Free no longer used variables
  304. self.roots = None
  305. self.parent_edge = None
  306. self.ordered_adjs = None
  307. self.left_ref = None
  308. self.right_ref = None
  309. self.side = None
  310. return self.embedding
  311. def lr_planarity_recursive(self):
  312. """Recursive version of :meth:`lr_planarity`."""
  313. if self.G.order() > 2 and self.G.size() > 3 * self.G.order() - 6:
  314. # graph is not planar
  315. return None
  316. # orientation of the graph by depth first search traversal
  317. for v in self.G:
  318. if self.height[v] is None:
  319. self.height[v] = 0
  320. self.roots.append(v)
  321. self.dfs_orientation_recursive(v)
  322. # Free no longer used variable
  323. self.G = None
  324. # testing
  325. for v in self.DG: # sort the adjacency lists by nesting depth
  326. # note: this sorting leads to non linear time
  327. self.ordered_adjs[v] = sorted(
  328. self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
  329. )
  330. for v in self.roots:
  331. if not self.dfs_testing_recursive(v):
  332. return None
  333. for e in self.DG.edges:
  334. self.nesting_depth[e] = self.sign_recursive(e) * self.nesting_depth[e]
  335. self.embedding.add_nodes_from(self.DG.nodes)
  336. for v in self.DG:
  337. # sort the adjacency lists again
  338. self.ordered_adjs[v] = sorted(
  339. self.DG[v], key=lambda x: self.nesting_depth[(v, x)]
  340. )
  341. # initialize the embedding
  342. previous_node = None
  343. for w in self.ordered_adjs[v]:
  344. self.embedding.add_half_edge_cw(v, w, previous_node)
  345. previous_node = w
  346. # compute the complete embedding
  347. for v in self.roots:
  348. self.dfs_embedding_recursive(v)
  349. return self.embedding
  350. def dfs_orientation(self, v):
  351. """Orient the graph by DFS, compute lowpoints and nesting order."""
  352. # the recursion stack
  353. dfs_stack = [v]
  354. # index of next edge to handle in adjacency list of each node
  355. ind = defaultdict(lambda: 0)
  356. # boolean to indicate whether to skip the initial work for an edge
  357. skip_init = defaultdict(lambda: False)
  358. while dfs_stack:
  359. v = dfs_stack.pop()
  360. e = self.parent_edge[v]
  361. for w in self.adjs[v][ind[v] :]:
  362. vw = (v, w)
  363. if not skip_init[vw]:
  364. if (v, w) in self.DG.edges or (w, v) in self.DG.edges:
  365. ind[v] += 1
  366. continue # the edge was already oriented
  367. self.DG.add_edge(v, w) # orient the edge
  368. self.lowpt[vw] = self.height[v]
  369. self.lowpt2[vw] = self.height[v]
  370. if self.height[w] is None: # (v, w) is a tree edge
  371. self.parent_edge[w] = vw
  372. self.height[w] = self.height[v] + 1
  373. dfs_stack.append(v) # revisit v after finishing w
  374. dfs_stack.append(w) # visit w next
  375. skip_init[vw] = True # don't redo this block
  376. break # handle next node in dfs_stack (i.e. w)
  377. else: # (v, w) is a back edge
  378. self.lowpt[vw] = self.height[w]
  379. # determine nesting graph
  380. self.nesting_depth[vw] = 2 * self.lowpt[vw]
  381. if self.lowpt2[vw] < self.height[v]: # chordal
  382. self.nesting_depth[vw] += 1
  383. # update lowpoints of parent edge e
  384. if e is not None:
  385. if self.lowpt[vw] < self.lowpt[e]:
  386. self.lowpt2[e] = min(self.lowpt[e], self.lowpt2[vw])
  387. self.lowpt[e] = self.lowpt[vw]
  388. elif self.lowpt[vw] > self.lowpt[e]:
  389. self.lowpt2[e] = min(self.lowpt2[e], self.lowpt[vw])
  390. else:
  391. self.lowpt2[e] = min(self.lowpt2[e], self.lowpt2[vw])
  392. ind[v] += 1
  393. def dfs_orientation_recursive(self, v):
  394. """Recursive version of :meth:`dfs_orientation`."""
  395. e = self.parent_edge[v]
  396. for w in self.G[v]:
  397. if (v, w) in self.DG.edges or (w, v) in self.DG.edges:
  398. continue # the edge was already oriented
  399. vw = (v, w)
  400. self.DG.add_edge(v, w) # orient the edge
  401. self.lowpt[vw] = self.height[v]
  402. self.lowpt2[vw] = self.height[v]
  403. if self.height[w] is None: # (v, w) is a tree edge
  404. self.parent_edge[w] = vw
  405. self.height[w] = self.height[v] + 1
  406. self.dfs_orientation_recursive(w)
  407. else: # (v, w) is a back edge
  408. self.lowpt[vw] = self.height[w]
  409. # determine nesting graph
  410. self.nesting_depth[vw] = 2 * self.lowpt[vw]
  411. if self.lowpt2[vw] < self.height[v]: # chordal
  412. self.nesting_depth[vw] += 1
  413. # update lowpoints of parent edge e
  414. if e is not None:
  415. if self.lowpt[vw] < self.lowpt[e]:
  416. self.lowpt2[e] = min(self.lowpt[e], self.lowpt2[vw])
  417. self.lowpt[e] = self.lowpt[vw]
  418. elif self.lowpt[vw] > self.lowpt[e]:
  419. self.lowpt2[e] = min(self.lowpt2[e], self.lowpt[vw])
  420. else:
  421. self.lowpt2[e] = min(self.lowpt2[e], self.lowpt2[vw])
  422. def dfs_testing(self, v):
  423. """Test for LR partition."""
  424. # the recursion stack
  425. dfs_stack = [v]
  426. # index of next edge to handle in adjacency list of each node
  427. ind = defaultdict(lambda: 0)
  428. # boolean to indicate whether to skip the initial work for an edge
  429. skip_init = defaultdict(lambda: False)
  430. while dfs_stack:
  431. v = dfs_stack.pop()
  432. e = self.parent_edge[v]
  433. # to indicate whether to skip the final block after the for loop
  434. skip_final = False
  435. for w in self.ordered_adjs[v][ind[v] :]:
  436. ei = (v, w)
  437. if not skip_init[ei]:
  438. self.stack_bottom[ei] = top_of_stack(self.S)
  439. if ei == self.parent_edge[w]: # tree edge
  440. dfs_stack.append(v) # revisit v after finishing w
  441. dfs_stack.append(w) # visit w next
  442. skip_init[ei] = True # don't redo this block
  443. skip_final = True # skip final work after breaking
  444. break # handle next node in dfs_stack (i.e. w)
  445. else: # back edge
  446. self.lowpt_edge[ei] = ei
  447. self.S.append(ConflictPair(right=Interval(ei, ei)))
  448. # integrate new return edges
  449. if self.lowpt[ei] < self.height[v]:
  450. if w == self.ordered_adjs[v][0]: # e_i has return edge
  451. self.lowpt_edge[e] = self.lowpt_edge[ei]
  452. else: # add constraints of e_i
  453. if not self.add_constraints(ei, e):
  454. # graph is not planar
  455. return False
  456. ind[v] += 1
  457. if not skip_final:
  458. # remove back edges returning to parent
  459. if e is not None: # v isn't root
  460. self.remove_back_edges(e)
  461. return True
  462. def dfs_testing_recursive(self, v):
  463. """Recursive version of :meth:`dfs_testing`."""
  464. e = self.parent_edge[v]
  465. for w in self.ordered_adjs[v]:
  466. ei = (v, w)
  467. self.stack_bottom[ei] = top_of_stack(self.S)
  468. if ei == self.parent_edge[w]: # tree edge
  469. if not self.dfs_testing_recursive(w):
  470. return False
  471. else: # back edge
  472. self.lowpt_edge[ei] = ei
  473. self.S.append(ConflictPair(right=Interval(ei, ei)))
  474. # integrate new return edges
  475. if self.lowpt[ei] < self.height[v]:
  476. if w == self.ordered_adjs[v][0]: # e_i has return edge
  477. self.lowpt_edge[e] = self.lowpt_edge[ei]
  478. else: # add constraints of e_i
  479. if not self.add_constraints(ei, e):
  480. # graph is not planar
  481. return False
  482. # remove back edges returning to parent
  483. if e is not None: # v isn't root
  484. self.remove_back_edges(e)
  485. return True
  486. def add_constraints(self, ei, e):
  487. P = ConflictPair()
  488. # merge return edges of e_i into P.right
  489. while True:
  490. Q = self.S.pop()
  491. if not Q.left.empty():
  492. Q.swap()
  493. if not Q.left.empty(): # not planar
  494. return False
  495. if self.lowpt[Q.right.low] > self.lowpt[e]:
  496. # merge intervals
  497. if P.right.empty(): # topmost interval
  498. P.right = Q.right.copy()
  499. else:
  500. self.ref[P.right.low] = Q.right.high
  501. P.right.low = Q.right.low
  502. else: # align
  503. self.ref[Q.right.low] = self.lowpt_edge[e]
  504. if top_of_stack(self.S) == self.stack_bottom[ei]:
  505. break
  506. # merge conflicting return edges of e_1,...,e_i-1 into P.L
  507. while top_of_stack(self.S).left.conflicting(ei, self) or top_of_stack(
  508. self.S
  509. ).right.conflicting(ei, self):
  510. Q = self.S.pop()
  511. if Q.right.conflicting(ei, self):
  512. Q.swap()
  513. if Q.right.conflicting(ei, self): # not planar
  514. return False
  515. # merge interval below lowpt(e_i) into P.R
  516. self.ref[P.right.low] = Q.right.high
  517. if Q.right.low is not None:
  518. P.right.low = Q.right.low
  519. if P.left.empty(): # topmost interval
  520. P.left = Q.left.copy()
  521. else:
  522. self.ref[P.left.low] = Q.left.high
  523. P.left.low = Q.left.low
  524. if not (P.left.empty() and P.right.empty()):
  525. self.S.append(P)
  526. return True
  527. def remove_back_edges(self, e):
  528. u = e[0]
  529. # trim back edges ending at parent u
  530. # drop entire conflict pairs
  531. while self.S and top_of_stack(self.S).lowest(self) == self.height[u]:
  532. P = self.S.pop()
  533. if P.left.low is not None:
  534. self.side[P.left.low] = -1
  535. if self.S: # one more conflict pair to consider
  536. P = self.S.pop()
  537. # trim left interval
  538. while P.left.high is not None and P.left.high[1] == u:
  539. P.left.high = self.ref[P.left.high]
  540. if P.left.high is None and P.left.low is not None:
  541. # just emptied
  542. self.ref[P.left.low] = P.right.low
  543. self.side[P.left.low] = -1
  544. P.left.low = None
  545. # trim right interval
  546. while P.right.high is not None and P.right.high[1] == u:
  547. P.right.high = self.ref[P.right.high]
  548. if P.right.high is None and P.right.low is not None:
  549. # just emptied
  550. self.ref[P.right.low] = P.left.low
  551. self.side[P.right.low] = -1
  552. P.right.low = None
  553. self.S.append(P)
  554. # side of e is side of a highest return edge
  555. if self.lowpt[e] < self.height[u]: # e has return edge
  556. hl = top_of_stack(self.S).left.high
  557. hr = top_of_stack(self.S).right.high
  558. if hl is not None and (hr is None or self.lowpt[hl] > self.lowpt[hr]):
  559. self.ref[e] = hl
  560. else:
  561. self.ref[e] = hr
  562. def dfs_embedding(self, v):
  563. """Completes the embedding."""
  564. # the recursion stack
  565. dfs_stack = [v]
  566. # index of next edge to handle in adjacency list of each node
  567. ind = defaultdict(lambda: 0)
  568. while dfs_stack:
  569. v = dfs_stack.pop()
  570. for w in self.ordered_adjs[v][ind[v] :]:
  571. ind[v] += 1
  572. ei = (v, w)
  573. if ei == self.parent_edge[w]: # tree edge
  574. self.embedding.add_half_edge_first(w, v)
  575. self.left_ref[v] = w
  576. self.right_ref[v] = w
  577. dfs_stack.append(v) # revisit v after finishing w
  578. dfs_stack.append(w) # visit w next
  579. break # handle next node in dfs_stack (i.e. w)
  580. else: # back edge
  581. if self.side[ei] == 1:
  582. self.embedding.add_half_edge_cw(w, v, self.right_ref[w])
  583. else:
  584. self.embedding.add_half_edge_ccw(w, v, self.left_ref[w])
  585. self.left_ref[w] = v
  586. def dfs_embedding_recursive(self, v):
  587. """Recursive version of :meth:`dfs_embedding`."""
  588. for w in self.ordered_adjs[v]:
  589. ei = (v, w)
  590. if ei == self.parent_edge[w]: # tree edge
  591. self.embedding.add_half_edge_first(w, v)
  592. self.left_ref[v] = w
  593. self.right_ref[v] = w
  594. self.dfs_embedding_recursive(w)
  595. else: # back edge
  596. if self.side[ei] == 1:
  597. # place v directly after right_ref[w] in embed. list of w
  598. self.embedding.add_half_edge_cw(w, v, self.right_ref[w])
  599. else:
  600. # place v directly before left_ref[w] in embed. list of w
  601. self.embedding.add_half_edge_ccw(w, v, self.left_ref[w])
  602. self.left_ref[w] = v
  603. def sign(self, e):
  604. """Resolve the relative side of an edge to the absolute side."""
  605. # the recursion stack
  606. dfs_stack = [e]
  607. # dict to remember reference edges
  608. old_ref = defaultdict(lambda: None)
  609. while dfs_stack:
  610. e = dfs_stack.pop()
  611. if self.ref[e] is not None:
  612. dfs_stack.append(e) # revisit e after finishing self.ref[e]
  613. dfs_stack.append(self.ref[e]) # visit self.ref[e] next
  614. old_ref[e] = self.ref[e] # remember value of self.ref[e]
  615. self.ref[e] = None
  616. else:
  617. self.side[e] *= self.side[old_ref[e]]
  618. return self.side[e]
  619. def sign_recursive(self, e):
  620. """Recursive version of :meth:`sign`."""
  621. if self.ref[e] is not None:
  622. self.side[e] = self.side[e] * self.sign_recursive(self.ref[e])
  623. self.ref[e] = None
  624. return self.side[e]
  625. class PlanarEmbedding(nx.DiGraph):
  626. """Represents a planar graph with its planar embedding.
  627. The planar embedding is given by a `combinatorial embedding
  628. <https://en.wikipedia.org/wiki/Graph_embedding#Combinatorial_embedding>`_.
  629. .. note:: `check_planarity` is the preferred way to check if a graph is planar.
  630. **Neighbor ordering:**
  631. In comparison to a usual graph structure, the embedding also stores the
  632. order of all neighbors for every vertex.
  633. The order of the neighbors can be given in clockwise (cw) direction or
  634. counterclockwise (ccw) direction. This order is stored as edge attributes
  635. in the underlying directed graph. For the edge (u, v) the edge attribute
  636. 'cw' is set to the neighbor of u that follows immediately after v in
  637. clockwise direction.
  638. In order for a PlanarEmbedding to be valid it must fulfill multiple
  639. conditions. It is possible to check if these conditions are fulfilled with
  640. the method :meth:`check_structure`.
  641. The conditions are:
  642. * Edges must go in both directions (because the edge attributes differ)
  643. * Every edge must have a 'cw' and 'ccw' attribute which corresponds to a
  644. correct planar embedding.
  645. * A node with non zero degree must have a node attribute 'first_nbr'.
  646. As long as a PlanarEmbedding is invalid only the following methods should
  647. be called:
  648. * :meth:`add_half_edge_ccw`
  649. * :meth:`add_half_edge_cw`
  650. * :meth:`connect_components`
  651. * :meth:`add_half_edge_first`
  652. Even though the graph is a subclass of nx.DiGraph, it can still be used
  653. for algorithms that require undirected graphs, because the method
  654. :meth:`is_directed` is overridden. This is possible, because a valid
  655. PlanarGraph must have edges in both directions.
  656. **Half edges:**
  657. In methods like `add_half_edge_ccw` the term "half-edge" is used, which is
  658. a term that is used in `doubly connected edge lists
  659. <https://en.wikipedia.org/wiki/Doubly_connected_edge_list>`_. It is used
  660. to emphasize that the edge is only in one direction and there exists
  661. another half-edge in the opposite direction.
  662. While conventional edges always have two faces (including outer face) next
  663. to them, it is possible to assign each half-edge *exactly one* face.
  664. For a half-edge (u, v) that is orientated such that u is below v then the
  665. face that belongs to (u, v) is to the right of this half-edge.
  666. See Also
  667. --------
  668. is_planar :
  669. Preferred way to check if an existing graph is planar.
  670. check_planarity :
  671. A convenient way to create a `PlanarEmbedding`. If not planar,
  672. it returns a subgraph that shows this.
  673. Examples
  674. --------
  675. Create an embedding of a star graph (compare `nx.star_graph(3)`):
  676. >>> G = nx.PlanarEmbedding()
  677. >>> G.add_half_edge_cw(0, 1, None)
  678. >>> G.add_half_edge_cw(0, 2, 1)
  679. >>> G.add_half_edge_cw(0, 3, 2)
  680. >>> G.add_half_edge_cw(1, 0, None)
  681. >>> G.add_half_edge_cw(2, 0, None)
  682. >>> G.add_half_edge_cw(3, 0, None)
  683. Alternatively the same embedding can also be defined in counterclockwise
  684. orientation. The following results in exactly the same PlanarEmbedding:
  685. >>> G = nx.PlanarEmbedding()
  686. >>> G.add_half_edge_ccw(0, 1, None)
  687. >>> G.add_half_edge_ccw(0, 3, 1)
  688. >>> G.add_half_edge_ccw(0, 2, 3)
  689. >>> G.add_half_edge_ccw(1, 0, None)
  690. >>> G.add_half_edge_ccw(2, 0, None)
  691. >>> G.add_half_edge_ccw(3, 0, None)
  692. After creating a graph, it is possible to validate that the PlanarEmbedding
  693. object is correct:
  694. >>> G.check_structure()
  695. """
  696. def get_data(self):
  697. """Converts the adjacency structure into a better readable structure.
  698. Returns
  699. -------
  700. embedding : dict
  701. A dict mapping all nodes to a list of neighbors sorted in
  702. clockwise order.
  703. See Also
  704. --------
  705. set_data
  706. """
  707. embedding = {}
  708. for v in self:
  709. embedding[v] = list(self.neighbors_cw_order(v))
  710. return embedding
  711. def set_data(self, data):
  712. """Inserts edges according to given sorted neighbor list.
  713. The input format is the same as the output format of get_data().
  714. Parameters
  715. ----------
  716. data : dict
  717. A dict mapping all nodes to a list of neighbors sorted in
  718. clockwise order.
  719. See Also
  720. --------
  721. get_data
  722. """
  723. for v in data:
  724. for w in reversed(data[v]):
  725. self.add_half_edge_first(v, w)
  726. def neighbors_cw_order(self, v):
  727. """Generator for the neighbors of v in clockwise order.
  728. Parameters
  729. ----------
  730. v : node
  731. Yields
  732. ------
  733. node
  734. """
  735. if len(self[v]) == 0:
  736. # v has no neighbors
  737. return
  738. start_node = self.nodes[v]["first_nbr"]
  739. yield start_node
  740. current_node = self[v][start_node]["cw"]
  741. while start_node != current_node:
  742. yield current_node
  743. current_node = self[v][current_node]["cw"]
  744. def check_structure(self):
  745. """Runs without exceptions if this object is valid.
  746. Checks that the following properties are fulfilled:
  747. * Edges go in both directions (because the edge attributes differ).
  748. * Every edge has a 'cw' and 'ccw' attribute which corresponds to a
  749. correct planar embedding.
  750. * A node with a degree larger than 0 has a node attribute 'first_nbr'.
  751. Running this method verifies that the underlying Graph must be planar.
  752. Raises
  753. ------
  754. NetworkXException
  755. This exception is raised with a short explanation if the
  756. PlanarEmbedding is invalid.
  757. """
  758. # Check fundamental structure
  759. for v in self:
  760. try:
  761. sorted_nbrs = set(self.neighbors_cw_order(v))
  762. except KeyError as err:
  763. msg = f"Bad embedding. Missing orientation for a neighbor of {v}"
  764. raise nx.NetworkXException(msg) from err
  765. unsorted_nbrs = set(self[v])
  766. if sorted_nbrs != unsorted_nbrs:
  767. msg = "Bad embedding. Edge orientations not set correctly."
  768. raise nx.NetworkXException(msg)
  769. for w in self[v]:
  770. # Check if opposite half-edge exists
  771. if not self.has_edge(w, v):
  772. msg = "Bad embedding. Opposite half-edge is missing."
  773. raise nx.NetworkXException(msg)
  774. # Check planarity
  775. counted_half_edges = set()
  776. for component in nx.connected_components(self):
  777. if len(component) == 1:
  778. # Don't need to check single node component
  779. continue
  780. num_nodes = len(component)
  781. num_half_edges = 0
  782. num_faces = 0
  783. for v in component:
  784. for w in self.neighbors_cw_order(v):
  785. num_half_edges += 1
  786. if (v, w) not in counted_half_edges:
  787. # We encountered a new face
  788. num_faces += 1
  789. # Mark all half-edges belonging to this face
  790. self.traverse_face(v, w, counted_half_edges)
  791. num_edges = num_half_edges // 2 # num_half_edges is even
  792. if num_nodes - num_edges + num_faces != 2:
  793. # The result does not match Euler's formula
  794. msg = "Bad embedding. The graph does not match Euler's formula"
  795. raise nx.NetworkXException(msg)
  796. def add_half_edge_ccw(self, start_node, end_node, reference_neighbor):
  797. """Adds a half-edge from start_node to end_node.
  798. The half-edge is added counter clockwise next to the existing half-edge
  799. (start_node, reference_neighbor).
  800. Parameters
  801. ----------
  802. start_node : node
  803. Start node of inserted edge.
  804. end_node : node
  805. End node of inserted edge.
  806. reference_neighbor: node
  807. End node of reference edge.
  808. Raises
  809. ------
  810. NetworkXException
  811. If the reference_neighbor does not exist.
  812. See Also
  813. --------
  814. add_half_edge_cw
  815. connect_components
  816. add_half_edge_first
  817. """
  818. if reference_neighbor is None:
  819. # The start node has no neighbors
  820. self.add_edge(start_node, end_node) # Add edge to graph
  821. self[start_node][end_node]["cw"] = end_node
  822. self[start_node][end_node]["ccw"] = end_node
  823. self.nodes[start_node]["first_nbr"] = end_node
  824. else:
  825. ccw_reference = self[start_node][reference_neighbor]["ccw"]
  826. self.add_half_edge_cw(start_node, end_node, ccw_reference)
  827. if reference_neighbor == self.nodes[start_node].get("first_nbr", None):
  828. # Update first neighbor
  829. self.nodes[start_node]["first_nbr"] = end_node
  830. def add_half_edge_cw(self, start_node, end_node, reference_neighbor):
  831. """Adds a half-edge from start_node to end_node.
  832. The half-edge is added clockwise next to the existing half-edge
  833. (start_node, reference_neighbor).
  834. Parameters
  835. ----------
  836. start_node : node
  837. Start node of inserted edge.
  838. end_node : node
  839. End node of inserted edge.
  840. reference_neighbor: node
  841. End node of reference edge.
  842. Raises
  843. ------
  844. NetworkXException
  845. If the reference_neighbor does not exist.
  846. See Also
  847. --------
  848. add_half_edge_ccw
  849. connect_components
  850. add_half_edge_first
  851. """
  852. self.add_edge(start_node, end_node) # Add edge to graph
  853. if reference_neighbor is None:
  854. # The start node has no neighbors
  855. self[start_node][end_node]["cw"] = end_node
  856. self[start_node][end_node]["ccw"] = end_node
  857. self.nodes[start_node]["first_nbr"] = end_node
  858. return
  859. if reference_neighbor not in self[start_node]:
  860. raise nx.NetworkXException(
  861. "Cannot add edge. Reference neighbor does not exist"
  862. )
  863. # Get half-edge at the other side
  864. cw_reference = self[start_node][reference_neighbor]["cw"]
  865. # Alter half-edge data structures
  866. self[start_node][reference_neighbor]["cw"] = end_node
  867. self[start_node][end_node]["cw"] = cw_reference
  868. self[start_node][cw_reference]["ccw"] = end_node
  869. self[start_node][end_node]["ccw"] = reference_neighbor
  870. def connect_components(self, v, w):
  871. """Adds half-edges for (v, w) and (w, v) at some position.
  872. This method should only be called if v and w are in different
  873. components, or it might break the embedding.
  874. This especially means that if `connect_components(v, w)`
  875. is called it is not allowed to call `connect_components(w, v)`
  876. afterwards. The neighbor orientations in both directions are
  877. all set correctly after the first call.
  878. Parameters
  879. ----------
  880. v : node
  881. w : node
  882. See Also
  883. --------
  884. add_half_edge_ccw
  885. add_half_edge_cw
  886. add_half_edge_first
  887. """
  888. self.add_half_edge_first(v, w)
  889. self.add_half_edge_first(w, v)
  890. def add_half_edge_first(self, start_node, end_node):
  891. """The added half-edge is inserted at the first position in the order.
  892. Parameters
  893. ----------
  894. start_node : node
  895. end_node : node
  896. See Also
  897. --------
  898. add_half_edge_ccw
  899. add_half_edge_cw
  900. connect_components
  901. """
  902. if start_node in self and "first_nbr" in self.nodes[start_node]:
  903. reference = self.nodes[start_node]["first_nbr"]
  904. else:
  905. reference = None
  906. self.add_half_edge_ccw(start_node, end_node, reference)
  907. def next_face_half_edge(self, v, w):
  908. """Returns the following half-edge left of a face.
  909. Parameters
  910. ----------
  911. v : node
  912. w : node
  913. Returns
  914. -------
  915. half-edge : tuple
  916. """
  917. new_node = self[w][v]["ccw"]
  918. return w, new_node
  919. def traverse_face(self, v, w, mark_half_edges=None):
  920. """Returns nodes on the face that belong to the half-edge (v, w).
  921. The face that is traversed lies to the right of the half-edge (in an
  922. orientation where v is below w).
  923. Optionally it is possible to pass a set to which all encountered half
  924. edges are added. Before calling this method, this set must not include
  925. any half-edges that belong to the face.
  926. Parameters
  927. ----------
  928. v : node
  929. Start node of half-edge.
  930. w : node
  931. End node of half-edge.
  932. mark_half_edges: set, optional
  933. Set to which all encountered half-edges are added.
  934. Returns
  935. -------
  936. face : list
  937. A list of nodes that lie on this face.
  938. """
  939. if mark_half_edges is None:
  940. mark_half_edges = set()
  941. face_nodes = [v]
  942. mark_half_edges.add((v, w))
  943. prev_node = v
  944. cur_node = w
  945. # Last half-edge is (incoming_node, v)
  946. incoming_node = self[v][w]["cw"]
  947. while cur_node != v or prev_node != incoming_node:
  948. face_nodes.append(cur_node)
  949. prev_node, cur_node = self.next_face_half_edge(prev_node, cur_node)
  950. if (prev_node, cur_node) in mark_half_edges:
  951. raise nx.NetworkXException("Bad planar embedding. Impossible face.")
  952. mark_half_edges.add((prev_node, cur_node))
  953. return face_nodes
  954. def is_directed(self):
  955. """A valid PlanarEmbedding is undirected.
  956. All reverse edges are contained, i.e. for every existing
  957. half-edge (v, w) the half-edge in the opposite direction (w, v) is also
  958. contained.
  959. """
  960. return False