graph.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this
  3. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. from __future__ import absolute_import, print_function, unicode_literals
  5. import collections
  6. class Graph(object):
  7. """
  8. Generic representation of a directed acyclic graph with labeled edges
  9. connecting the nodes. Graph operations are implemented in a functional
  10. manner, so the data structure is immutable.
  11. It permits at most one edge of a given name between any set of nodes. The
  12. graph is not checked for cycles, and methods may hang or otherwise fail if
  13. given a cyclic graph.
  14. The `nodes` and `edges` attributes may be accessed in a read-only fashion.
  15. The `nodes` attribute is a set of node names, while `edges` is a set of
  16. `(left, right, name)` tuples representing an edge named `name` going from
  17. node `left` to node `right..
  18. """
  19. def __init__(self, nodes, edges):
  20. """
  21. Create a graph. Nodes and edges are both as described in the class
  22. documentation. Both values are used by reference, and should not be
  23. modified after building a graph.
  24. """
  25. assert isinstance(nodes, set)
  26. assert isinstance(edges, set)
  27. self.nodes = nodes
  28. self.edges = edges
  29. def __eq__(self, other):
  30. return self.nodes == other.nodes and self.edges == other.edges
  31. def __repr__(self):
  32. return "<Graph nodes={!r} edges={!r}>".format(self.nodes, self.edges)
  33. def transitive_closure(self, nodes):
  34. """
  35. Return the transitive closure of <nodes>: the graph containing all
  36. specified nodes as well as any nodes reachable from them, and any
  37. intervening edges.
  38. """
  39. assert isinstance(nodes, set)
  40. assert nodes <= self.nodes
  41. # generate a new graph by expanding along edges until reaching a fixed
  42. # point
  43. new_nodes, new_edges = nodes, set()
  44. nodes, edges = set(), set()
  45. while (new_nodes, new_edges) != (nodes, edges):
  46. nodes, edges = new_nodes, new_edges
  47. add_edges = set((left, right, name)
  48. for (left, right, name) in self.edges
  49. if left in nodes)
  50. add_nodes = set(right for (_, right, _) in add_edges)
  51. new_nodes = nodes | add_nodes
  52. new_edges = edges | add_edges
  53. return Graph(new_nodes, new_edges)
  54. def visit_postorder(self):
  55. """
  56. Generate a sequence of nodes in postorder, such that every node is
  57. visited *after* any nodes it links to.
  58. Behavior is undefined (read: it will hang) if the graph contains a
  59. cycle.
  60. """
  61. queue = collections.deque(sorted(self.nodes))
  62. links_by_node = self.links_dict()
  63. seen = set()
  64. while queue:
  65. node = queue.popleft()
  66. if node in seen:
  67. continue
  68. links = links_by_node[node]
  69. if all((n in seen) for n in links):
  70. seen.add(node)
  71. yield node
  72. else:
  73. queue.extend(n for n in links if n not in seen)
  74. queue.append(node)
  75. def links_dict(self):
  76. """
  77. Return a dictionary mapping each node to a set of the nodes it links to
  78. (omitting edge names)
  79. """
  80. links = collections.defaultdict(set)
  81. for left, right, _ in self.edges:
  82. links[left].add(right)
  83. return links
  84. def named_links_dict(self):
  85. """
  86. Return a two-level dictionary mapping each node to a dictionary mapping
  87. edge names to labels.
  88. """
  89. links = collections.defaultdict(dict)
  90. for left, right, name in self.edges:
  91. links[left][name] = right
  92. return links
  93. def reverse_links_dict(self):
  94. """
  95. Return a dictionary mapping each node to a set of the nodes linking to
  96. it (omitting edge names)
  97. """
  98. links = collections.defaultdict(set)
  99. for left, right, _ in self.edges:
  100. links[right].add(left)
  101. return links