Skip to content

Commit

Permalink
Add immediate_dominators function (#1323)
Browse files Browse the repository at this point in the history
  • Loading branch information
airwoodix authored Nov 20, 2024
1 parent 0017d00 commit 537f67f
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 0 deletions.
9 changes: 9 additions & 0 deletions docs/source/api/algorithm_functions/dominance.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. _dominance:

Dominance
=========

.. autosummary::
:toctree: ../../apiref

rustworkx.immediate_dominators
1 change: 1 addition & 0 deletions docs/source/api/algorithm_functions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Algorithm Functions
coloring
connectivity_and_cycles
dag_algorithms
dominance
graph_operations
isomorphism
link_analysis
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions rustworkx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions rustworkx/rustworkx.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions src/dominance.rs
Original file line number Diff line number Diff line change
@@ -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<DictMap<usize, usize>> {
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())
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod coloring;
mod connectivity;
mod dag_algo;
mod digraph;
mod dominance;
mod dot_utils;
mod generators;
mod graph;
Expand Down Expand Up @@ -47,6 +48,7 @@ use centrality::*;
use coloring::*;
use connectivity::*;
use dag_algo::*;
use dominance::*;
use graphml::*;
use isomorphism::*;
use json::*;
Expand Down Expand Up @@ -464,6 +466,7 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> 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))?;
Expand Down
139 changes: 139 additions & 0 deletions tests/digraph/test_dominance.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 537f67f

Please sign in to comment.