From e6e89f8b1d2fe2a2d269730e4a23d6564041f183 Mon Sep 17 00:00:00 2001 From: Etienne Wodey Date: Mon, 18 Nov 2024 22:21:05 +0100 Subject: [PATCH] Add immediate_dominators function --- .../api/algorithm_functions/dominance.rst | 9 ++ docs/source/api/algorithm_functions/index.rst | 1 + ...immediate-dominators-0a713b22657cd19a.yaml | 6 + rustworkx/__init__.pyi | 1 + rustworkx/rustworkx.pyi | 4 + src/dominance.rs | 60 ++++++++ src/lib.rs | 3 + tests/digraph/test_dominance.py | 139 ++++++++++++++++++ 8 files changed, 223 insertions(+) create mode 100644 docs/source/api/algorithm_functions/dominance.rst create mode 100644 releasenotes/notes/digraph-immediate-dominators-0a713b22657cd19a.yaml create mode 100644 src/dominance.rs create mode 100644 tests/digraph/test_dominance.py diff --git a/docs/source/api/algorithm_functions/dominance.rst b/docs/source/api/algorithm_functions/dominance.rst new file mode 100644 index 0000000000..d711e89a71 --- /dev/null +++ b/docs/source/api/algorithm_functions/dominance.rst @@ -0,0 +1,9 @@ +.. _dominance: + +Dominance +========= + +.. autosummary:: + :toctree: ../../apiref + + rustworkx.immediate_dominators diff --git a/docs/source/api/algorithm_functions/index.rst b/docs/source/api/algorithm_functions/index.rst index 1241cae34a..0bbd84b287 100644 --- a/docs/source/api/algorithm_functions/index.rst +++ b/docs/source/api/algorithm_functions/index.rst @@ -10,6 +10,7 @@ Algorithm Functions coloring connectivity_and_cycles dag_algorithms + dominance graph_operations isomorphism link_analysis diff --git a/releasenotes/notes/digraph-immediate-dominators-0a713b22657cd19a.yaml b/releasenotes/notes/digraph-immediate-dominators-0a713b22657cd19a.yaml new file mode 100644 index 0000000000..dc89cc6cca --- /dev/null +++ b/releasenotes/notes/digraph-immediate-dominators-0a713b22657cd19a.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add :func:`rustworkx.immediate_dominators` function for computing + immediate dominators of all nodes in a directed graph. + This function mirrors the ``networkx.immediate_dominators`` function. diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index 2152b4f60b..7a7af8be41 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -242,6 +242,7 @@ from .rustworkx import steiner_tree as steiner_tree 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 NodeIndices as NodeIndices from .rustworkx import PathLengthMapping as PathLengthMapping from .rustworkx import PathMapping as PathMapping diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi index beebd50419..1655a759e4 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -1052,6 +1052,10 @@ def graph_union( merge_edges: bool = ..., ) -> PyGraph[_S, _T]: ... +# Dominance + +def immediate_dominators(graph: PyDiGraph[_S, _T], start_node: int, /) -> dict[int, int]: ... + # Iterators _T_co = TypeVar("_T_co", covariant=True) diff --git a/src/dominance.rs b/src/dominance.rs new file mode 100644 index 0000000000..2dd31e0f17 --- /dev/null +++ b/src/dominance.rs @@ -0,0 +1,60 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +use super::{digraph, InvalidNode, NullGraph}; +use rustworkx_core::dictmap::DictMap; + +use petgraph::algo::dominators; +use petgraph::graph::NodeIndex; + +use pyo3::prelude::*; + +/// Determine the immediate dominators of all nodes in a directed graph. +/// +/// The dominance computation uses the algorithm published in 2006 by +/// Cooper, Harvey, and Kennedy (https://hdl.handle.net/1911/96345). +/// The time complexity is quadratic in the number of vertices. +/// +/// :param PyDiGraph graph: directed graph +/// :param int start_node: the start node for the dominance computation +/// +/// :returns: a mapping of node indices to their immediate dominators +/// :rtype: dict[int, 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 immediate_dominators( + graph: &digraph::PyDiGraph, + start_node: usize, +) -> PyResult> { + if graph.graph.node_count() == 0 { + return Err(NullGraph::new_err("Invalid operation on a NullGraph")); + } + + let start_node_index = NodeIndex::new(start_node); + + if !graph.graph.contains_node(start_node_index) { + return Err(InvalidNode::new_err("Start node is not in the graph")); + } + + let dom = dominators::simple_fast(&graph.graph, start_node_index); + + // Include the root node to match networkx.immediate_dominators + let root_dom = [(start_node, start_node)]; + let others_dom = graph.graph.node_indices().filter_map(|index| { + dom.immediate_dominator(index) + .map(|res| (index.index(), res.index())) + }); + Ok(root_dom.into_iter().chain(others_dom).collect()) +} diff --git a/src/lib.rs b/src/lib.rs index 4d83e41587..45a9629d6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ mod coloring; mod connectivity; mod dag_algo; mod digraph; +mod dominance; mod dot_utils; mod generators; mod graph; @@ -47,6 +48,7 @@ use centrality::*; use coloring::*; use connectivity::*; use dag_algo::*; +use dominance::*; use graphml::*; use isomorphism::*; use json::*; @@ -464,6 +466,7 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(graph_vf2_mapping))?; 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!(digraph_maximum_bisimulation))?; m.add_wrapped(wrap_pyfunction!(digraph_cartesian_product))?; m.add_wrapped(wrap_pyfunction!(graph_cartesian_product))?; diff --git a/tests/digraph/test_dominance.py b/tests/digraph/test_dominance.py new file mode 100644 index 0000000000..ed37a54962 --- /dev/null +++ b/tests/digraph/test_dominance.py @@ -0,0 +1,139 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import rustworkx as rx +import networkx as nx + + +class TestImmediateDominators(unittest.TestCase): + """Test `rustworkx.immediate_dominators`. + + 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.immediate_dominators(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.immediate_dominators(graph, 1) + + def test_singleton(self): + """ + Edge cases: single node, optionally cyclic. + """ + graph = rx.PyDiGraph() + graph.add_node(0) + self.assertDictEqual(rx.immediate_dominators(graph, 0), {0: 0}) + graph.add_edge(0, 0, None) + self.assertDictEqual(rx.immediate_dominators(graph, 0), {0: 0}) + + nx_graph = nx.DiGraph() + nx_graph.add_edges_from(graph.edge_list()) + self.assertDictEqual(nx.immediate_dominators(nx_graph, 0), {0: 0}) + + 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.immediate_dominators(graph, 5) + self.assertDictEqual(result, {i: 5 for i in range(1, 6)}) + + nx_graph = nx.DiGraph() + nx_graph.add_edges_from(graph.edge_list()) + self.assertDictEqual(nx.immediate_dominators(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.immediate_dominators(graph, 6) + self.assertDictEqual(result, {i: 6 for i in range(1, 7)}) + + nx_graph = nx.DiGraph() + nx_graph.add_edges_from(graph.edge_list()) + self.assertDictEqual(nx.immediate_dominators(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.immediate_dominators(graph, 1) + self.assertDictEqual(result, {1: 1, 2: 1, 3: 2, 4: 2, 5: 2, 6: 2}) + + nx_graph = nx.DiGraph() + nx_graph.add_edges_from(graph.edge_list()) + self.assertDictEqual(nx.immediate_dominators(nx_graph, 1), result) + + # Test postdominance. + graph.reverse() + result = rx.immediate_dominators(graph, 6) + self.assertDictEqual(result, {1: 2, 2: 6, 3: 5, 4: 5, 5: 2, 6: 6}) + + self.assertDictEqual(nx.immediate_dominators(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) + result = rx.immediate_dominators(graph, 0) + self.assertDictEqual(result, {0: 0, 1: 0, 2: 1, 3: 1, 4: 3, 5: 4, 6: 4, 7: 1}) + + nx_graph = nx.DiGraph() + nx_graph.add_edges_from(graph.edge_list()) + self.assertDictEqual(nx.immediate_dominators(nx_graph, 0), result) + + # Test postdominance. + graph.reverse() + result = rx.immediate_dominators(graph, 7) + 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)