diff --git a/loopy/tools.py b/loopy/tools.py index e16bac6b28d4800a6f15072885cf7366e4e40836..3324d8e62988b5c6a5955b5be7ea0f92b3d3301c 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -414,6 +414,161 @@ def compute_sccs(graph): # }}} +# {{{ compute topological order + +class CycleError(Exception): + """Raised when a topological ordering cannot be computed due to a cycle.""" + pass + + +def compute_topological_order(graph): + """Compute a toplogical order of nodes in a directed graph. + + :arg graph: A :class:`dict` representing a directed graph. The dictionary + contains one key representing each node in the graph, and this key maps + to a :class:`set` of nodes that are connected to the node by outgoing + edges. + + :returns: A :class:`list` representing a valid topological ordering of the + nodes in the directed graph. + """ + + # find a valid ordering of graph nodes + reverse_order = [] + visited = set() + visiting = set() + # go through each node + for root in graph: + + if root in visited: + # already encountered root as someone else's child + # and processed it at that time + continue + + stack = [(root, iter(graph[root]))] + visiting.add(root) + + while stack: + node, children = stack.pop() + + for child in children: + # note: each iteration removes child from children + if child in visiting: + raise CycleError() + + if child in visited: + continue + + visiting.add(child) + + # put (node, remaining children) back on stack + stack.append((node, children)) + + # put (child, grandchildren) on stack + stack.append((child, iter(graph.get(child, ())))) + break + else: + # loop did not break, + # so either this is a leaf or all children have been visited + visiting.remove(node) + visited.add(node) + reverse_order.append(node) + + return list(reversed(reverse_order)) + +# }}} + + +# {{{ compute transitive closure + +def compute_transitive_closure(graph): + """Compute the transitive closure of a directed graph using Warshall's + algorithm. + + :arg graph: A :class:`dict` representing a directed graph. The dictionary + contains one key representing each node in the graph, and this key maps + to a :class:`set` of nodes that are connected to the node by outgoing + edges. This graph may contain cycles. + + :returns: A :class:`dict` representing the transitive closure of the graph. + """ + # Warshall's algorithm + + from copy import deepcopy + closure = deepcopy(graph) + + # (assumes all graph nodes are included in keys) + for k in graph.keys(): + for n1 in graph.keys(): + for n2 in graph.keys(): + if k in closure[n1] and n2 in closure[k]: + closure[n1].add(n2) + + return closure + +# }}} + + +# {{{ check for cycle + +def contains_cycle(graph): + """Determine whether a graph contains a cycle. + + :arg graph: A :class:`dict` representing a directed graph. The dictionary + contains one key representing each node in the graph, and this key maps + to a :class:`set` of nodes that are connected to the node by outgoing + edges. + + :returns: A :class:`bool` indicating whether the graph contains a cycle. + """ + + try: + compute_topological_order(graph) + return False + except CycleError: + return True + +# }}} + + +# {{{ get induced subgraph + +def get_induced_subgraph(graph, subgraph_nodes): + """Compute the induced subgraph formed by a subset of the vertices in a + graph. + + :arg graph: A :class:`dict` representing a directed graph. The dictionary + contains one key representing each node in the graph, and this key maps + to a :class:`set` of nodes that are connected to the node by outgoing + edges. + + :arg subgraph_nodes: A :class:`set` containing a subset of the graph nodes + graph. + + :returns: A :class:`dict` representing the induced subgraph formed by + the subset of the vertices included in `subgraph_nodes`. + """ + + new_graph = {} + for node, children in graph.items(): + if node in subgraph_nodes: + new_graph[node] = children & subgraph_nodes + return new_graph + +# }}} + + +# {{{ get graph sources + +def get_graph_sources(graph): + sources = set(graph.keys()) + for non_sources in graph.values(): + sources -= non_sources + return sources + +# }}} + + # {{{ pickled container value class _PickledObject(object): diff --git a/test/test_misc.py b/test/test_misc.py index 7a834a6f5d393298e97df22d47a1de3b64354a42..2cd5b4be240dae74ca7c7876272961bb55af3a5a 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -79,6 +79,182 @@ def test_compute_sccs(): verify_sccs(graph, compute_sccs(graph)) +def test_compute_topological_order(): + from loopy.tools import compute_topological_order, CycleError + + empty = {} + assert compute_topological_order(empty) == [] + + disconnected = {1: [], 2: [], 3: []} + assert len(compute_topological_order(disconnected)) == 3 + + line = list(zip(range(10), ([i] for i in range(1, 11)))) + import random + random.seed(0) + random.shuffle(line) + expected = list(range(11)) + assert compute_topological_order(dict(line)) == expected + + claw = {1: [2, 3], 0: [1]} + assert compute_topological_order(claw)[:2] == [0, 1] + + repeated_edges = {1: [2, 2], 2: [0]} + assert compute_topological_order(repeated_edges) == [1, 2, 0] + + self_cycle = {1: [1]} + with pytest.raises(CycleError): + compute_topological_order(self_cycle) + + cycle = {0: [2], 1: [2], 2: [3], 3: [4, 1]} + with pytest.raises(CycleError): + compute_topological_order(cycle) + + +def test_transitive_closure(): + from loopy.tools import compute_transitive_closure + + # simple test + graph = { + 1: set([2, ]), + 2: set([3, ]), + 3: set([4, ]), + 4: set(), + } + + expected_closure = { + 1: set([2, 3, 4, ]), + 2: set([3, 4, ]), + 3: set([4, ]), + 4: set(), + } + + closure = compute_transitive_closure(graph) + + assert closure == expected_closure + + # test with branches that re-connect + graph = { + 1: set([2, ]), + 2: set(), + 3: set([1, ]), + 4: set([1, ]), + 5: set([6, 7, ]), + 6: set([7, ]), + 7: set([1, ]), + 8: set([3, 4, ]), + } + + expected_closure = { + 1: set([2, ]), + 2: set(), + 3: set([1, 2, ]), + 4: set([1, 2, ]), + 5: set([1, 2, 6, 7, ]), + 6: set([1, 2, 7, ]), + 7: set([1, 2, ]), + 8: set([1, 2, 3, 4, ]), + } + + closure = compute_transitive_closure(graph) + + assert closure == expected_closure + + # test with cycles + graph = { + 1: set([2, ]), + 2: set([3, ]), + 3: set([4, ]), + 4: set([1, ]), + } + + expected_closure = { + 1: set([1, 2, 3, 4, ]), + 2: set([1, 2, 3, 4, ]), + 3: set([1, 2, 3, 4, ]), + 4: set([1, 2, 3, 4, ]), + } + + closure = compute_transitive_closure(graph) + + assert closure == expected_closure + + +def test_graph_cycle_finder(): + + from loopy.tools import contains_cycle + + graph = { + "a": set(["b", "c"]), + "b": set(["d", "e"]), + "c": set(["d", "f"]), + "d": set(), + "e": set(), + "f": set(["g", ]), + "g": set(), + } + + assert not contains_cycle(graph) + + graph = { + "a": set(["b", "c"]), + "b": set(["d", "e"]), + "c": set(["d", "f"]), + "d": set(), + "e": set(), + "f": set(["g", ]), + "g": set(["a", ]), + } + + assert contains_cycle(graph) + + graph = { + "a": set(["a", "c"]), + "b": set(["d", "e"]), + "c": set(["d", "f"]), + "d": set(), + "e": set(), + "f": set(["g", ]), + "g": set(), + } + + assert contains_cycle(graph) + + graph = { + "a": set(["a"]), + } + + assert contains_cycle(graph) + + +def test_induced_subgraph(): + + from loopy.tools import get_induced_subgraph + + graph = { + "a": set(["b", "c"]), + "b": set(["d", "e"]), + "c": set(["d", "f"]), + "d": set(), + "e": set(), + "f": set(["g", ]), + "g": set(["h", "i", "j"]), + } + + node_subset = set(["b", "c", "e", "f", "g"]) + + expected_subgraph = { + "b": set(["e", ]), + "c": set(["f", ]), + "e": set(), + "f": set(["g", ]), + "g": set(), + } + + subgraph = get_induced_subgraph(graph, node_subset) + + assert subgraph == expected_subgraph + + def test_SetTrie(): from loopy.kernel.tools import SetTrie