From 06fa47a1d3f8861065ea31b7ae636f0e9f34979d Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 20:47:27 -0500 Subject: [PATCH 01/29] add CycleError class --- pytools/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pytools/__init__.py b/pytools/__init__.py index 778976a..7228e80 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1432,6 +1432,11 @@ def a_star( # pylint: disable=too-many-locals raise RuntimeError("no solution") + +class CycleError(Exception): + """Raised when a topological ordering cannot be computed due to a cycle.""" + pass + # }}} -- GitLab From bff668eef58aea404526e241d4da055fa3421480 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 20:49:14 -0500 Subject: [PATCH 02/29] add compute_topological_order --- pytools/__init__.py | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pytools/__init__.py b/pytools/__init__.py index 7228e80..b57d1f5 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1433,10 +1433,70 @@ def a_star( # pylint: disable=too-many-locals raise RuntimeError("no solution") +# {{{ 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)) + +# }}} + # }}} -- GitLab From 204e6e7489280ce714ce021de6160b2d78250406 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 20:56:18 -0500 Subject: [PATCH 03/29] add test for compute_topological_order --- test/test_graph_tools.py | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/test_graph_tools.py diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py new file mode 100644 index 0000000..bcc5728 --- /dev/null +++ b/test/test_graph_tools.py @@ -0,0 +1,41 @@ +import sys +import pytest + + +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) + + +if __name__ == "__main__": + if len(sys.argv) > 1: + exec(sys.argv[1]) + else: + from pytest import main + main([__file__]) -- GitLab From bb722546bb59a4e75fb70bec3b39068b4f581ba7 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 20:57:49 -0500 Subject: [PATCH 04/29] add compute_transitive_closure --- pytools/__init__.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pytools/__init__.py b/pytools/__init__.py index b57d1f5..6467b5b 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1497,6 +1497,36 @@ 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 + 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 + +# }}} + # }}} -- GitLab From 62915e63315e87fce62a177fc297b81eeda423c4 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 20:59:29 -0500 Subject: [PATCH 05/29] add test for compute_transitive_closure --- test/test_graph_tools.py | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py index bcc5728..285a92f 100644 --- a/test/test_graph_tools.py +++ b/test/test_graph_tools.py @@ -33,6 +33,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 reconnect + 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 + + if __name__ == "__main__": if len(sys.argv) > 1: exec(sys.argv[1]) -- GitLab From d15babf042d994a1ecbbdcfa04f8b990c387093a Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:00:16 -0500 Subject: [PATCH 06/29] add contains_cycle() --- pytools/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pytools/__init__.py b/pytools/__init__.py index 6467b5b..35131f5 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1527,6 +1527,28 @@ def compute_transitive_closure(graph): # }}} + +# {{{ 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 + +# }}} + # }}} -- GitLab From e09f3626a6bfc8d8b379f86c1b1006fc180b68b4 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:01:14 -0500 Subject: [PATCH 07/29] add test for contains_cycle() --- test/test_graph_tools.py | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py index 285a92f..dec59ad 100644 --- a/test/test_graph_tools.py +++ b/test/test_graph_tools.py @@ -102,6 +102,53 @@ def test_transitive_closure(): 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) + + if __name__ == "__main__": if len(sys.argv) > 1: exec(sys.argv[1]) -- GitLab From b575870f9a35164a72a7c930bbc8f5d081a98f0a Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:01:59 -0500 Subject: [PATCH 08/29] add get_induced_subgraph() --- pytools/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pytools/__init__.py b/pytools/__init__.py index 35131f5..713813d 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1549,6 +1549,33 @@ def contains_cycle(graph): # }}} + +# {{{ 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 + +# }}} + # }}} -- GitLab From c2cec1f4b663bbe13dacd7da33e22805c743cc20 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:03:04 -0500 Subject: [PATCH 09/29] add test for get_induced_subgraph --- test/test_graph_tools.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py index dec59ad..feb5ea4 100644 --- a/test/test_graph_tools.py +++ b/test/test_graph_tools.py @@ -149,6 +149,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 + + if __name__ == "__main__": if len(sys.argv) > 1: exec(sys.argv[1]) -- GitLab From 6c5a066158ec11e5dcb9c39dbb7ad3fcd06b0404 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:05:09 -0500 Subject: [PATCH 10/29] add a newline --- pytools/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytools/__init__.py b/pytools/__init__.py index 713813d..7841269 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1456,6 +1456,7 @@ def compute_topological_order(graph): reverse_order = [] visited = set() visiting = set() + # go through each node for root in graph: -- GitLab From 62e8f592bfb0be32ec1f8d2c8f1a601331bfd4ba Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:10:35 -0500 Subject: [PATCH 11/29] bump version --- pytools/__init__.py | 8 ++++++++ pytools/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index 7841269..93f80b0 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1450,6 +1450,8 @@ def compute_topological_order(graph): :returns: A :class:`list` representing a valid topological ordering of the nodes in the directed graph. + + .. versionadded:: 2020.2 """ # find a valid ordering of graph nodes @@ -1511,6 +1513,8 @@ def compute_transitive_closure(graph): edges. This graph may contain cycles. :returns: A :class:`dict` representing the transitive closure of the graph. + + .. versionadded:: 2020.2 """ # Warshall's algorithm @@ -1540,6 +1544,8 @@ def contains_cycle(graph): edges. :returns: A :class:`bool` indicating whether the graph contains a cycle. + + .. versionadded:: 2020.2 """ try: @@ -1567,6 +1573,8 @@ def get_induced_subgraph(graph, subgraph_nodes): :returns: A :class:`dict` representing the induced subgraph formed by the subset of the vertices included in `subgraph_nodes`. + + .. versionadded:: 2020.2 """ new_graph = {} diff --git a/pytools/version.py b/pytools/version.py index ea2c3fd..0371d44 100644 --- a/pytools/version.py +++ b/pytools/version.py @@ -1,3 +1,3 @@ -VERSION = (2020, 1) +VERSION = (2020, 2) VERSION_STATUS = "" VERSION_TEXT = ".".join(str(x) for x in VERSION) + VERSION_STATUS -- GitLab From be61e493f545cdd23ce6b5b120f3375780246f8c Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:13:45 -0500 Subject: [PATCH 12/29] add compute_sccs (moved from loopy) --- pytools/__init__.py | 63 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pytools/__init__.py b/pytools/__init__.py index 93f80b0..40341ae 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1371,6 +1371,8 @@ def get_write_to_map_from_permutation(original, permuted): # {{{ graph algorithms +# {{{ a_star + def a_star( # pylint: disable=too-many-locals initial_state, goal_state, neighbor_map, estimate_remaining_cost=None, @@ -1432,6 +1434,67 @@ def a_star( # pylint: disable=too-many-locals raise RuntimeError("no solution") +# }}} + + +# {{{ compute SCCs with Tarjan's algorithm + +def compute_sccs(graph): + to_search = set(graph.keys()) + visit_order = {} + scc_root = {} + sccs = [] + + while to_search: + top = next(iter(to_search)) + call_stack = [(top, iter(graph[top]), None)] + visit_stack = [] + visiting = set() + + scc = [] + + while call_stack: + top, children, last_popped_child = call_stack.pop() + + if top not in visiting: + # Unvisited: mark as visited, initialize SCC root. + count = len(visit_order) + visit_stack.append(top) + visit_order[top] = count + scc_root[top] = count + visiting.add(top) + to_search.discard(top) + + # Returned from a recursion, update SCC. + if last_popped_child is not None: + scc_root[top] = min( + scc_root[top], + scc_root[last_popped_child]) + + for child in children: + if child not in visit_order: + # Recurse. + call_stack.append((top, children, child)) + call_stack.append((child, iter(graph[child]), None)) + break + if child in visiting: + scc_root[top] = min( + scc_root[top], + visit_order[child]) + else: + if scc_root[top] == visit_order[top]: + scc = [] + while visit_stack[-1] != top: + scc.append(visit_stack.pop()) + scc.append(visit_stack.pop()) + for item in scc: + visiting.remove(item) + sccs.append(scc) + + return sccs + +# }}} + # {{{ compute topological order -- GitLab From 86f15ae4896124c322d5ca6ddbe802242ed32dad Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:15:23 -0500 Subject: [PATCH 13/29] add test for compute_sccs (copied in from loopy) --- test/test_graph_tools.py | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py index feb5ea4..b598829 100644 --- a/test/test_graph_tools.py +++ b/test/test_graph_tools.py @@ -2,6 +2,50 @@ import sys import pytest +def test_compute_sccs(): + from loopy.tools import compute_sccs + import random + + rng = random.Random(0) + + def generate_random_graph(nnodes): + graph = dict((i, set()) for i in range(nnodes)) + for i in range(nnodes): + for j in range(nnodes): + # Edge probability 2/n: Generates decently interesting inputs. + if rng.randint(0, nnodes - 1) <= 1: + graph[i].add(j) + return graph + + def verify_sccs(graph, sccs): + visited = set() + + def visit(node): + if node in visited: + return [] + else: + visited.add(node) + result = [] + for child in graph[node]: + result = result + visit(child) + return result + [node] + + for scc in sccs: + scc = set(scc) + assert not scc & visited + # Check that starting from each element of the SCC results + # in the same set of reachable nodes. + for scc_root in scc: + visited.difference_update(scc) + result = visit(scc_root) + assert set(result) == scc, (set(result), scc) + + for nnodes in range(10, 20): + for i in range(40): + graph = generate_random_graph(nnodes) + verify_sccs(graph, compute_sccs(graph)) + + def test_compute_topological_order(): from loopy.tools import compute_topological_order, CycleError -- GitLab From ccdafe3cc63d5c4d97839d0940bbea66f99a2169 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:17:38 -0500 Subject: [PATCH 14/29] import from pytools, not loopy --- test/test_graph_tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py index b598829..001e911 100644 --- a/test/test_graph_tools.py +++ b/test/test_graph_tools.py @@ -3,7 +3,7 @@ import pytest def test_compute_sccs(): - from loopy.tools import compute_sccs + from pytools import compute_sccs import random rng = random.Random(0) @@ -47,7 +47,7 @@ def test_compute_sccs(): def test_compute_topological_order(): - from loopy.tools import compute_topological_order, CycleError + from pytools import compute_topological_order, CycleError empty = {} assert compute_topological_order(empty) == [] @@ -78,7 +78,7 @@ def test_compute_topological_order(): def test_transitive_closure(): - from loopy.tools import compute_transitive_closure + from pytools import compute_transitive_closure # simple test graph = { @@ -148,7 +148,7 @@ def test_transitive_closure(): def test_graph_cycle_finder(): - from loopy.tools import contains_cycle + from pytools import contains_cycle graph = { "a": set(["b", "c"]), @@ -195,7 +195,7 @@ def test_graph_cycle_finder(): def test_induced_subgraph(): - from loopy.tools import get_induced_subgraph + from pytools import get_induced_subgraph graph = { "a": set(["b", "c"]), -- GitLab From 1606142d40275b171f89175e901945bc78813d26 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 11 May 2020 21:29:30 -0500 Subject: [PATCH 15/29] change single letter var name l->line --- pytools/py_codegen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytools/py_codegen.py b/pytools/py_codegen.py index e2a5144..b2a0ef7 100644 --- a/pytools/py_codegen.py +++ b/pytools/py_codegen.py @@ -76,8 +76,8 @@ class PythonCodeGenerator(object): if "\n" in s: s = remove_common_indentation(s) - for l in s.split("\n"): - self.code.append(" "*(4*self.level) + l) + for line in s.split("\n"): + self.code.append(" "*(4*self.level) + line) def indent(self): self.level += 1 -- GitLab From 2865c6d7f4208f4c8e970ad3058d8f5890170790 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Wed, 13 May 2020 17:40:03 -0500 Subject: [PATCH 16/29] move graph tools into submodule graph.py --- pytools/__init__.py | 287 ---------------------------------- pytools/graph.py | 330 +++++++++++++++++++++++++++++++++++++++ test/test_graph_tools.py | 10 +- 3 files changed, 335 insertions(+), 292 deletions(-) create mode 100644 pytools/graph.py diff --git a/pytools/__init__.py b/pytools/__init__.py index 40341ae..06efcf2 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -92,11 +92,6 @@ Permutations, Tuples, Integer sequences .. autofunction:: generate_permutations .. autofunction:: generate_unique_permutations -Graph Algorithms ----------------- - -.. autofunction:: a_star - Formatting ---------- @@ -1369,288 +1364,6 @@ def get_write_to_map_from_permutation(original, permuted): # }}} -# {{{ graph algorithms - -# {{{ a_star - -def a_star( # pylint: disable=too-many-locals - initial_state, goal_state, neighbor_map, - estimate_remaining_cost=None, - get_step_cost=lambda x, y: 1 - ): - """ - With the default cost and heuristic, this amounts to Dijkstra's algorithm. - """ - - from heapq import heappop, heappush - - if estimate_remaining_cost is None: - def estimate_remaining_cost(x): # pylint: disable=function-redefined - if x != goal_state: - return 1 - else: - return 0 - - class AStarNode(object): - __slots__ = ["state", "parent", "path_cost"] - - def __init__(self, state, parent, path_cost): - self.state = state - self.parent = parent - self.path_cost = path_cost - - inf = float("inf") - init_remcost = estimate_remaining_cost(initial_state) - assert init_remcost != inf - - queue = [(init_remcost, AStarNode(initial_state, parent=None, path_cost=0))] - visited_states = set() - - while queue: - _, top = heappop(queue) - visited_states.add(top.state) - - if top.state == goal_state: - result = [] - it = top - while it is not None: - result.append(it.state) - it = it.parent - return result[::-1] - - for state in neighbor_map[top.state]: - if state in visited_states: - continue - - remaining_cost = estimate_remaining_cost(state) - if remaining_cost == inf: - continue - step_cost = get_step_cost(top, state) - - estimated_path_cost = top.path_cost+step_cost+remaining_cost - heappush(queue, - (estimated_path_cost, - AStarNode(state, top, path_cost=top.path_cost + step_cost))) - - raise RuntimeError("no solution") - -# }}} - - -# {{{ compute SCCs with Tarjan's algorithm - -def compute_sccs(graph): - to_search = set(graph.keys()) - visit_order = {} - scc_root = {} - sccs = [] - - while to_search: - top = next(iter(to_search)) - call_stack = [(top, iter(graph[top]), None)] - visit_stack = [] - visiting = set() - - scc = [] - - while call_stack: - top, children, last_popped_child = call_stack.pop() - - if top not in visiting: - # Unvisited: mark as visited, initialize SCC root. - count = len(visit_order) - visit_stack.append(top) - visit_order[top] = count - scc_root[top] = count - visiting.add(top) - to_search.discard(top) - - # Returned from a recursion, update SCC. - if last_popped_child is not None: - scc_root[top] = min( - scc_root[top], - scc_root[last_popped_child]) - - for child in children: - if child not in visit_order: - # Recurse. - call_stack.append((top, children, child)) - call_stack.append((child, iter(graph[child]), None)) - break - if child in visiting: - scc_root[top] = min( - scc_root[top], - visit_order[child]) - else: - if scc_root[top] == visit_order[top]: - scc = [] - while visit_stack[-1] != top: - scc.append(visit_stack.pop()) - scc.append(visit_stack.pop()) - for item in scc: - visiting.remove(item) - sccs.append(scc) - - return sccs - -# }}} - - -# {{{ 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. - - .. versionadded:: 2020.2 - """ - - # 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. - - .. versionadded:: 2020.2 - """ - # 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. - - .. versionadded:: 2020.2 - """ - - 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`. - - .. versionadded:: 2020.2 - """ - - new_graph = {} - for node, children in graph.items(): - if node in subgraph_nodes: - new_graph[node] = children & subgraph_nodes - return new_graph - -# }}} - -# }}} - - # {{{ formatting # {{{ table formatting diff --git a/pytools/graph.py b/pytools/graph.py new file mode 100644 index 0000000..f092736 --- /dev/null +++ b/pytools/graph.py @@ -0,0 +1,330 @@ +from __future__ import division, absolute_import, print_function + +__copyright__ = """ +Copyright (C) 2009-2013 Andreas Kloeckner +Copyright (C) 2020 Matt Wala +Copyright (C) 2020 James Stevens +""" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + + +__doc__ = """ +Graph Algorithms +========================= + +.. autofunction:: a_star +.. autofunction:: compute_sccs +.. autoclass:: CycleError +.. autofunction:: compute_topological_order +.. autofunction:: compute_transitive_closure +.. autofunction:: contains_cycle +.. autofunction:: get_induced_subgraph +""" + + +# {{{ a_star + +def a_star( # pylint: disable=too-many-locals + initial_state, goal_state, neighbor_map, + estimate_remaining_cost=None, + get_step_cost=lambda x, y: 1 + ): + """ + With the default cost and heuristic, this amounts to Dijkstra's algorithm. + """ + + from heapq import heappop, heappush + + if estimate_remaining_cost is None: + def estimate_remaining_cost(x): # pylint: disable=function-redefined + if x != goal_state: + return 1 + else: + return 0 + + class AStarNode(object): + __slots__ = ["state", "parent", "path_cost"] + + def __init__(self, state, parent, path_cost): + self.state = state + self.parent = parent + self.path_cost = path_cost + + inf = float("inf") + init_remcost = estimate_remaining_cost(initial_state) + assert init_remcost != inf + + queue = [(init_remcost, AStarNode(initial_state, parent=None, path_cost=0))] + visited_states = set() + + while queue: + _, top = heappop(queue) + visited_states.add(top.state) + + if top.state == goal_state: + result = [] + it = top + while it is not None: + result.append(it.state) + it = it.parent + return result[::-1] + + for state in neighbor_map[top.state]: + if state in visited_states: + continue + + remaining_cost = estimate_remaining_cost(state) + if remaining_cost == inf: + continue + step_cost = get_step_cost(top, state) + + estimated_path_cost = top.path_cost+step_cost+remaining_cost + heappush(queue, + (estimated_path_cost, + AStarNode(state, top, path_cost=top.path_cost + step_cost))) + + raise RuntimeError("no solution") + +# }}} + + +# {{{ compute SCCs with Tarjan's algorithm + +def compute_sccs(graph): + to_search = set(graph.keys()) + visit_order = {} + scc_root = {} + sccs = [] + + while to_search: + top = next(iter(to_search)) + call_stack = [(top, iter(graph[top]), None)] + visit_stack = [] + visiting = set() + + scc = [] + + while call_stack: + top, children, last_popped_child = call_stack.pop() + + if top not in visiting: + # Unvisited: mark as visited, initialize SCC root. + count = len(visit_order) + visit_stack.append(top) + visit_order[top] = count + scc_root[top] = count + visiting.add(top) + to_search.discard(top) + + # Returned from a recursion, update SCC. + if last_popped_child is not None: + scc_root[top] = min( + scc_root[top], + scc_root[last_popped_child]) + + for child in children: + if child not in visit_order: + # Recurse. + call_stack.append((top, children, child)) + call_stack.append((child, iter(graph[child]), None)) + break + if child in visiting: + scc_root[top] = min( + scc_root[top], + visit_order[child]) + else: + if scc_root[top] == visit_order[top]: + scc = [] + while visit_stack[-1] != top: + scc.append(visit_stack.pop()) + scc.append(visit_stack.pop()) + for item in scc: + visiting.remove(item) + sccs.append(scc) + + return sccs + +# }}} + + +# {{{ 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. + + .. versionadded:: 2020.2 + """ + + # 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. + + .. versionadded:: 2020.2 + """ + # 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. + + .. versionadded:: 2020.2 + """ + + 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`. + + .. versionadded:: 2020.2 + """ + + new_graph = {} + for node, children in graph.items(): + if node in subgraph_nodes: + new_graph[node] = children & subgraph_nodes + return new_graph + +# }}} + + +def _test(): + import doctest + doctest.testmod() + + +if __name__ == "__main__": + _test() + +# vim: foldmethod=marker diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py index 001e911..d502257 100644 --- a/test/test_graph_tools.py +++ b/test/test_graph_tools.py @@ -3,7 +3,7 @@ import pytest def test_compute_sccs(): - from pytools import compute_sccs + from pytools.graph import compute_sccs import random rng = random.Random(0) @@ -47,7 +47,7 @@ def test_compute_sccs(): def test_compute_topological_order(): - from pytools import compute_topological_order, CycleError + from pytools.graph import compute_topological_order, CycleError empty = {} assert compute_topological_order(empty) == [] @@ -78,7 +78,7 @@ def test_compute_topological_order(): def test_transitive_closure(): - from pytools import compute_transitive_closure + from pytools.graph import compute_transitive_closure # simple test graph = { @@ -148,7 +148,7 @@ def test_transitive_closure(): def test_graph_cycle_finder(): - from pytools import contains_cycle + from pytools.graph import contains_cycle graph = { "a": set(["b", "c"]), @@ -195,7 +195,7 @@ def test_graph_cycle_finder(): def test_induced_subgraph(): - from pytools import get_induced_subgraph + from pytools.graph import get_induced_subgraph graph = { "a": set(["b", "c"]), -- GitLab From c00a4949928a5a449c36bb1b58a35cf92730ad9d Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Wed, 13 May 2020 17:41:35 -0500 Subject: [PATCH 17/29] rename get_induced_subgraph->compute_induced_subgraph --- pytools/graph.py | 6 +++--- test/test_graph_tools.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pytools/graph.py b/pytools/graph.py index f092736..c413310 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -37,7 +37,7 @@ Graph Algorithms .. autofunction:: compute_topological_order .. autofunction:: compute_transitive_closure .. autofunction:: contains_cycle -.. autofunction:: get_induced_subgraph +.. autofunction:: compute_induced_subgraph """ @@ -290,9 +290,9 @@ def contains_cycle(graph): # }}} -# {{{ get induced subgraph +# {{{ compute induced subgraph -def get_induced_subgraph(graph, subgraph_nodes): +def compute_induced_subgraph(graph, subgraph_nodes): """Compute the induced subgraph formed by a subset of the vertices in a graph. diff --git a/test/test_graph_tools.py b/test/test_graph_tools.py index d502257..f1b349c 100644 --- a/test/test_graph_tools.py +++ b/test/test_graph_tools.py @@ -195,7 +195,7 @@ def test_graph_cycle_finder(): def test_induced_subgraph(): - from pytools.graph import get_induced_subgraph + from pytools.graph import compute_induced_subgraph graph = { "a": set(["b", "c"]), @@ -217,7 +217,7 @@ def test_induced_subgraph(): "g": set(), } - subgraph = get_induced_subgraph(graph, node_subset) + subgraph = compute_induced_subgraph(graph, node_subset) assert subgraph == expected_subgraph -- GitLab From 5902369a7fcf2c7a1bc4285dbf5cb2024c7e5ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Thu, 14 May 2020 00:54:03 +0200 Subject: [PATCH 18/29] Add docs for graph module --- doc/graph.rst | 1 + doc/index.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 doc/graph.rst diff --git a/doc/graph.rst b/doc/graph.rst new file mode 100644 index 0000000..7ad4b81 --- /dev/null +++ b/doc/graph.rst @@ -0,0 +1 @@ +.. automodule:: pytools.graph diff --git a/doc/index.rst b/doc/index.rst index fb3dedd..d2860d0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,6 +8,7 @@ Welcome to pytools's documentation! reference obj_array persistent_dict + graph misc Indices and tables -- GitLab From d9ba86c1a4d9e731fdd78d9014004a29dd0eaec5 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Wed, 13 May 2020 18:16:39 -0500 Subject: [PATCH 19/29] add graph module to docs --- doc/graph.rst | 1 + doc/index.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 doc/graph.rst diff --git a/doc/graph.rst b/doc/graph.rst new file mode 100644 index 0000000..7ad4b81 --- /dev/null +++ b/doc/graph.rst @@ -0,0 +1 @@ +.. automodule:: pytools.graph diff --git a/doc/index.rst b/doc/index.rst index fb3dedd..d2860d0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,6 +8,7 @@ Welcome to pytools's documentation! reference obj_array persistent_dict + graph misc Indices and tables -- GitLab From 431d4bdf376a1393cc2bdf8522ed4f3e225eeeef Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Wed, 13 May 2020 18:50:14 -0500 Subject: [PATCH 20/29] keep pytools.a_star callable; add deprecation warning pointing to pytools.graph.a_star --- pytools/__init__.py | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/pytools/__init__.py b/pytools/__init__.py index 06efcf2..d3dcb77 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -1364,6 +1364,31 @@ def get_write_to_map_from_permutation(original, permuted): # }}} +# {{{ code maintenance + +class MovedFunctionDeprecationWrapper: + def __init__(self, f): + self.f = f + + def __call__(self, *args, **kwargs): + from warnings import warn + warn("This function is deprecated. Use %s.%s instead." % ( + self.f.__module__, self.f.__name__), + DeprecationWarning, stacklevel=2) + + return self.f(*args, **kwargs) + +# }}} + + +# {{{ graph algorithms + +from pytools.graph import a_star as a_star_moved +a_star = MovedFunctionDeprecationWrapper(a_star_moved) + +# }}} + + # {{{ formatting # {{{ table formatting @@ -1568,23 +1593,6 @@ class CPyUserInterface(object): # }}} -# {{{ code maintenance - -class MovedFunctionDeprecationWrapper: - def __init__(self, f): - self.f = f - - def __call__(self, *args, **kwargs): - from warnings import warn - warn("This function is deprecated. Use %s.%s instead." % ( - self.f.__module__, self.f.__name__), - DeprecationWarning, stacklevel=2) - - return self.f(*args, **kwargs) - -# }}} - - # {{{ debugging class StderrToStdout(object): -- GitLab From 69fde78f39cb54c8808eb8bb6067b009b9f1aee3 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Thu, 14 May 2020 23:19:19 -0500 Subject: [PATCH 21/29] use higher level Python concept names in docstrings wherever possible --- pytools/graph.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pytools/graph.py b/pytools/graph.py index c413310..4654d0f 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -176,9 +176,9 @@ 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 + :arg graph: A :class:`Mapping` 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 + to a :class:`Collection` of nodes that are connected to the node by outgoing edges. :returns: A :class:`list` representing a valid topological ordering of the @@ -240,9 +240,9 @@ 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 + :arg graph: A :class:`Mapping` 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 + to a :class:`MutableSet` 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. @@ -271,9 +271,9 @@ def compute_transitive_closure(graph): def contains_cycle(graph): """Determine whether a graph contains a cycle. - :arg graph: A :class:`dict` representing a directed graph. The dictionary + :arg graph: A :class:`Mapping` 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 + to a :class:`Collection` of nodes that are connected to the node by outgoing edges. :returns: A :class:`bool` indicating whether the graph contains a cycle. @@ -296,12 +296,12 @@ def compute_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 + :arg graph: A :class:`Mapping` 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 + 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 + :arg subgraph_nodes: A :class:`Set` containing a subset of the graph nodes graph. :returns: A :class:`dict` representing the induced subgraph formed by -- GitLab From df9867f4eac8a2924de59028ad43c5695d5bfae8 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Sun, 17 May 2020 19:26:43 -0500 Subject: [PATCH 22/29] prepend collections.abc to types in docs --- pytools/graph.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/pytools/graph.py b/pytools/graph.py index 4654d0f..d96b49d 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -176,10 +176,10 @@ class CycleError(Exception): def compute_topological_order(graph): """Compute a toplogical order of nodes in a directed graph. - :arg graph: A :class:`Mapping` representing a directed graph. The dictionary - contains one key representing each node in the graph, and this key maps - to a :class:`Collection` of nodes that are connected to the node by outgoing - edges. + :arg graph: A :class:`collections.abc.Mapping` representing a directed + graph. The dictionary contains one key representing each node in the + graph, and this key maps to a :class:`collections.abc.Collection` 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. @@ -240,10 +240,11 @@ def compute_transitive_closure(graph): """Compute the transitive closure of a directed graph using Warshall's algorithm. - :arg graph: A :class:`Mapping` representing a directed graph. The dictionary - contains one key representing each node in the graph, and this key maps - to a :class:`MutableSet` of nodes that are connected to the node by outgoing - edges. This graph may contain cycles. + :arg graph: A :class:`collections.abc.Mapping` representing a directed + graph. The dictionary contains one key representing each node in the + graph, and this key maps to a :class:`collections.abc.MutableSet` 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. @@ -271,10 +272,10 @@ def compute_transitive_closure(graph): def contains_cycle(graph): """Determine whether a graph contains a cycle. - :arg graph: A :class:`Mapping` representing a directed graph. The dictionary - contains one key representing each node in the graph, and this key maps - to a :class:`Collection` of nodes that are connected to the node by outgoing - edges. + :arg graph: A :class:`collections.abc.Mapping` representing a directed + graph. The dictionary contains one key representing each node in the + graph, and this key maps to a :class:`collections.abc.Collection` of + nodes that are connected to the node by outgoing edges. :returns: A :class:`bool` indicating whether the graph contains a cycle. @@ -296,13 +297,13 @@ def compute_induced_subgraph(graph, subgraph_nodes): """Compute the induced subgraph formed by a subset of the vertices in a graph. - :arg graph: A :class:`Mapping` 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 graph: A :class:`collections.abc.Mapping` representing a directed + graph. The dictionary contains one key representing each node in the + graph, and this key maps to a :class:`collections.abc.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. + :arg subgraph_nodes: A :class:`collections.abc.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`. -- GitLab From ea569e53db97573fbd1298a630063c113586db91 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Sun, 17 May 2020 19:30:34 -0500 Subject: [PATCH 23/29] (in docstring for compute_topological_order()) graph vals only needs to be iterable --- pytools/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytools/graph.py b/pytools/graph.py index d96b49d..7c829de 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -178,7 +178,7 @@ def compute_topological_order(graph): :arg graph: A :class:`collections.abc.Mapping` representing a directed graph. The dictionary contains one key representing each node in the - graph, and this key maps to a :class:`collections.abc.Collection` of + graph, and this key maps to a :class:`collections.abc.Iterable` of nodes that are connected to the node by outgoing edges. :returns: A :class:`list` representing a valid topological ordering of the -- GitLab From 2416414ff07c19a7e31c374d25e3a6f98a5dec00 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Sun, 17 May 2020 19:41:26 -0500 Subject: [PATCH 24/29] (in docstring for contains_cycle()) graph vals only needs to be iterable --- pytools/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytools/graph.py b/pytools/graph.py index 7c829de..452859a 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -274,7 +274,7 @@ def contains_cycle(graph): :arg graph: A :class:`collections.abc.Mapping` representing a directed graph. The dictionary contains one key representing each node in the - graph, and this key maps to a :class:`collections.abc.Collection` of + graph, and this key maps to a :class:`collections.abc.Iterable` of nodes that are connected to the node by outgoing edges. :returns: A :class:`bool` indicating whether the graph contains a cycle. -- GitLab From 89fb21d8474da5ff81fbb5a8bae8bab2ee9cf594 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Sun, 17 May 2020 19:42:03 -0500 Subject: [PATCH 25/29] fix data type info in docstring for compute_transitive_closure() --- pytools/graph.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytools/graph.py b/pytools/graph.py index 452859a..7b40f52 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -244,9 +244,10 @@ def compute_transitive_closure(graph): graph. The dictionary contains one key representing each node in the graph, and this key maps to a :class:`collections.abc.MutableSet` of nodes that are connected to the node by outgoing edges. This graph may - contain cycles. + contain cycles. This object must be picklable. - :returns: A :class:`dict` representing the transitive closure of the graph. + :returns: The transitive closure of the graph, represented using the same + data type. .. versionadded:: 2020.2 """ -- GitLab From 6db13d2c4233dd99ea656ade276e62e079addd69 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Sun, 17 May 2020 20:00:04 -0500 Subject: [PATCH 26/29] allow compute_transitive_closure() graph val dtype to be Set (rather than enforcing MutableSet); update docstring --- pytools/graph.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pytools/graph.py b/pytools/graph.py index 7b40f52..11c8338 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -242,9 +242,10 @@ def compute_transitive_closure(graph): :arg graph: A :class:`collections.abc.Mapping` representing a directed graph. The dictionary contains one key representing each node in the - graph, and this key maps to a :class:`collections.abc.MutableSet` of + graph, and this key maps to a :class:`collections.abc.Set` of nodes that are connected to the node by outgoing edges. This graph may - contain cycles. This object must be picklable. + contain cycles. This object must be picklable. Every graph node must + be included as a key in the graph. :returns: The transitive closure of the graph, represented using the same data type. @@ -261,7 +262,7 @@ def compute_transitive_closure(graph): for n1 in graph.keys(): for n2 in graph.keys(): if k in closure[n1] and n2 in closure[k]: - closure[n1].add(n2) + closure[n1] = closure[n1] | set([n2, ]) return closure -- GitLab From f2f32f84ddacc48b12a46ae809829ec9baa5c017 Mon Sep 17 00:00:00 2001 From: jdsteve2 Date: Mon, 18 May 2020 00:40:10 -0500 Subject: [PATCH 27/29] change compute_transitive_closure() back to requiring graph vals be MutablSets --- pytools/graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytools/graph.py b/pytools/graph.py index 11c8338..e0fcd73 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -242,7 +242,7 @@ def compute_transitive_closure(graph): :arg graph: A :class:`collections.abc.Mapping` representing a directed graph. The dictionary contains one key representing each node in the - graph, and this key maps to a :class:`collections.abc.Set` of + graph, and this key maps to a :class:`collections.abc.MutableSet` of nodes that are connected to the node by outgoing edges. This graph may contain cycles. This object must be picklable. Every graph node must be included as a key in the graph. @@ -262,7 +262,7 @@ def compute_transitive_closure(graph): for n1 in graph.keys(): for n2 in graph.keys(): if k in closure[n1] and n2 in closure[k]: - closure[n1] = closure[n1] | set([n2, ]) + closure[n1].add(n2) return closure -- GitLab From 7e2497a7e703ea24ae5c4e2ca01bc58256fa9c5e Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Mon, 18 May 2020 16:23:10 -0500 Subject: [PATCH 28/29] Add intersphinx mapping, fix doc build warning --- doc/conf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index c87091c..bf3d374 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -33,6 +33,7 @@ # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] @@ -114,7 +115,7 @@ html_sidebars = { # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ @@ -174,4 +175,9 @@ texinfo_documents = [ ] +intersphinx_mapping = { + 'http://docs.python.org/dev': None, + 'http://docs.scipy.org/doc/numpy/': None, + } + -- GitLab From a968ec2ac3bd0dbcaac83d5aa323db8a99b37f51 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Mon, 18 May 2020 16:23:31 -0500 Subject: [PATCH 29/29] Fix typo --- pytools/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytools/graph.py b/pytools/graph.py index e0fcd73..1279767 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -305,7 +305,7 @@ def compute_induced_subgraph(graph, subgraph_nodes): that are connected to the node by outgoing edges. :arg subgraph_nodes: A :class:`collections.abc.Set` containing a subset of - the graph nodes graph. + the graph nodes in the graph. :returns: A :class:`dict` representing the induced subgraph formed by the subset of the vertices included in `subgraph_nodes`. -- GitLab