Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/networkx/algorithms/cycles.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 # Copyright (C) 2010-2019 by | |
| 2 # Aric Hagberg <hagberg@lanl.gov> | |
| 3 # Dan Schult <dschult@colgate.edu> | |
| 4 # Pieter Swart <swart@lanl.gov> | |
| 5 # All rights reserved. | |
| 6 # BSD license. | |
| 7 # | |
| 8 # Authors: Jon Olav Vik <jonovik@gmail.com> | |
| 9 # Dan Schult <dschult@colgate.edu> | |
| 10 # Aric Hagberg <hagberg@lanl.gov> | |
| 11 # Debsankha Manik <dmanik@nld.ds.mpg.de> | |
| 12 """ | |
| 13 ======================== | |
| 14 Cycle finding algorithms | |
| 15 ======================== | |
| 16 """ | |
| 17 | |
| 18 from collections import defaultdict | |
| 19 from itertools import tee | |
| 20 | |
| 21 import networkx as nx | |
| 22 from networkx.utils import not_implemented_for, pairwise | |
| 23 | |
| 24 __all__ = [ | |
| 25 'cycle_basis', 'simple_cycles', | |
| 26 'recursive_simple_cycles', 'find_cycle', | |
| 27 'minimum_cycle_basis', | |
| 28 ] | |
| 29 | |
| 30 | |
| 31 @not_implemented_for('directed') | |
| 32 @not_implemented_for('multigraph') | |
| 33 def cycle_basis(G, root=None): | |
| 34 """ Returns a list of cycles which form a basis for cycles of G. | |
| 35 | |
| 36 A basis for cycles of a network is a minimal collection of | |
| 37 cycles such that any cycle in the network can be written | |
| 38 as a sum of cycles in the basis. Here summation of cycles | |
| 39 is defined as "exclusive or" of the edges. Cycle bases are | |
| 40 useful, e.g. when deriving equations for electric circuits | |
| 41 using Kirchhoff's Laws. | |
| 42 | |
| 43 Parameters | |
| 44 ---------- | |
| 45 G : NetworkX Graph | |
| 46 root : node, optional | |
| 47 Specify starting node for basis. | |
| 48 | |
| 49 Returns | |
| 50 ------- | |
| 51 A list of cycle lists. Each cycle list is a list of nodes | |
| 52 which forms a cycle (loop) in G. | |
| 53 | |
| 54 Examples | |
| 55 -------- | |
| 56 >>> G = nx.Graph() | |
| 57 >>> nx.add_cycle(G, [0, 1, 2, 3]) | |
| 58 >>> nx.add_cycle(G, [0, 3, 4, 5]) | |
| 59 >>> print(nx.cycle_basis(G, 0)) | |
| 60 [[3, 4, 5, 0], [1, 2, 3, 0]] | |
| 61 | |
| 62 Notes | |
| 63 ----- | |
| 64 This is adapted from algorithm CACM 491 [1]_. | |
| 65 | |
| 66 References | |
| 67 ---------- | |
| 68 .. [1] Paton, K. An algorithm for finding a fundamental set of | |
| 69 cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. | |
| 70 | |
| 71 See Also | |
| 72 -------- | |
| 73 simple_cycles | |
| 74 """ | |
| 75 gnodes = set(G.nodes()) | |
| 76 cycles = [] | |
| 77 while gnodes: # loop over connected components | |
| 78 if root is None: | |
| 79 root = gnodes.pop() | |
| 80 stack = [root] | |
| 81 pred = {root: root} | |
| 82 used = {root: set()} | |
| 83 while stack: # walk the spanning tree finding cycles | |
| 84 z = stack.pop() # use last-in so cycles easier to find | |
| 85 zused = used[z] | |
| 86 for nbr in G[z]: | |
| 87 if nbr not in used: # new node | |
| 88 pred[nbr] = z | |
| 89 stack.append(nbr) | |
| 90 used[nbr] = set([z]) | |
| 91 elif nbr == z: # self loops | |
| 92 cycles.append([z]) | |
| 93 elif nbr not in zused: # found a cycle | |
| 94 pn = used[nbr] | |
| 95 cycle = [nbr, z] | |
| 96 p = pred[z] | |
| 97 while p not in pn: | |
| 98 cycle.append(p) | |
| 99 p = pred[p] | |
| 100 cycle.append(p) | |
| 101 cycles.append(cycle) | |
| 102 used[nbr].add(z) | |
| 103 gnodes -= set(pred) | |
| 104 root = None | |
| 105 return cycles | |
| 106 | |
| 107 | |
| 108 @not_implemented_for('undirected') | |
| 109 def simple_cycles(G): | |
| 110 """Find simple cycles (elementary circuits) of a directed graph. | |
| 111 | |
| 112 A `simple cycle`, or `elementary circuit`, is a closed path where | |
| 113 no node appears twice. Two elementary circuits are distinct if they | |
| 114 are not cyclic permutations of each other. | |
| 115 | |
| 116 This is a nonrecursive, iterator/generator version of Johnson's | |
| 117 algorithm [1]_. There may be better algorithms for some cases [2]_ [3]_. | |
| 118 | |
| 119 Parameters | |
| 120 ---------- | |
| 121 G : NetworkX DiGraph | |
| 122 A directed graph | |
| 123 | |
| 124 Returns | |
| 125 ------- | |
| 126 cycle_generator: generator | |
| 127 A generator that produces elementary cycles of the graph. | |
| 128 Each cycle is represented by a list of nodes along the cycle. | |
| 129 | |
| 130 Examples | |
| 131 -------- | |
| 132 >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] | |
| 133 >>> G = nx.DiGraph(edges) | |
| 134 >>> len(list(nx.simple_cycles(G))) | |
| 135 5 | |
| 136 | |
| 137 To filter the cycles so that they don't include certain nodes or edges, | |
| 138 copy your graph and eliminate those nodes or edges before calling | |
| 139 | |
| 140 >>> copyG = G.copy() | |
| 141 >>> copyG.remove_nodes_from([1]) | |
| 142 >>> copyG.remove_edges_from([(0, 1)]) | |
| 143 >>> len(list(nx.simple_cycles(copyG))) | |
| 144 3 | |
| 145 | |
| 146 | |
| 147 Notes | |
| 148 ----- | |
| 149 The implementation follows pp. 79-80 in [1]_. | |
| 150 | |
| 151 The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ | |
| 152 elementary circuits. | |
| 153 | |
| 154 References | |
| 155 ---------- | |
| 156 .. [1] Finding all the elementary circuits of a directed graph. | |
| 157 D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. | |
| 158 https://doi.org/10.1137/0204007 | |
| 159 .. [2] Enumerating the cycles of a digraph: a new preprocessing strategy. | |
| 160 G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982. | |
| 161 .. [3] A search strategy for the elementary cycles of a directed graph. | |
| 162 J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS, | |
| 163 v. 16, no. 2, 192-204, 1976. | |
| 164 | |
| 165 See Also | |
| 166 -------- | |
| 167 cycle_basis | |
| 168 """ | |
| 169 def _unblock(thisnode, blocked, B): | |
| 170 stack = set([thisnode]) | |
| 171 while stack: | |
| 172 node = stack.pop() | |
| 173 if node in blocked: | |
| 174 blocked.remove(node) | |
| 175 stack.update(B[node]) | |
| 176 B[node].clear() | |
| 177 | |
| 178 # Johnson's algorithm requires some ordering of the nodes. | |
| 179 # We assign the arbitrary ordering given by the strongly connected comps | |
| 180 # There is no need to track the ordering as each node removed as processed. | |
| 181 # Also we save the actual graph so we can mutate it. We only take the | |
| 182 # edges because we do not want to copy edge and node attributes here. | |
| 183 subG = type(G)(G.edges()) | |
| 184 sccs = [scc for scc in nx.strongly_connected_components(subG) | |
| 185 if len(scc) > 1] | |
| 186 | |
| 187 # Johnson's algorithm exclude self cycle edges like (v, v) | |
| 188 # To be backward compatible, we record those cycles in advance | |
| 189 # and then remove from subG | |
| 190 for v in subG: | |
| 191 if subG.has_edge(v, v): | |
| 192 yield [v] | |
| 193 subG.remove_edge(v, v) | |
| 194 | |
| 195 while sccs: | |
| 196 scc = sccs.pop() | |
| 197 sccG = subG.subgraph(scc) | |
| 198 # order of scc determines ordering of nodes | |
| 199 startnode = scc.pop() | |
| 200 # Processing node runs "circuit" routine from recursive version | |
| 201 path = [startnode] | |
| 202 blocked = set() # vertex: blocked from search? | |
| 203 closed = set() # nodes involved in a cycle | |
| 204 blocked.add(startnode) | |
| 205 B = defaultdict(set) # graph portions that yield no elementary circuit | |
| 206 stack = [(startnode, list(sccG[startnode]))] # sccG gives comp nbrs | |
| 207 while stack: | |
| 208 thisnode, nbrs = stack[-1] | |
| 209 if nbrs: | |
| 210 nextnode = nbrs.pop() | |
| 211 if nextnode == startnode: | |
| 212 yield path[:] | |
| 213 closed.update(path) | |
| 214 # print "Found a cycle", path, closed | |
| 215 elif nextnode not in blocked: | |
| 216 path.append(nextnode) | |
| 217 stack.append((nextnode, list(sccG[nextnode]))) | |
| 218 closed.discard(nextnode) | |
| 219 blocked.add(nextnode) | |
| 220 continue | |
| 221 # done with nextnode... look for more neighbors | |
| 222 if not nbrs: # no more nbrs | |
| 223 if thisnode in closed: | |
| 224 _unblock(thisnode, blocked, B) | |
| 225 else: | |
| 226 for nbr in sccG[thisnode]: | |
| 227 if thisnode not in B[nbr]: | |
| 228 B[nbr].add(thisnode) | |
| 229 stack.pop() | |
| 230 # assert path[-1] == thisnode | |
| 231 path.pop() | |
| 232 # done processing this node | |
| 233 H = subG.subgraph(scc) # make smaller to avoid work in SCC routine | |
| 234 sccs.extend(scc for scc in nx.strongly_connected_components(H) | |
| 235 if len(scc) > 1) | |
| 236 | |
| 237 | |
| 238 @not_implemented_for('undirected') | |
| 239 def recursive_simple_cycles(G): | |
| 240 """Find simple cycles (elementary circuits) of a directed graph. | |
| 241 | |
| 242 A `simple cycle`, or `elementary circuit`, is a closed path where | |
| 243 no node appears twice. Two elementary circuits are distinct if they | |
| 244 are not cyclic permutations of each other. | |
| 245 | |
| 246 This version uses a recursive algorithm to build a list of cycles. | |
| 247 You should probably use the iterator version called simple_cycles(). | |
| 248 Warning: This recursive version uses lots of RAM! | |
| 249 | |
| 250 Parameters | |
| 251 ---------- | |
| 252 G : NetworkX DiGraph | |
| 253 A directed graph | |
| 254 | |
| 255 Returns | |
| 256 ------- | |
| 257 A list of cycles, where each cycle is represented by a list of nodes | |
| 258 along the cycle. | |
| 259 | |
| 260 Example: | |
| 261 | |
| 262 >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] | |
| 263 >>> G = nx.DiGraph(edges) | |
| 264 >>> nx.recursive_simple_cycles(G) | |
| 265 [[0], [2], [0, 1, 2], [0, 2], [1, 2]] | |
| 266 | |
| 267 See Also | |
| 268 -------- | |
| 269 cycle_basis (for undirected graphs) | |
| 270 | |
| 271 Notes | |
| 272 ----- | |
| 273 The implementation follows pp. 79-80 in [1]_. | |
| 274 | |
| 275 The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ | |
| 276 elementary circuits. | |
| 277 | |
| 278 References | |
| 279 ---------- | |
| 280 .. [1] Finding all the elementary circuits of a directed graph. | |
| 281 D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. | |
| 282 https://doi.org/10.1137/0204007 | |
| 283 | |
| 284 See Also | |
| 285 -------- | |
| 286 simple_cycles, cycle_basis | |
| 287 """ | |
| 288 # Jon Olav Vik, 2010-08-09 | |
| 289 def _unblock(thisnode): | |
| 290 """Recursively unblock and remove nodes from B[thisnode].""" | |
| 291 if blocked[thisnode]: | |
| 292 blocked[thisnode] = False | |
| 293 while B[thisnode]: | |
| 294 _unblock(B[thisnode].pop()) | |
| 295 | |
| 296 def circuit(thisnode, startnode, component): | |
| 297 closed = False # set to True if elementary path is closed | |
| 298 path.append(thisnode) | |
| 299 blocked[thisnode] = True | |
| 300 for nextnode in component[thisnode]: # direct successors of thisnode | |
| 301 if nextnode == startnode: | |
| 302 result.append(path[:]) | |
| 303 closed = True | |
| 304 elif not blocked[nextnode]: | |
| 305 if circuit(nextnode, startnode, component): | |
| 306 closed = True | |
| 307 if closed: | |
| 308 _unblock(thisnode) | |
| 309 else: | |
| 310 for nextnode in component[thisnode]: | |
| 311 if thisnode not in B[nextnode]: # TODO: use set for speedup? | |
| 312 B[nextnode].append(thisnode) | |
| 313 path.pop() # remove thisnode from path | |
| 314 return closed | |
| 315 | |
| 316 path = [] # stack of nodes in current path | |
| 317 blocked = defaultdict(bool) # vertex: blocked from search? | |
| 318 B = defaultdict(list) # graph portions that yield no elementary circuit | |
| 319 result = [] # list to accumulate the circuits found | |
| 320 | |
| 321 # Johnson's algorithm exclude self cycle edges like (v, v) | |
| 322 # To be backward compatible, we record those cycles in advance | |
| 323 # and then remove from subG | |
| 324 for v in G: | |
| 325 if G.has_edge(v, v): | |
| 326 result.append([v]) | |
| 327 G.remove_edge(v, v) | |
| 328 | |
| 329 # Johnson's algorithm requires some ordering of the nodes. | |
| 330 # They might not be sortable so we assign an arbitrary ordering. | |
| 331 ordering = dict(zip(G, range(len(G)))) | |
| 332 for s in ordering: | |
| 333 # Build the subgraph induced by s and following nodes in the ordering | |
| 334 subgraph = G.subgraph(node for node in G | |
| 335 if ordering[node] >= ordering[s]) | |
| 336 # Find the strongly connected component in the subgraph | |
| 337 # that contains the least node according to the ordering | |
| 338 strongcomp = nx.strongly_connected_components(subgraph) | |
| 339 mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns)) | |
| 340 component = G.subgraph(mincomp) | |
| 341 if len(component) > 1: | |
| 342 # smallest node in the component according to the ordering | |
| 343 startnode = min(component, key=ordering.__getitem__) | |
| 344 for node in component: | |
| 345 blocked[node] = False | |
| 346 B[node][:] = [] | |
| 347 dummy = circuit(startnode, startnode, component) | |
| 348 return result | |
| 349 | |
| 350 | |
| 351 def find_cycle(G, source=None, orientation=None): | |
| 352 """Returns a cycle found via depth-first traversal. | |
| 353 | |
| 354 The cycle is a list of edges indicating the cyclic path. | |
| 355 Orientation of directed edges is controlled by `orientation`. | |
| 356 | |
| 357 Parameters | |
| 358 ---------- | |
| 359 G : graph | |
| 360 A directed/undirected graph/multigraph. | |
| 361 | |
| 362 source : node, list of nodes | |
| 363 The node from which the traversal begins. If None, then a source | |
| 364 is chosen arbitrarily and repeatedly until all edges from each node in | |
| 365 the graph are searched. | |
| 366 | |
| 367 orientation : None | 'original' | 'reverse' | 'ignore' (default: None) | |
| 368 For directed graphs and directed multigraphs, edge traversals need not | |
| 369 respect the original orientation of the edges. | |
| 370 When set to 'reverse' every edge is traversed in the reverse direction. | |
| 371 When set to 'ignore', every edge is treated as undirected. | |
| 372 When set to 'original', every edge is treated as directed. | |
| 373 In all three cases, the yielded edge tuples add a last entry to | |
| 374 indicate the direction in which that edge was traversed. | |
| 375 If orientation is None, the yielded edge has no direction indicated. | |
| 376 The direction is respected, but not reported. | |
| 377 | |
| 378 Returns | |
| 379 ------- | |
| 380 edges : directed edges | |
| 381 A list of directed edges indicating the path taken for the loop. | |
| 382 If no cycle is found, then an exception is raised. | |
| 383 For graphs, an edge is of the form `(u, v)` where `u` and `v` | |
| 384 are the tail and head of the edge as determined by the traversal. | |
| 385 For multigraphs, an edge is of the form `(u, v, key)`, where `key` is | |
| 386 the key of the edge. When the graph is directed, then `u` and `v` | |
| 387 are always in the order of the actual directed edge. | |
| 388 If orientation is not None then the edge tuple is extended to include | |
| 389 the direction of traversal ('forward' or 'reverse') on that edge. | |
| 390 | |
| 391 Raises | |
| 392 ------ | |
| 393 NetworkXNoCycle | |
| 394 If no cycle was found. | |
| 395 | |
| 396 Examples | |
| 397 -------- | |
| 398 In this example, we construct a DAG and find, in the first call, that there | |
| 399 are no directed cycles, and so an exception is raised. In the second call, | |
| 400 we ignore edge orientations and find that there is an undirected cycle. | |
| 401 Note that the second call finds a directed cycle while effectively | |
| 402 traversing an undirected graph, and so, we found an "undirected cycle". | |
| 403 This means that this DAG structure does not form a directed tree (which | |
| 404 is also known as a polytree). | |
| 405 | |
| 406 >>> import networkx as nx | |
| 407 >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) | |
| 408 >>> try: | |
| 409 ... nx.find_cycle(G, orientation='original') | |
| 410 ... except: | |
| 411 ... pass | |
| 412 ... | |
| 413 >>> list(nx.find_cycle(G, orientation='ignore')) | |
| 414 [(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')] | |
| 415 | |
| 416 """ | |
| 417 if not G.is_directed() or orientation in (None, 'original'): | |
| 418 def tailhead(edge): | |
| 419 return edge[:2] | |
| 420 elif orientation == 'reverse': | |
| 421 def tailhead(edge): | |
| 422 return edge[1], edge[0] | |
| 423 elif orientation == 'ignore': | |
| 424 def tailhead(edge): | |
| 425 if edge[-1] == 'reverse': | |
| 426 return edge[1], edge[0] | |
| 427 return edge[:2] | |
| 428 | |
| 429 explored = set() | |
| 430 cycle = [] | |
| 431 final_node = None | |
| 432 for start_node in G.nbunch_iter(source): | |
| 433 if start_node in explored: | |
| 434 # No loop is possible. | |
| 435 continue | |
| 436 | |
| 437 edges = [] | |
| 438 # All nodes seen in this iteration of edge_dfs | |
| 439 seen = {start_node} | |
| 440 # Nodes in active path. | |
| 441 active_nodes = {start_node} | |
| 442 previous_head = None | |
| 443 | |
| 444 for edge in nx.edge_dfs(G, start_node, orientation): | |
| 445 # Determine if this edge is a continuation of the active path. | |
| 446 tail, head = tailhead(edge) | |
| 447 if head in explored: | |
| 448 # Then we've already explored it. No loop is possible. | |
| 449 continue | |
| 450 if previous_head is not None and tail != previous_head: | |
| 451 # This edge results from backtracking. | |
| 452 # Pop until we get a node whose head equals the current tail. | |
| 453 # So for example, we might have: | |
| 454 # (0, 1), (1, 2), (2, 3), (1, 4) | |
| 455 # which must become: | |
| 456 # (0, 1), (1, 4) | |
| 457 while True: | |
| 458 try: | |
| 459 popped_edge = edges.pop() | |
| 460 except IndexError: | |
| 461 edges = [] | |
| 462 active_nodes = {tail} | |
| 463 break | |
| 464 else: | |
| 465 popped_head = tailhead(popped_edge)[1] | |
| 466 active_nodes.remove(popped_head) | |
| 467 | |
| 468 if edges: | |
| 469 last_head = tailhead(edges[-1])[1] | |
| 470 if tail == last_head: | |
| 471 break | |
| 472 edges.append(edge) | |
| 473 | |
| 474 if head in active_nodes: | |
| 475 # We have a loop! | |
| 476 cycle.extend(edges) | |
| 477 final_node = head | |
| 478 break | |
| 479 else: | |
| 480 seen.add(head) | |
| 481 active_nodes.add(head) | |
| 482 previous_head = head | |
| 483 | |
| 484 if cycle: | |
| 485 break | |
| 486 else: | |
| 487 explored.update(seen) | |
| 488 | |
| 489 else: | |
| 490 assert(len(cycle) == 0) | |
| 491 raise nx.exception.NetworkXNoCycle('No cycle found.') | |
| 492 | |
| 493 # We now have a list of edges which ends on a cycle. | |
| 494 # So we need to remove from the beginning edges that are not relevant. | |
| 495 | |
| 496 for i, edge in enumerate(cycle): | |
| 497 tail, head = tailhead(edge) | |
| 498 if tail == final_node: | |
| 499 break | |
| 500 | |
| 501 return cycle[i:] | |
| 502 | |
| 503 | |
| 504 @not_implemented_for('directed') | |
| 505 @not_implemented_for('multigraph') | |
| 506 def minimum_cycle_basis(G, weight=None): | |
| 507 """ Returns a minimum weight cycle basis for G | |
| 508 | |
| 509 Minimum weight means a cycle basis for which the total weight | |
| 510 (length for unweighted graphs) of all the cycles is minimum. | |
| 511 | |
| 512 Parameters | |
| 513 ---------- | |
| 514 G : NetworkX Graph | |
| 515 weight: string | |
| 516 name of the edge attribute to use for edge weights | |
| 517 | |
| 518 Returns | |
| 519 ------- | |
| 520 A list of cycle lists. Each cycle list is a list of nodes | |
| 521 which forms a cycle (loop) in G. Note that the nodes are not | |
| 522 necessarily returned in a order by which they appear in the cycle | |
| 523 | |
| 524 Examples | |
| 525 -------- | |
| 526 >>> G=nx.Graph() | |
| 527 >>> nx.add_cycle(G, [0,1,2,3]) | |
| 528 >>> nx.add_cycle(G, [0,3,4,5]) | |
| 529 >>> print([sorted(c) for c in nx.minimum_cycle_basis(G)]) | |
| 530 [[0, 1, 2, 3], [0, 3, 4, 5]] | |
| 531 | |
| 532 References: | |
| 533 [1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for | |
| 534 Minimum Cycle Basis of Graphs." | |
| 535 http://link.springer.com/article/10.1007/s00453-007-9064-z | |
| 536 [2] de Pina, J. 1995. Applications of shortest path methods. | |
| 537 Ph.D. thesis, University of Amsterdam, Netherlands | |
| 538 | |
| 539 See Also | |
| 540 -------- | |
| 541 simple_cycles, cycle_basis | |
| 542 """ | |
| 543 # We first split the graph in commected subgraphs | |
| 544 return sum((_min_cycle_basis(G.subgraph(c), weight) for c in | |
| 545 nx.connected_components(G)), []) | |
| 546 | |
| 547 | |
| 548 def _min_cycle_basis(comp, weight): | |
| 549 cb = [] | |
| 550 # We extract the edges not in a spanning tree. We do not really need a | |
| 551 # *minimum* spanning tree. That is why we call the next function with | |
| 552 # weight=None. Depending on implementation, it may be faster as well | |
| 553 spanning_tree_edges = list(nx.minimum_spanning_edges(comp, weight=None, | |
| 554 data=False)) | |
| 555 edges_excl = [frozenset(e) for e in comp.edges() | |
| 556 if e not in spanning_tree_edges] | |
| 557 N = len(edges_excl) | |
| 558 | |
| 559 # We maintain a set of vectors orthogonal to sofar found cycles | |
| 560 set_orth = [set([edge]) for edge in edges_excl] | |
| 561 for k in range(N): | |
| 562 # kth cycle is "parallel" to kth vector in set_orth | |
| 563 new_cycle = _min_cycle(comp, set_orth[k], weight=weight) | |
| 564 cb.append(list(set().union(*new_cycle))) | |
| 565 # now update set_orth so that k+1,k+2... th elements are | |
| 566 # orthogonal to the newly found cycle, as per [p. 336, 1] | |
| 567 base = set_orth[k] | |
| 568 set_orth[k + 1:] = [orth ^ base if len(orth & new_cycle) % 2 else orth | |
| 569 for orth in set_orth[k + 1:]] | |
| 570 return cb | |
| 571 | |
| 572 | |
| 573 def _min_cycle(G, orth, weight=None): | |
| 574 """ | |
| 575 Computes the minimum weight cycle in G, | |
| 576 orthogonal to the vector orth as per [p. 338, 1] | |
| 577 """ | |
| 578 T = nx.Graph() | |
| 579 | |
| 580 nodes_idx = {node: idx for idx, node in enumerate(G.nodes())} | |
| 581 idx_nodes = {idx: node for node, idx in nodes_idx.items()} | |
| 582 | |
| 583 nnodes = len(nodes_idx) | |
| 584 | |
| 585 # Add 2 copies of each edge in G to T. If edge is in orth, add cross edge; | |
| 586 # otherwise in-plane edge | |
| 587 for u, v, data in G.edges(data=True): | |
| 588 uidx, vidx = nodes_idx[u], nodes_idx[v] | |
| 589 edge_w = data.get(weight, 1) | |
| 590 if frozenset((u, v)) in orth: | |
| 591 T.add_edges_from( | |
| 592 [(uidx, nnodes + vidx), (nnodes + uidx, vidx)], weight=edge_w) | |
| 593 else: | |
| 594 T.add_edges_from( | |
| 595 [(uidx, vidx), (nnodes + uidx, nnodes + vidx)], weight=edge_w) | |
| 596 | |
| 597 all_shortest_pathlens = dict(nx.shortest_path_length(T, weight=weight)) | |
| 598 cross_paths_w_lens = {n: all_shortest_pathlens[n][nnodes + n] | |
| 599 for n in range(nnodes)} | |
| 600 | |
| 601 # Now compute shortest paths in T, which translates to cyles in G | |
| 602 start = min(cross_paths_w_lens, key=cross_paths_w_lens.get) | |
| 603 end = nnodes + start | |
| 604 min_path = nx.shortest_path(T, source=start, target=end, weight='weight') | |
| 605 | |
| 606 # Now we obtain the actual path, re-map nodes in T to those in G | |
| 607 min_path_nodes = [node if node < nnodes else node - nnodes | |
| 608 for node in min_path] | |
| 609 # Now remove the edges that occur two times | |
| 610 mcycle_pruned = _path_to_cycle(min_path_nodes) | |
| 611 | |
| 612 return {frozenset((idx_nodes[u], idx_nodes[v])) for u, v in mcycle_pruned} | |
| 613 | |
| 614 | |
| 615 def _path_to_cycle(path): | |
| 616 """ | |
| 617 Removes the edges from path that occur even number of times. | |
| 618 Returns a set of edges | |
| 619 """ | |
| 620 edges = set() | |
| 621 for edge in pairwise(path): | |
| 622 # Toggle whether to keep the current edge. | |
| 623 edges ^= {edge} | |
| 624 return edges |
