Skip to content

Commit 37bee6f

Browse files
Add dominance_frontiers function (#1329)
Co-authored-by: Ivan Carvalho <[email protected]>
1 parent eaee0b5 commit 37bee6f

File tree

7 files changed

+268
-0
lines changed

7 files changed

+268
-0
lines changed

docs/source/api/algorithm_functions/dominance.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Dominance
77
:toctree: ../../apiref
88

99
rustworkx.immediate_dominators
10+
rustworkx.dominance_frontiers
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
Add the :func:`rustworkx.dominance_frontiers` function to compute
5+
the dominance frontiers of all nodes in a directed graph.
6+
This function mirrors the ``networkx.dominance_frontiers`` function.

rustworkx/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ from .rustworkx import metric_closure as metric_closure
243243
from .rustworkx import digraph_union as digraph_union
244244
from .rustworkx import graph_union as graph_union
245245
from .rustworkx import immediate_dominators as immediate_dominators
246+
from .rustworkx import dominance_frontiers as dominance_frontiers
246247
from .rustworkx import NodeIndices as NodeIndices
247248
from .rustworkx import PathLengthMapping as PathLengthMapping
248249
from .rustworkx import PathMapping as PathMapping

rustworkx/rustworkx.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@ def graph_union(
10551055
# Dominance
10561056

10571057
def immediate_dominators(graph: PyDiGraph[_S, _T], start_node: int, /) -> dict[int, int]: ...
1058+
def dominance_frontiers(graph: PyDiGraph[_S, _T], start_node: int, /) -> dict[int, set[int]]: ...
10581059

10591060
# Iterators
10601061

src/dominance.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use super::{digraph, InvalidNode, NullGraph};
1414
use rustworkx_core::dictmap::DictMap;
1515

16+
use hashbrown::HashSet;
17+
1618
use petgraph::algo::dominators;
1719
use petgraph::graph::NodeIndex;
1820

@@ -58,3 +60,53 @@ pub fn immediate_dominators(
5860
});
5961
Ok(root_dom.into_iter().chain(others_dom).collect())
6062
}
63+
64+
/// Compute the dominance frontiers of all nodes in a directed graph.
65+
///
66+
/// The dominance and dominance frontiers computations use the
67+
/// algorithms published in 2006 by Cooper, Harvey, and Kennedy
68+
/// (https://hdl.handle.net/1911/96345).
69+
///
70+
/// :param PyDiGraph graph: directed graph
71+
/// :param int start_node: the start node for the dominance computation
72+
///
73+
/// :returns: a mapping of node indices to their dominance frontiers
74+
/// :rtype: dict[int, set[int]]
75+
///
76+
/// :raises NullGraph: the passed graph is empty
77+
/// :raises InvalidNode: the start node is not in the graph
78+
#[pyfunction]
79+
#[pyo3(text_signature = "(graph, start_node, /)")]
80+
pub fn dominance_frontiers(
81+
graph: &digraph::PyDiGraph,
82+
start_node: usize,
83+
) -> PyResult<DictMap<usize, HashSet<usize>>> {
84+
let idom = immediate_dominators(graph, start_node)?;
85+
86+
let mut df: DictMap<_, _> = idom
87+
.iter()
88+
.map(|(&node, _)| (node, HashSet::default()))
89+
.collect();
90+
91+
for (&node, &node_idom) in &idom {
92+
let preds = graph.predecessor_indices(node);
93+
if preds.nodes.len() >= 2 {
94+
for mut runner in preds.nodes {
95+
while runner != node_idom {
96+
df.entry(runner)
97+
.and_modify(|e| {
98+
e.insert(node);
99+
})
100+
.or_insert([node].into_iter().collect());
101+
if let Some(&runner_idom) = idom.get(&runner) {
102+
runner = runner_idom;
103+
} else {
104+
break;
105+
}
106+
}
107+
}
108+
}
109+
}
110+
111+
Ok(df)
112+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
467467
m.add_wrapped(wrap_pyfunction!(digraph_union))?;
468468
m.add_wrapped(wrap_pyfunction!(graph_union))?;
469469
m.add_wrapped(wrap_pyfunction!(immediate_dominators))?;
470+
m.add_wrapped(wrap_pyfunction!(dominance_frontiers))?;
470471
m.add_wrapped(wrap_pyfunction!(digraph_maximum_bisimulation))?;
471472
m.add_wrapped(wrap_pyfunction!(digraph_cartesian_product))?;
472473
m.add_wrapped(wrap_pyfunction!(graph_cartesian_product))?;

tests/digraph/test_dominance.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,209 @@ def test_boost_example(self):
137137
self.assertDictEqual(result, {0: 1, 1: 7, 2: 7, 3: 4, 4: 5, 5: 7, 6: 4, 7: 7})
138138

139139
self.assertDictEqual(nx.immediate_dominators(nx_graph.reverse(copy=False), 7), result)
140+
141+
142+
class TestDominanceFrontiers(unittest.TestCase):
143+
"""
144+
Test `rustworkx.dominance_frontiers`.
145+
146+
Test cases adapted from `networkx`:
147+
https://github.com/networkx/networkx/blob/9c5ca54b7e5310a21568bb2e0104f8c87bf74ff7/networkx/algorithms/tests/test_dominance.py
148+
(Copyright 2004-2024 NetworkX Developers, 3-clause BSD License)
149+
"""
150+
151+
def test_empty(self):
152+
"""
153+
Edge case: empty graph.
154+
"""
155+
graph = rx.PyDiGraph()
156+
157+
with self.assertRaises(rx.NullGraph):
158+
rx.dominance_frontiers(graph, 0)
159+
160+
def test_start_node_not_in_graph(self):
161+
"""
162+
Edge case: start_node is not in the graph.
163+
"""
164+
graph = rx.PyDiGraph()
165+
graph.add_node(0)
166+
167+
self.assertEqual(list(graph.node_indices()), [0])
168+
169+
with self.assertRaises(rx.InvalidNode):
170+
rx.dominance_frontiers(graph, 1)
171+
172+
def test_singleton(self):
173+
"""
174+
Edge cases: single node, optionally cyclic.
175+
"""
176+
graph = rx.PyDiGraph()
177+
graph.add_node(0)
178+
self.assertDictEqual(rx.dominance_frontiers(graph, 0), {0: set()})
179+
180+
graph.add_edge(0, 0, None)
181+
self.assertDictEqual(rx.dominance_frontiers(graph, 0), {0: set()})
182+
183+
def test_irreducible1(self):
184+
"""
185+
Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006).
186+
https://hdl.handle.net/1911/96345
187+
"""
188+
edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)]
189+
graph = rx.PyDiGraph()
190+
graph.add_node(0)
191+
graph.extend_from_edge_list(edges)
192+
193+
result = rx.dominance_frontiers(graph, 5)
194+
self.assertDictEqual(result, {1: {2}, 2: {1}, 3: {2}, 4: {1}, 5: set()})
195+
196+
nx_graph = nx.DiGraph()
197+
nx_graph.add_edges_from(graph.edge_list())
198+
self.assertDictEqual(nx.dominance_frontiers(nx_graph, 5), result)
199+
200+
def test_irreducible2(self):
201+
"""
202+
Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006).
203+
https://hdl.handle.net/1911/96345
204+
"""
205+
edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)]
206+
graph = rx.PyDiGraph()
207+
graph.add_node(0)
208+
graph.extend_from_edge_list(edges)
209+
210+
result = rx.dominance_frontiers(graph, 6)
211+
212+
self.assertDictEqual(
213+
result,
214+
{
215+
1: {2},
216+
2: {1, 3},
217+
3: {2},
218+
4: {2, 3},
219+
5: {1},
220+
6: set(),
221+
},
222+
)
223+
224+
nx_graph = nx.DiGraph()
225+
nx_graph.add_edges_from(graph.edge_list())
226+
self.assertDictEqual(nx.dominance_frontiers(nx_graph, 6), result)
227+
228+
def test_domrel_png(self):
229+
"""
230+
Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png
231+
"""
232+
edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)]
233+
graph = rx.PyDiGraph()
234+
graph.add_node(0)
235+
graph.extend_from_edge_list(edges)
236+
237+
result = rx.dominance_frontiers(graph, 1)
238+
239+
self.assertDictEqual(
240+
result,
241+
{
242+
1: set(),
243+
2: {2},
244+
3: {5},
245+
4: {5},
246+
5: {2},
247+
6: set(),
248+
},
249+
)
250+
251+
nx_graph = nx.DiGraph()
252+
nx_graph.add_edges_from(graph.edge_list())
253+
self.assertDictEqual(nx.dominance_frontiers(nx_graph, 1), result)
254+
255+
# Test postdominance.
256+
graph.reverse()
257+
result = rx.dominance_frontiers(graph, 6)
258+
self.assertDictEqual(
259+
result,
260+
{
261+
1: set(),
262+
2: {2},
263+
3: {2},
264+
4: {2},
265+
5: {2},
266+
6: set(),
267+
},
268+
)
269+
270+
self.assertDictEqual(nx.dominance_frontiers(nx_graph.reverse(copy=False), 6), result)
271+
272+
def test_boost_example(self):
273+
"""
274+
Graph taken from Figure 1 of
275+
http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm
276+
"""
277+
edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)]
278+
graph = rx.PyDiGraph()
279+
graph.extend_from_edge_list(edges)
280+
281+
nx_graph = nx.DiGraph()
282+
nx_graph.add_edges_from(graph.edge_list())
283+
284+
result = rx.dominance_frontiers(graph, 0)
285+
self.assertDictEqual(
286+
result,
287+
{
288+
0: set(),
289+
1: set(),
290+
2: {7},
291+
3: {7},
292+
4: {4, 7},
293+
5: {7},
294+
6: {4},
295+
7: set(),
296+
},
297+
)
298+
299+
self.assertDictEqual(nx.dominance_frontiers(nx_graph, 0), result)
300+
301+
# Test postdominance
302+
graph.reverse()
303+
result = rx.dominance_frontiers(graph, 7)
304+
self.assertDictEqual(
305+
result,
306+
{
307+
0: set(),
308+
1: set(),
309+
2: {1},
310+
3: {1},
311+
4: {1, 4},
312+
5: {1},
313+
6: {4},
314+
7: set(),
315+
},
316+
)
317+
318+
self.assertDictEqual(nx.dominance_frontiers(nx_graph.reverse(copy=False), 7), result)
319+
320+
def test_missing_immediate_doms(self):
321+
"""
322+
Test that the `dominance_frontiers` function doesn't regress on
323+
https://github.com/networkx/networkx/issues/2070
324+
"""
325+
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (5, 3)]
326+
graph = rx.PyDiGraph()
327+
graph.extend_from_edge_list(edges)
328+
329+
idom = rx.immediate_dominators(graph, 0)
330+
self.assertNotIn(5, idom)
331+
332+
# In networkx#2070, the call would fail because node 5
333+
# has no immediate dominators
334+
result = rx.dominance_frontiers(graph, 0)
335+
self.assertDictEqual(
336+
result,
337+
{
338+
0: set(),
339+
1: set(),
340+
2: set(),
341+
3: set(),
342+
4: set(),
343+
5: {3},
344+
},
345+
)

0 commit comments

Comments
 (0)