Skip to content

Commit 537f67f

Browse files
authored
Add immediate_dominators function (#1323)
1 parent 0017d00 commit 537f67f

File tree

8 files changed

+223
-0
lines changed

8 files changed

+223
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. _dominance:
2+
3+
Dominance
4+
=========
5+
6+
.. autosummary::
7+
:toctree: ../../apiref
8+
9+
rustworkx.immediate_dominators

docs/source/api/algorithm_functions/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Algorithm Functions
1010
coloring
1111
connectivity_and_cycles
1212
dag_algorithms
13+
dominance
1314
graph_operations
1415
isomorphism
1516
link_analysis
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
Add :func:`rustworkx.immediate_dominators` function for computing
5+
immediate dominators of all nodes in a directed graph.
6+
This function mirrors the ``networkx.immediate_dominators`` function.

rustworkx/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ from .rustworkx import steiner_tree as steiner_tree
242242
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
245+
from .rustworkx import immediate_dominators as immediate_dominators
245246
from .rustworkx import NodeIndices as NodeIndices
246247
from .rustworkx import PathLengthMapping as PathLengthMapping
247248
from .rustworkx import PathMapping as PathMapping

rustworkx/rustworkx.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,10 @@ def graph_union(
10521052
merge_edges: bool = ...,
10531053
) -> PyGraph[_S, _T]: ...
10541054

1055+
# Dominance
1056+
1057+
def immediate_dominators(graph: PyDiGraph[_S, _T], start_node: int, /) -> dict[int, int]: ...
1058+
10551059
# Iterators
10561060

10571061
_T_co = TypeVar("_T_co", covariant=True)

src/dominance.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
// not use this file except in compliance with the License. You may obtain
3+
// a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations
11+
// under the License.
12+
13+
use super::{digraph, InvalidNode, NullGraph};
14+
use rustworkx_core::dictmap::DictMap;
15+
16+
use petgraph::algo::dominators;
17+
use petgraph::graph::NodeIndex;
18+
19+
use pyo3::prelude::*;
20+
21+
/// Determine the immediate dominators of all nodes in a directed graph.
22+
///
23+
/// The dominance computation uses the algorithm published in 2006 by
24+
/// Cooper, Harvey, and Kennedy (https://hdl.handle.net/1911/96345).
25+
/// The time complexity is quadratic in the number of vertices.
26+
///
27+
/// :param PyDiGraph graph: directed graph
28+
/// :param int start_node: the start node for the dominance computation
29+
///
30+
/// :returns: a mapping of node indices to their immediate dominators
31+
/// :rtype: dict[int, int]
32+
///
33+
/// :raises NullGraph: the passed graph is empty
34+
/// :raises InvalidNode: the start node is not in the graph
35+
#[pyfunction]
36+
#[pyo3(text_signature = "(graph, start_node, /)")]
37+
pub fn immediate_dominators(
38+
graph: &digraph::PyDiGraph,
39+
start_node: usize,
40+
) -> PyResult<DictMap<usize, usize>> {
41+
if graph.graph.node_count() == 0 {
42+
return Err(NullGraph::new_err("Invalid operation on a NullGraph"));
43+
}
44+
45+
let start_node_index = NodeIndex::new(start_node);
46+
47+
if !graph.graph.contains_node(start_node_index) {
48+
return Err(InvalidNode::new_err("Start node is not in the graph"));
49+
}
50+
51+
let dom = dominators::simple_fast(&graph.graph, start_node_index);
52+
53+
// Include the root node to match networkx.immediate_dominators
54+
let root_dom = [(start_node, start_node)];
55+
let others_dom = graph.graph.node_indices().filter_map(|index| {
56+
dom.immediate_dominator(index)
57+
.map(|res| (index.index(), res.index()))
58+
});
59+
Ok(root_dom.into_iter().chain(others_dom).collect())
60+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod coloring;
1717
mod connectivity;
1818
mod dag_algo;
1919
mod digraph;
20+
mod dominance;
2021
mod dot_utils;
2122
mod generators;
2223
mod graph;
@@ -47,6 +48,7 @@ use centrality::*;
4748
use coloring::*;
4849
use connectivity::*;
4950
use dag_algo::*;
51+
use dominance::*;
5052
use graphml::*;
5153
use isomorphism::*;
5254
use json::*;
@@ -464,6 +466,7 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
464466
m.add_wrapped(wrap_pyfunction!(graph_vf2_mapping))?;
465467
m.add_wrapped(wrap_pyfunction!(digraph_union))?;
466468
m.add_wrapped(wrap_pyfunction!(graph_union))?;
469+
m.add_wrapped(wrap_pyfunction!(immediate_dominators))?;
467470
m.add_wrapped(wrap_pyfunction!(digraph_maximum_bisimulation))?;
468471
m.add_wrapped(wrap_pyfunction!(digraph_cartesian_product))?;
469472
m.add_wrapped(wrap_pyfunction!(graph_cartesian_product))?;

tests/digraph/test_dominance.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import unittest
14+
15+
import rustworkx as rx
16+
import networkx as nx
17+
18+
19+
class TestImmediateDominators(unittest.TestCase):
20+
"""Test `rustworkx.immediate_dominators`.
21+
22+
Test cases adapted from `networkx`:
23+
https://github.com/networkx/networkx/blob/9c5ca54b7e5310a21568bb2e0104f8c87bf74ff7/networkx/algorithms/tests/test_dominance.py
24+
(Copyright 2004-2024 NetworkX Developers, 3-clause BSD License)
25+
"""
26+
27+
def test_empty(self):
28+
"""
29+
Edge case: empty graph.
30+
"""
31+
graph = rx.PyDiGraph()
32+
33+
with self.assertRaises(rx.NullGraph):
34+
rx.immediate_dominators(graph, 0)
35+
36+
def test_start_node_not_in_graph(self):
37+
"""
38+
Edge case: start_node is not in the graph.
39+
"""
40+
graph = rx.PyDiGraph()
41+
graph.add_node(0)
42+
43+
self.assertEqual(list(graph.node_indices()), [0])
44+
45+
with self.assertRaises(rx.InvalidNode):
46+
rx.immediate_dominators(graph, 1)
47+
48+
def test_singleton(self):
49+
"""
50+
Edge cases: single node, optionally cyclic.
51+
"""
52+
graph = rx.PyDiGraph()
53+
graph.add_node(0)
54+
self.assertDictEqual(rx.immediate_dominators(graph, 0), {0: 0})
55+
graph.add_edge(0, 0, None)
56+
self.assertDictEqual(rx.immediate_dominators(graph, 0), {0: 0})
57+
58+
nx_graph = nx.DiGraph()
59+
nx_graph.add_edges_from(graph.edge_list())
60+
self.assertDictEqual(nx.immediate_dominators(nx_graph, 0), {0: 0})
61+
62+
def test_irreducible1(self):
63+
"""
64+
Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006).
65+
https://hdl.handle.net/1911/96345
66+
"""
67+
edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)]
68+
graph = rx.PyDiGraph()
69+
graph.add_node(0)
70+
graph.extend_from_edge_list(edges)
71+
72+
result = rx.immediate_dominators(graph, 5)
73+
self.assertDictEqual(result, {i: 5 for i in range(1, 6)})
74+
75+
nx_graph = nx.DiGraph()
76+
nx_graph.add_edges_from(graph.edge_list())
77+
self.assertDictEqual(nx.immediate_dominators(nx_graph, 5), result)
78+
79+
def test_irreducible2(self):
80+
"""
81+
Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006).
82+
https://hdl.handle.net/1911/96345
83+
"""
84+
edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)]
85+
graph = rx.PyDiGraph()
86+
graph.add_node(0)
87+
graph.extend_from_edge_list(edges)
88+
89+
result = rx.immediate_dominators(graph, 6)
90+
self.assertDictEqual(result, {i: 6 for i in range(1, 7)})
91+
92+
nx_graph = nx.DiGraph()
93+
nx_graph.add_edges_from(graph.edge_list())
94+
self.assertDictEqual(nx.immediate_dominators(nx_graph, 6), result)
95+
96+
def test_domrel_png(self):
97+
"""
98+
Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png
99+
"""
100+
edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)]
101+
graph = rx.PyDiGraph()
102+
graph.add_node(0)
103+
graph.extend_from_edge_list(edges)
104+
105+
result = rx.immediate_dominators(graph, 1)
106+
self.assertDictEqual(result, {1: 1, 2: 1, 3: 2, 4: 2, 5: 2, 6: 2})
107+
108+
nx_graph = nx.DiGraph()
109+
nx_graph.add_edges_from(graph.edge_list())
110+
self.assertDictEqual(nx.immediate_dominators(nx_graph, 1), result)
111+
112+
# Test postdominance.
113+
graph.reverse()
114+
result = rx.immediate_dominators(graph, 6)
115+
self.assertDictEqual(result, {1: 2, 2: 6, 3: 5, 4: 5, 5: 2, 6: 6})
116+
117+
self.assertDictEqual(nx.immediate_dominators(nx_graph.reverse(copy=False), 6), result)
118+
119+
def test_boost_example(self):
120+
"""
121+
Graph taken from Figure 1 of
122+
http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm
123+
"""
124+
edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)]
125+
graph = rx.PyDiGraph()
126+
graph.extend_from_edge_list(edges)
127+
result = rx.immediate_dominators(graph, 0)
128+
self.assertDictEqual(result, {0: 0, 1: 0, 2: 1, 3: 1, 4: 3, 5: 4, 6: 4, 7: 1})
129+
130+
nx_graph = nx.DiGraph()
131+
nx_graph.add_edges_from(graph.edge_list())
132+
self.assertDictEqual(nx.immediate_dominators(nx_graph, 0), result)
133+
134+
# Test postdominance.
135+
graph.reverse()
136+
result = rx.immediate_dominators(graph, 7)
137+
self.assertDictEqual(result, {0: 1, 1: 7, 2: 7, 3: 4, 4: 5, 5: 7, 6: 4, 7: 7})
138+
139+
self.assertDictEqual(nx.immediate_dominators(nx_graph.reverse(copy=False), 7), result)

0 commit comments

Comments
 (0)