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