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