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/.
13 Generic representation of a directed acyclic graph with labeled edges
14 connecting the nodes. Graph operations are implemented in a functional
15 manner, so the data structure is immutable.
17 It permits at most one edge of a given name between any set of nodes. The
18 graph is not checked for cycles, and methods may hang or otherwise fail if
21 The `nodes` and `edges` attributes may be accessed in a read-only fashion.
22 The `nodes` attribute is a set of node names, while `edges` is a set of
23 `(left, right, name)` tuples representing an edge named `name` going from
24 node `left` to node `right..
27 nodes
= attr
.ib(converter
=frozenset)
28 edges
= attr
.ib(converter
=frozenset)
30 def transitive_closure(self
, nodes
, reverse
=False):
32 Return the transitive closure of <nodes>: the graph containing all
33 specified nodes as well as any nodes reachable from them, and any
36 If `reverse` is true, the "reachability" will be reversed and this
37 will return the set of nodes that can reach the specified nodes.
46 transitive_closure([b]).nodes == set([a, b])
47 transitive_closure([c]).nodes == set([c, b, a])
48 transitive_closure([c], reverse=True).nodes == set([c])
49 transitive_closure([b], reverse=True).nodes == set([b, c, d])
51 assert isinstance(nodes
, set)
52 if not (nodes
<= self
.nodes
):
54 f
"Unknown nodes in transitive closure: {nodes - self.nodes}"
57 # generate a new graph by expanding along edges until reaching a fixed
59 new_nodes
, new_edges
= nodes
, set()
60 nodes
, edges
= set(), set()
61 while (new_nodes
, new_edges
) != (nodes
, edges
):
62 nodes
, edges
= new_nodes
, new_edges
65 for (left
, right
, name
) in self
.edges
66 if (right
if reverse
else left
) in nodes
68 add_nodes
= {(left
if reverse
else right
) for (left
, right
, _
) in add_edges
}
69 new_nodes
= nodes | add_nodes
70 new_edges
= edges | add_edges
71 return Graph(new_nodes
, new_edges
)
73 def _visit(self
, reverse
):
74 queue
= collections
.deque(sorted(self
.nodes
))
75 links_by_node
= self
.reverse_links_dict() if reverse
else self
.links_dict()
78 node
= queue
.popleft()
81 links
= links_by_node
[node
]
82 if all((n
in seen
) for n
in links
):
86 queue
.extend(n
for n
in links
if n
not in seen
)
89 def visit_postorder(self
):
91 Generate a sequence of nodes in postorder, such that every node is
92 visited *after* any nodes it links to.
94 Behavior is undefined (read: it will hang) if the graph contains a
97 return self
._visit
(False)
99 def visit_preorder(self
):
101 Like visit_postorder, but in reverse: evrey node is visited *before*
102 any nodes it links to.
104 return self
._visit
(True)
106 def links_dict(self
):
108 Return a dictionary mapping each node to a set of the nodes it links to
109 (omitting edge names)
111 links
= collections
.defaultdict(set)
112 for left
, right
, _
in self
.edges
:
113 links
[left
].add(right
)
116 def named_links_dict(self
):
118 Return a two-level dictionary mapping each node to a dictionary mapping
119 edge names to labels.
121 links
= collections
.defaultdict(dict)
122 for left
, right
, name
in self
.edges
:
123 links
[left
][name
] = right
126 def reverse_links_dict(self
):
128 Return a dictionary mapping each node to a set of the nodes linking to
129 it (omitting edge names)
131 links
= collections
.defaultdict(set)
132 for left
, right
, _
in self
.edges
:
133 links
[right
].add(left
)