Mercurial > repos > guerler > springsuite
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/planemo/lib/python3.7/site-packages/networkx/algorithms/cycles.py Fri Jul 31 00:32:28 2020 -0400 @@ -0,0 +1,624 @@ +# Copyright (C) 2010-2019 by +# Aric Hagberg <hagberg@lanl.gov> +# Dan Schult <dschult@colgate.edu> +# Pieter Swart <swart@lanl.gov> +# All rights reserved. +# BSD license. +# +# Authors: Jon Olav Vik <jonovik@gmail.com> +# Dan Schult <dschult@colgate.edu> +# Aric Hagberg <hagberg@lanl.gov> +# Debsankha Manik <dmanik@nld.ds.mpg.de> +""" +======================== +Cycle finding algorithms +======================== +""" + +from collections import defaultdict +from itertools import tee + +import networkx as nx +from networkx.utils import not_implemented_for, pairwise + +__all__ = [ + 'cycle_basis', 'simple_cycles', + 'recursive_simple_cycles', 'find_cycle', + 'minimum_cycle_basis', +] + + +@not_implemented_for('directed') +@not_implemented_for('multigraph') +def cycle_basis(G, root=None): + """ Returns a list of cycles which form a basis for cycles of G. + + A basis for cycles of a network is a minimal collection of + cycles such that any cycle in the network can be written + as a sum of cycles in the basis. Here summation of cycles + is defined as "exclusive or" of the edges. Cycle bases are + useful, e.g. when deriving equations for electric circuits + using Kirchhoff's Laws. + + Parameters + ---------- + G : NetworkX Graph + root : node, optional + Specify starting node for basis. + + Returns + ------- + A list of cycle lists. Each cycle list is a list of nodes + which forms a cycle (loop) in G. + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_cycle(G, [0, 1, 2, 3]) + >>> nx.add_cycle(G, [0, 3, 4, 5]) + >>> print(nx.cycle_basis(G, 0)) + [[3, 4, 5, 0], [1, 2, 3, 0]] + + Notes + ----- + This is adapted from algorithm CACM 491 [1]_. + + References + ---------- + .. [1] Paton, K. An algorithm for finding a fundamental set of + cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. + + See Also + -------- + simple_cycles + """ + gnodes = set(G.nodes()) + cycles = [] + while gnodes: # loop over connected components + if root is None: + root = gnodes.pop() + stack = [root] + pred = {root: root} + used = {root: set()} + while stack: # walk the spanning tree finding cycles + z = stack.pop() # use last-in so cycles easier to find + zused = used[z] + for nbr in G[z]: + if nbr not in used: # new node + pred[nbr] = z + stack.append(nbr) + used[nbr] = set([z]) + elif nbr == z: # self loops + cycles.append([z]) + elif nbr not in zused: # found a cycle + pn = used[nbr] + cycle = [nbr, z] + p = pred[z] + while p not in pn: + cycle.append(p) + p = pred[p] + cycle.append(p) + cycles.append(cycle) + used[nbr].add(z) + gnodes -= set(pred) + root = None + return cycles + + +@not_implemented_for('undirected') +def simple_cycles(G): + """Find simple cycles (elementary circuits) of a directed graph. + + A `simple cycle`, or `elementary circuit`, is a closed path where + no node appears twice. Two elementary circuits are distinct if they + are not cyclic permutations of each other. + + This is a nonrecursive, iterator/generator version of Johnson's + algorithm [1]_. There may be better algorithms for some cases [2]_ [3]_. + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph + + Returns + ------- + cycle_generator: generator + A generator that produces elementary cycles of the graph. + Each cycle is represented by a list of nodes along the cycle. + + Examples + -------- + >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] + >>> G = nx.DiGraph(edges) + >>> len(list(nx.simple_cycles(G))) + 5 + + To filter the cycles so that they don't include certain nodes or edges, + copy your graph and eliminate those nodes or edges before calling + + >>> copyG = G.copy() + >>> copyG.remove_nodes_from([1]) + >>> copyG.remove_edges_from([(0, 1)]) + >>> len(list(nx.simple_cycles(copyG))) + 3 + + + Notes + ----- + The implementation follows pp. 79-80 in [1]_. + + The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ + elementary circuits. + + References + ---------- + .. [1] Finding all the elementary circuits of a directed graph. + D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. + https://doi.org/10.1137/0204007 + .. [2] Enumerating the cycles of a digraph: a new preprocessing strategy. + G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982. + .. [3] A search strategy for the elementary cycles of a directed graph. + J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS, + v. 16, no. 2, 192-204, 1976. + + See Also + -------- + cycle_basis + """ + def _unblock(thisnode, blocked, B): + stack = set([thisnode]) + while stack: + node = stack.pop() + if node in blocked: + blocked.remove(node) + stack.update(B[node]) + B[node].clear() + + # Johnson's algorithm requires some ordering of the nodes. + # We assign the arbitrary ordering given by the strongly connected comps + # There is no need to track the ordering as each node removed as processed. + # Also we save the actual graph so we can mutate it. We only take the + # edges because we do not want to copy edge and node attributes here. + subG = type(G)(G.edges()) + sccs = [scc for scc in nx.strongly_connected_components(subG) + if len(scc) > 1] + + # Johnson's algorithm exclude self cycle edges like (v, v) + # To be backward compatible, we record those cycles in advance + # and then remove from subG + for v in subG: + if subG.has_edge(v, v): + yield [v] + subG.remove_edge(v, v) + + while sccs: + scc = sccs.pop() + sccG = subG.subgraph(scc) + # order of scc determines ordering of nodes + startnode = scc.pop() + # Processing node runs "circuit" routine from recursive version + path = [startnode] + blocked = set() # vertex: blocked from search? + closed = set() # nodes involved in a cycle + blocked.add(startnode) + B = defaultdict(set) # graph portions that yield no elementary circuit + stack = [(startnode, list(sccG[startnode]))] # sccG gives comp nbrs + while stack: + thisnode, nbrs = stack[-1] + if nbrs: + nextnode = nbrs.pop() + if nextnode == startnode: + yield path[:] + closed.update(path) +# print "Found a cycle", path, closed + elif nextnode not in blocked: + path.append(nextnode) + stack.append((nextnode, list(sccG[nextnode]))) + closed.discard(nextnode) + blocked.add(nextnode) + continue + # done with nextnode... look for more neighbors + if not nbrs: # no more nbrs + if thisnode in closed: + _unblock(thisnode, blocked, B) + else: + for nbr in sccG[thisnode]: + if thisnode not in B[nbr]: + B[nbr].add(thisnode) + stack.pop() +# assert path[-1] == thisnode + path.pop() + # done processing this node + H = subG.subgraph(scc) # make smaller to avoid work in SCC routine + sccs.extend(scc for scc in nx.strongly_connected_components(H) + if len(scc) > 1) + + +@not_implemented_for('undirected') +def recursive_simple_cycles(G): + """Find simple cycles (elementary circuits) of a directed graph. + + A `simple cycle`, or `elementary circuit`, is a closed path where + no node appears twice. Two elementary circuits are distinct if they + are not cyclic permutations of each other. + + This version uses a recursive algorithm to build a list of cycles. + You should probably use the iterator version called simple_cycles(). + Warning: This recursive version uses lots of RAM! + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph + + Returns + ------- + A list of cycles, where each cycle is represented by a list of nodes + along the cycle. + + Example: + + >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] + >>> G = nx.DiGraph(edges) + >>> nx.recursive_simple_cycles(G) + [[0], [2], [0, 1, 2], [0, 2], [1, 2]] + + See Also + -------- + cycle_basis (for undirected graphs) + + Notes + ----- + The implementation follows pp. 79-80 in [1]_. + + The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ + elementary circuits. + + References + ---------- + .. [1] Finding all the elementary circuits of a directed graph. + D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. + https://doi.org/10.1137/0204007 + + See Also + -------- + simple_cycles, cycle_basis + """ + # Jon Olav Vik, 2010-08-09 + def _unblock(thisnode): + """Recursively unblock and remove nodes from B[thisnode].""" + if blocked[thisnode]: + blocked[thisnode] = False + while B[thisnode]: + _unblock(B[thisnode].pop()) + + def circuit(thisnode, startnode, component): + closed = False # set to True if elementary path is closed + path.append(thisnode) + blocked[thisnode] = True + for nextnode in component[thisnode]: # direct successors of thisnode + if nextnode == startnode: + result.append(path[:]) + closed = True + elif not blocked[nextnode]: + if circuit(nextnode, startnode, component): + closed = True + if closed: + _unblock(thisnode) + else: + for nextnode in component[thisnode]: + if thisnode not in B[nextnode]: # TODO: use set for speedup? + B[nextnode].append(thisnode) + path.pop() # remove thisnode from path + return closed + + path = [] # stack of nodes in current path + blocked = defaultdict(bool) # vertex: blocked from search? + B = defaultdict(list) # graph portions that yield no elementary circuit + result = [] # list to accumulate the circuits found + + # Johnson's algorithm exclude self cycle edges like (v, v) + # To be backward compatible, we record those cycles in advance + # and then remove from subG + for v in G: + if G.has_edge(v, v): + result.append([v]) + G.remove_edge(v, v) + + # Johnson's algorithm requires some ordering of the nodes. + # They might not be sortable so we assign an arbitrary ordering. + ordering = dict(zip(G, range(len(G)))) + for s in ordering: + # Build the subgraph induced by s and following nodes in the ordering + subgraph = G.subgraph(node for node in G + if ordering[node] >= ordering[s]) + # Find the strongly connected component in the subgraph + # that contains the least node according to the ordering + strongcomp = nx.strongly_connected_components(subgraph) + mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns)) + component = G.subgraph(mincomp) + if len(component) > 1: + # smallest node in the component according to the ordering + startnode = min(component, key=ordering.__getitem__) + for node in component: + blocked[node] = False + B[node][:] = [] + dummy = circuit(startnode, startnode, component) + return result + + +def find_cycle(G, source=None, orientation=None): + """Returns a cycle found via depth-first traversal. + + The cycle is a list of edges indicating the cyclic path. + Orientation of directed edges is controlled by `orientation`. + + Parameters + ---------- + G : graph + A directed/undirected graph/multigraph. + + source : node, list of nodes + The node from which the traversal begins. If None, then a source + is chosen arbitrarily and repeatedly until all edges from each node in + the graph are searched. + + orientation : None | 'original' | 'reverse' | 'ignore' (default: None) + For directed graphs and directed multigraphs, edge traversals need not + respect the original orientation of the edges. + When set to 'reverse' every edge is traversed in the reverse direction. + When set to 'ignore', every edge is treated as undirected. + When set to 'original', every edge is treated as directed. + In all three cases, the yielded edge tuples add a last entry to + indicate the direction in which that edge was traversed. + If orientation is None, the yielded edge has no direction indicated. + The direction is respected, but not reported. + + Returns + ------- + edges : directed edges + A list of directed edges indicating the path taken for the loop. + If no cycle is found, then an exception is raised. + For graphs, an edge is of the form `(u, v)` where `u` and `v` + are the tail and head of the edge as determined by the traversal. + For multigraphs, an edge is of the form `(u, v, key)`, where `key` is + the key of the edge. When the graph is directed, then `u` and `v` + are always in the order of the actual directed edge. + If orientation is not None then the edge tuple is extended to include + the direction of traversal ('forward' or 'reverse') on that edge. + + Raises + ------ + NetworkXNoCycle + If no cycle was found. + + Examples + -------- + In this example, we construct a DAG and find, in the first call, that there + are no directed cycles, and so an exception is raised. In the second call, + we ignore edge orientations and find that there is an undirected cycle. + Note that the second call finds a directed cycle while effectively + traversing an undirected graph, and so, we found an "undirected cycle". + This means that this DAG structure does not form a directed tree (which + is also known as a polytree). + + >>> import networkx as nx + >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) + >>> try: + ... nx.find_cycle(G, orientation='original') + ... except: + ... pass + ... + >>> list(nx.find_cycle(G, orientation='ignore')) + [(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')] + + """ + if not G.is_directed() or orientation in (None, 'original'): + def tailhead(edge): + return edge[:2] + elif orientation == 'reverse': + def tailhead(edge): + return edge[1], edge[0] + elif orientation == 'ignore': + def tailhead(edge): + if edge[-1] == 'reverse': + return edge[1], edge[0] + return edge[:2] + + explored = set() + cycle = [] + final_node = None + for start_node in G.nbunch_iter(source): + if start_node in explored: + # No loop is possible. + continue + + edges = [] + # All nodes seen in this iteration of edge_dfs + seen = {start_node} + # Nodes in active path. + active_nodes = {start_node} + previous_head = None + + for edge in nx.edge_dfs(G, start_node, orientation): + # Determine if this edge is a continuation of the active path. + tail, head = tailhead(edge) + if head in explored: + # Then we've already explored it. No loop is possible. + continue + if previous_head is not None and tail != previous_head: + # This edge results from backtracking. + # Pop until we get a node whose head equals the current tail. + # So for example, we might have: + # (0, 1), (1, 2), (2, 3), (1, 4) + # which must become: + # (0, 1), (1, 4) + while True: + try: + popped_edge = edges.pop() + except IndexError: + edges = [] + active_nodes = {tail} + break + else: + popped_head = tailhead(popped_edge)[1] + active_nodes.remove(popped_head) + + if edges: + last_head = tailhead(edges[-1])[1] + if tail == last_head: + break + edges.append(edge) + + if head in active_nodes: + # We have a loop! + cycle.extend(edges) + final_node = head + break + else: + seen.add(head) + active_nodes.add(head) + previous_head = head + + if cycle: + break + else: + explored.update(seen) + + else: + assert(len(cycle) == 0) + raise nx.exception.NetworkXNoCycle('No cycle found.') + + # We now have a list of edges which ends on a cycle. + # So we need to remove from the beginning edges that are not relevant. + + for i, edge in enumerate(cycle): + tail, head = tailhead(edge) + if tail == final_node: + break + + return cycle[i:] + + +@not_implemented_for('directed') +@not_implemented_for('multigraph') +def minimum_cycle_basis(G, weight=None): + """ Returns a minimum weight cycle basis for G + + Minimum weight means a cycle basis for which the total weight + (length for unweighted graphs) of all the cycles is minimum. + + Parameters + ---------- + G : NetworkX Graph + weight: string + name of the edge attribute to use for edge weights + + Returns + ------- + A list of cycle lists. Each cycle list is a list of nodes + which forms a cycle (loop) in G. Note that the nodes are not + necessarily returned in a order by which they appear in the cycle + + Examples + -------- + >>> G=nx.Graph() + >>> nx.add_cycle(G, [0,1,2,3]) + >>> nx.add_cycle(G, [0,3,4,5]) + >>> print([sorted(c) for c in nx.minimum_cycle_basis(G)]) + [[0, 1, 2, 3], [0, 3, 4, 5]] + + References: + [1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for + Minimum Cycle Basis of Graphs." + http://link.springer.com/article/10.1007/s00453-007-9064-z + [2] de Pina, J. 1995. Applications of shortest path methods. + Ph.D. thesis, University of Amsterdam, Netherlands + + See Also + -------- + simple_cycles, cycle_basis + """ + # We first split the graph in commected subgraphs + return sum((_min_cycle_basis(G.subgraph(c), weight) for c in + nx.connected_components(G)), []) + + +def _min_cycle_basis(comp, weight): + cb = [] + # We extract the edges not in a spanning tree. We do not really need a + # *minimum* spanning tree. That is why we call the next function with + # weight=None. Depending on implementation, it may be faster as well + spanning_tree_edges = list(nx.minimum_spanning_edges(comp, weight=None, + data=False)) + edges_excl = [frozenset(e) for e in comp.edges() + if e not in spanning_tree_edges] + N = len(edges_excl) + + # We maintain a set of vectors orthogonal to sofar found cycles + set_orth = [set([edge]) for edge in edges_excl] + for k in range(N): + # kth cycle is "parallel" to kth vector in set_orth + new_cycle = _min_cycle(comp, set_orth[k], weight=weight) + cb.append(list(set().union(*new_cycle))) + # now update set_orth so that k+1,k+2... th elements are + # orthogonal to the newly found cycle, as per [p. 336, 1] + base = set_orth[k] + set_orth[k + 1:] = [orth ^ base if len(orth & new_cycle) % 2 else orth + for orth in set_orth[k + 1:]] + return cb + + +def _min_cycle(G, orth, weight=None): + """ + Computes the minimum weight cycle in G, + orthogonal to the vector orth as per [p. 338, 1] + """ + T = nx.Graph() + + nodes_idx = {node: idx for idx, node in enumerate(G.nodes())} + idx_nodes = {idx: node for node, idx in nodes_idx.items()} + + nnodes = len(nodes_idx) + + # Add 2 copies of each edge in G to T. If edge is in orth, add cross edge; + # otherwise in-plane edge + for u, v, data in G.edges(data=True): + uidx, vidx = nodes_idx[u], nodes_idx[v] + edge_w = data.get(weight, 1) + if frozenset((u, v)) in orth: + T.add_edges_from( + [(uidx, nnodes + vidx), (nnodes + uidx, vidx)], weight=edge_w) + else: + T.add_edges_from( + [(uidx, vidx), (nnodes + uidx, nnodes + vidx)], weight=edge_w) + + all_shortest_pathlens = dict(nx.shortest_path_length(T, weight=weight)) + cross_paths_w_lens = {n: all_shortest_pathlens[n][nnodes + n] + for n in range(nnodes)} + + # Now compute shortest paths in T, which translates to cyles in G + start = min(cross_paths_w_lens, key=cross_paths_w_lens.get) + end = nnodes + start + min_path = nx.shortest_path(T, source=start, target=end, weight='weight') + + # Now we obtain the actual path, re-map nodes in T to those in G + min_path_nodes = [node if node < nnodes else node - nnodes + for node in min_path] + # Now remove the edges that occur two times + mcycle_pruned = _path_to_cycle(min_path_nodes) + + return {frozenset((idx_nodes[u], idx_nodes[v])) for u, v in mcycle_pruned} + + +def _path_to_cycle(path): + """ + Removes the edges from path that occur even number of times. + Returns a set of edges + """ + edges = set() + for edge in pairwise(path): + # Toggle whether to keep the current edge. + edges ^= {edge} + return edges