Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add immediate_dominators function #1323

Merged
merged 1 commit into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)