Skip to content

Commit

Permalink
Add dominance_frontiers function (#1329)
Browse files Browse the repository at this point in the history
Co-authored-by: Ivan Carvalho <[email protected]>
  • Loading branch information
airwoodix and IvanIsCoding authored Nov 23, 2024
1 parent eaee0b5 commit 37bee6f
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/source/api/algorithm_functions/dominance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Dominance
:toctree: ../../apiref

rustworkx.immediate_dominators
rustworkx.dominance_frontiers
6 changes: 6 additions & 0 deletions releasenotes/notes/dominance-frontiers-6e3dcd59e9201b24.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
Add the :func:`rustworkx.dominance_frontiers` function to compute
the dominance frontiers of all nodes in a directed graph.
This function mirrors the ``networkx.dominance_frontiers`` function.
1 change: 1 addition & 0 deletions rustworkx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ from .rustworkx import metric_closure as metric_closure
from .rustworkx import digraph_union as digraph_union
from .rustworkx import graph_union as graph_union
from .rustworkx import immediate_dominators as immediate_dominators
from .rustworkx import dominance_frontiers as dominance_frontiers
from .rustworkx import NodeIndices as NodeIndices
from .rustworkx import PathLengthMapping as PathLengthMapping
from .rustworkx import PathMapping as PathMapping
Expand Down
1 change: 1 addition & 0 deletions rustworkx/rustworkx.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,7 @@ def graph_union(
# Dominance

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

# Iterators

Expand Down
52 changes: 52 additions & 0 deletions src/dominance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use super::{digraph, InvalidNode, NullGraph};
use rustworkx_core::dictmap::DictMap;

use hashbrown::HashSet;

use petgraph::algo::dominators;
use petgraph::graph::NodeIndex;

Expand Down Expand Up @@ -58,3 +60,53 @@ pub fn immediate_dominators(
});
Ok(root_dom.into_iter().chain(others_dom).collect())
}

/// Compute the dominance frontiers of all nodes in a directed graph.
///
/// The dominance and dominance frontiers computations use the
/// algorithms published in 2006 by Cooper, Harvey, and Kennedy
/// (https://hdl.handle.net/1911/96345).
///
/// :param PyDiGraph graph: directed graph
/// :param int start_node: the start node for the dominance computation
///
/// :returns: a mapping of node indices to their dominance frontiers
/// :rtype: dict[int, set[int]]
///
/// :raises NullGraph: the passed graph is empty
/// :raises InvalidNode: the start node is not in the graph
#[pyfunction]
#[pyo3(text_signature = "(graph, start_node, /)")]
pub fn dominance_frontiers(
graph: &digraph::PyDiGraph,
start_node: usize,
) -> PyResult<DictMap<usize, HashSet<usize>>> {
let idom = immediate_dominators(graph, start_node)?;

let mut df: DictMap<_, _> = idom
.iter()
.map(|(&node, _)| (node, HashSet::default()))
.collect();

for (&node, &node_idom) in &idom {
let preds = graph.predecessor_indices(node);
if preds.nodes.len() >= 2 {
for mut runner in preds.nodes {
while runner != node_idom {
df.entry(runner)
.and_modify(|e| {
e.insert(node);
})
.or_insert([node].into_iter().collect());
if let Some(&runner_idom) = idom.get(&runner) {
runner = runner_idom;
} else {
break;
}
}
}
}
}

Ok(df)
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(digraph_union))?;
m.add_wrapped(wrap_pyfunction!(graph_union))?;
m.add_wrapped(wrap_pyfunction!(immediate_dominators))?;
m.add_wrapped(wrap_pyfunction!(dominance_frontiers))?;
m.add_wrapped(wrap_pyfunction!(digraph_maximum_bisimulation))?;
m.add_wrapped(wrap_pyfunction!(digraph_cartesian_product))?;
m.add_wrapped(wrap_pyfunction!(graph_cartesian_product))?;
Expand Down
206 changes: 206 additions & 0 deletions tests/digraph/test_dominance.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,209 @@ def test_boost_example(self):
self.assertDictEqual(result, {0: 1, 1: 7, 2: 7, 3: 4, 4: 5, 5: 7, 6: 4, 7: 7})

self.assertDictEqual(nx.immediate_dominators(nx_graph.reverse(copy=False), 7), result)


class TestDominanceFrontiers(unittest.TestCase):
"""
Test `rustworkx.dominance_frontiers`.
Test cases adapted from `networkx`:
https://github.com/networkx/networkx/blob/9c5ca54b7e5310a21568bb2e0104f8c87bf74ff7/networkx/algorithms/tests/test_dominance.py
(Copyright 2004-2024 NetworkX Developers, 3-clause BSD License)
"""

def test_empty(self):
"""
Edge case: empty graph.
"""
graph = rx.PyDiGraph()

with self.assertRaises(rx.NullGraph):
rx.dominance_frontiers(graph, 0)

def test_start_node_not_in_graph(self):
"""
Edge case: start_node is not in the graph.
"""
graph = rx.PyDiGraph()
graph.add_node(0)

self.assertEqual(list(graph.node_indices()), [0])

with self.assertRaises(rx.InvalidNode):
rx.dominance_frontiers(graph, 1)

def test_singleton(self):
"""
Edge cases: single node, optionally cyclic.
"""
graph = rx.PyDiGraph()
graph.add_node(0)
self.assertDictEqual(rx.dominance_frontiers(graph, 0), {0: set()})

graph.add_edge(0, 0, None)
self.assertDictEqual(rx.dominance_frontiers(graph, 0), {0: set()})

def test_irreducible1(self):
"""
Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006).
https://hdl.handle.net/1911/96345
"""
edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)]
graph = rx.PyDiGraph()
graph.add_node(0)
graph.extend_from_edge_list(edges)

result = rx.dominance_frontiers(graph, 5)
self.assertDictEqual(result, {1: {2}, 2: {1}, 3: {2}, 4: {1}, 5: set()})

nx_graph = nx.DiGraph()
nx_graph.add_edges_from(graph.edge_list())
self.assertDictEqual(nx.dominance_frontiers(nx_graph, 5), result)

def test_irreducible2(self):
"""
Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006).
https://hdl.handle.net/1911/96345
"""
edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)]
graph = rx.PyDiGraph()
graph.add_node(0)
graph.extend_from_edge_list(edges)

result = rx.dominance_frontiers(graph, 6)

self.assertDictEqual(
result,
{
1: {2},
2: {1, 3},
3: {2},
4: {2, 3},
5: {1},
6: set(),
},
)

nx_graph = nx.DiGraph()
nx_graph.add_edges_from(graph.edge_list())
self.assertDictEqual(nx.dominance_frontiers(nx_graph, 6), result)

def test_domrel_png(self):
"""
Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png
"""
edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)]
graph = rx.PyDiGraph()
graph.add_node(0)
graph.extend_from_edge_list(edges)

result = rx.dominance_frontiers(graph, 1)

self.assertDictEqual(
result,
{
1: set(),
2: {2},
3: {5},
4: {5},
5: {2},
6: set(),
},
)

nx_graph = nx.DiGraph()
nx_graph.add_edges_from(graph.edge_list())
self.assertDictEqual(nx.dominance_frontiers(nx_graph, 1), result)

# Test postdominance.
graph.reverse()
result = rx.dominance_frontiers(graph, 6)
self.assertDictEqual(
result,
{
1: set(),
2: {2},
3: {2},
4: {2},
5: {2},
6: set(),
},
)

self.assertDictEqual(nx.dominance_frontiers(nx_graph.reverse(copy=False), 6), result)

def test_boost_example(self):
"""
Graph taken from Figure 1 of
http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm
"""
edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)]
graph = rx.PyDiGraph()
graph.extend_from_edge_list(edges)

nx_graph = nx.DiGraph()
nx_graph.add_edges_from(graph.edge_list())

result = rx.dominance_frontiers(graph, 0)
self.assertDictEqual(
result,
{
0: set(),
1: set(),
2: {7},
3: {7},
4: {4, 7},
5: {7},
6: {4},
7: set(),
},
)

self.assertDictEqual(nx.dominance_frontiers(nx_graph, 0), result)

# Test postdominance
graph.reverse()
result = rx.dominance_frontiers(graph, 7)
self.assertDictEqual(
result,
{
0: set(),
1: set(),
2: {1},
3: {1},
4: {1, 4},
5: {1},
6: {4},
7: set(),
},
)

self.assertDictEqual(nx.dominance_frontiers(nx_graph.reverse(copy=False), 7), result)

def test_missing_immediate_doms(self):
"""
Test that the `dominance_frontiers` function doesn't regress on
https://github.com/networkx/networkx/issues/2070
"""
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (5, 3)]
graph = rx.PyDiGraph()
graph.extend_from_edge_list(edges)

idom = rx.immediate_dominators(graph, 0)
self.assertNotIn(5, idom)

# In networkx#2070, the call would fail because node 5
# has no immediate dominators
result = rx.dominance_frontiers(graph, 0)
self.assertDictEqual(
result,
{
0: set(),
1: set(),
2: set(),
3: set(),
4: set(),
5: {3},
},
)

0 comments on commit 37bee6f

Please sign in to comment.