From cb6310efe6366f8cc3e66056567e996234105349 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 18:46:47 -0500 Subject: [PATCH 01/17] add Matt's compute_topological_order() fn --- loopy/tools.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index e16bac6b2..c520e2c20 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -414,6 +414,60 @@ 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): + # 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)) + +# }}} + + # {{{ pickled container value class _PickledObject(object): -- GitLab From 83c05b0d14fb3893bce766460458583e82da5f24 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 18:47:08 -0500 Subject: [PATCH 02/17] add Matt's test for compute_topological_order() --- test/test_misc.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/test_misc.py b/test/test_misc.py index 7a834a6f5..7f867b149 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -79,6 +79,37 @@ 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_SetTrie(): from loopy.kernel.tools import SetTrie -- GitLab From 3085ea261ca43dac1023d7863421e5747aa7ee5b Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 18:51:01 -0500 Subject: [PATCH 03/17] add compute_transitive_closure() --- loopy/tools.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index c520e2c20..7b7d70f8e 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -468,6 +468,28 @@ def compute_topological_order(graph): # }}} +# {{{ compute transitive closure + +def compute_transitive_closure(graph): + + # TODO use floyd-warshal algorithm, don't error with cycle + + def collect_all_descendants(node, visited): + descendants = set() + for child in graph[node]: + if child in visited: + raise CycleError + else: + descendants.update( + collect_all_descendants(child, visited | set([child, ]))) + return graph[node] | descendants + + return dict([ + (k, collect_all_descendants(k, set([k, ]))) for k in graph.keys()]) + +# }}} + + # {{{ pickled container value class _PickledObject(object): -- GitLab From 8c772618350ce84d473ac43a171a48070562a6cf Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 18:51:49 -0500 Subject: [PATCH 04/17] add contains_cycle() --- loopy/tools.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index 7b7d70f8e..19808e23c 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -490,6 +490,26 @@ def compute_transitive_closure(graph): # }}} +# {{{ check for cycle + +def contains_cycle(graph): + + def visit_descendants(node, visited): + for child in graph[node]: + if child in visited or visit_descendants( + child, visited | set([child, ])): + return True + return False + + for node in graph.keys(): + if visit_descendants(node, set([node, ])): + return True + + return False + +# }}} + + # {{{ pickled container value class _PickledObject(object): -- GitLab From 58b50c941662dd77d4272f530d2da41ac9ba538f Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 18:52:27 -0500 Subject: [PATCH 05/17] add get_induced_subgraph() --- loopy/tools.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index 19808e23c..4854b80dc 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -510,6 +510,18 @@ def contains_cycle(graph): # }}} +# {{{ get induced subgraph + +def get_induced_subgraph(graph, items): + new_graph = {} + for node, children in graph.items(): + if node in items: + new_graph[node] = graph[node] & items + return new_graph + +# }}} + + # {{{ pickled container value class _PickledObject(object): -- GitLab From ef3641c3620b1a31d9288f227fd919da182b505d Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 18:52:56 -0500 Subject: [PATCH 06/17] add get_graph_sources() --- loopy/tools.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index 4854b80dc..4200ed80c 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -522,6 +522,17 @@ def get_induced_subgraph(graph, items): # }}} +# {{{ 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): -- GitLab From 8baabc102c7745318707a597c141f6949109ec44 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 18:53:44 -0500 Subject: [PATCH 07/17] add test for graph cycle finder --- test/test_misc.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/test_misc.py b/test/test_misc.py index 7f867b149..499ba088c 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -110,6 +110,47 @@ def test_compute_topological_order(): compute_topological_order(cycle) +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) + + def test_SetTrie(): from loopy.kernel.tools import SetTrie -- GitLab From 50bb6cdebb51452eb124c20926fe3847e862019c Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 27 Apr 2020 23:34:34 -0500 Subject: [PATCH 08/17] add test for get_induced_subgraph() --- test/test_misc.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/test_misc.py b/test/test_misc.py index 499ba088c..649f83e8e 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -151,6 +151,35 @@ def test_graph_cycle_finder(): 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 -- GitLab From a8ac48872984971e992a104b6d9ce292bab70999 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Tue, 28 Apr 2020 00:57:39 -0500 Subject: [PATCH 09/17] change compute_transitive_closure() to use Warshall's algorithm, which allows cycles --- loopy/tools.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/loopy/tools.py b/loopy/tools.py index 4200ed80c..9d64291e6 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -471,21 +471,19 @@ def compute_topological_order(graph): # {{{ compute transitive closure def compute_transitive_closure(graph): + # Warshall's algorithm - # TODO use floyd-warshal algorithm, don't error with cycle + from copy import deepcopy + closure = deepcopy(graph) - def collect_all_descendants(node, visited): - descendants = set() - for child in graph[node]: - if child in visited: - raise CycleError - else: - descendants.update( - collect_all_descendants(child, visited | set([child, ]))) - return graph[node] | descendants + # (assumes all graph nodes are included in keys) + for k in graph.keys(): + for n1 in graph.keys(): + for n2 in graph.keys(): + if n2 in closure[n1] or (k in closure[n1] and n2 in closure[k]): + closure[n1].add(n2) - return dict([ - (k, collect_all_descendants(k, set([k, ]))) for k in graph.keys()]) + return closure # }}} -- GitLab From bd8d43dd0cc8b9e2bd922e67ce915ec214c8633b Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Tue, 28 Apr 2020 00:57:51 -0500 Subject: [PATCH 10/17] add test for compute_transitive_closure() --- test/test_misc.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/test_misc.py b/test/test_misc.py index 649f83e8e..fb06c609a 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -110,6 +110,75 @@ def test_compute_topological_order(): 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 -- GitLab From 0328d2df90d5cc677da8dd7b3f2345392b23b2b8 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Tue, 28 Apr 2020 01:11:01 -0500 Subject: [PATCH 11/17] docstring for compute_topological_order() --- loopy/tools.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index 9d64291e6..2a3d347c4 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -422,6 +422,17 @@ class CycleError(Exception): 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() -- GitLab From 3bbba9d7f8835cb75bf42e9f87fb5ef77f550b53 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Tue, 28 Apr 2020 01:14:44 -0500 Subject: [PATCH 12/17] docstring for compute_transitive_closure() --- loopy/tools.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index 2a3d347c4..258d1854c 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -482,6 +482,16 @@ def compute_topological_order(graph): # {{{ 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 -- GitLab From 88fc04c23b01585b1384eb00ab03dc459a9b9197 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Tue, 28 Apr 2020 01:16:50 -0500 Subject: [PATCH 13/17] docstring for contains_cycle() --- loopy/tools.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/loopy/tools.py b/loopy/tools.py index 258d1854c..47b18f7e7 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -512,6 +512,15 @@ def compute_transitive_closure(graph): # {{{ check for cycle def contains_cycle(graph): + """Determine whether a graph conatains 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. + """ def visit_descendants(node, visited): for child in graph[node]: -- GitLab From 7a03a31c833a8573e2fd899cebd14422f562634e Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Tue, 28 Apr 2020 01:24:31 -0500 Subject: [PATCH 14/17] docstring for get_induced_subgraph() --- loopy/tools.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/loopy/tools.py b/loopy/tools.py index 47b18f7e7..e19f7df16 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -512,7 +512,7 @@ def compute_transitive_closure(graph): # {{{ check for cycle def contains_cycle(graph): - """Determine whether a graph conatains a cycle. + """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 @@ -540,11 +540,26 @@ def contains_cycle(graph): # {{{ get induced subgraph -def get_induced_subgraph(graph, items): +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 items: - new_graph[node] = graph[node] & items + if node in subgraph_nodes: + new_graph[node] = graph[node] & subgraph_nodes return new_graph # }}} -- GitLab From 58276ced003c2c00bee308063d890df29b2a1ab3 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Tue, 28 Apr 2020 11:13:06 -0500 Subject: [PATCH 15/17] use existing variable 'children' instead of 'graph[node]' in get_induced_subgraph --- loopy/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopy/tools.py b/loopy/tools.py index e19f7df16..e325dda2a 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -559,7 +559,7 @@ def get_induced_subgraph(graph, subgraph_nodes): new_graph = {} for node, children in graph.items(): if node in subgraph_nodes: - new_graph[node] = graph[node] & subgraph_nodes + new_graph[node] = children & subgraph_nodes return new_graph # }}} -- GitLab From f757e5f3905b516e7e041a5ff8f2acf30b5ffcf5 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Sat, 2 May 2020 21:55:55 -0500 Subject: [PATCH 16/17] remove unnecessary conditional branch in compute_transitive_closure --- loopy/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopy/tools.py b/loopy/tools.py index e325dda2a..ae0eaac76 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -501,7 +501,7 @@ def compute_transitive_closure(graph): for k in graph.keys(): for n1 in graph.keys(): for n2 in graph.keys(): - if n2 in closure[n1] or (k in closure[n1] and n2 in closure[k]): + if k in closure[n1] and n2 in closure[k]: closure[n1].add(n2) return closure -- GitLab From 2b2edfdf2101defd5f50c2c3336c5c1eef276077 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Sat, 2 May 2020 22:17:48 -0500 Subject: [PATCH 17/17] make contains_cycle() just attempt to compute a topological order to detect cycles --- loopy/tools.py | 15 ++++----------- test/test_misc.py | 6 ++++++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/loopy/tools.py b/loopy/tools.py index ae0eaac76..3324d8e62 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -522,18 +522,11 @@ def contains_cycle(graph): :returns: A :class:`bool` indicating whether the graph contains a cycle. """ - def visit_descendants(node, visited): - for child in graph[node]: - if child in visited or visit_descendants( - child, visited | set([child, ])): - return True + try: + compute_topological_order(graph) return False - - for node in graph.keys(): - if visit_descendants(node, set([node, ])): - return True - - return False + except CycleError: + return True # }}} diff --git a/test/test_misc.py b/test/test_misc.py index fb06c609a..2cd5b4be2 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -219,6 +219,12 @@ def test_graph_cycle_finder(): assert contains_cycle(graph) + graph = { + "a": set(["a"]), + } + + assert contains_cycle(graph) + def test_induced_subgraph(): -- GitLab