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 |
