From 497a679eadea371298d88b39c70fd5c7ffc26d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sat, 22 Mar 2025 06:29:16 +0100 Subject: [PATCH 1/4] Add is_strongly_connected method --- .../connectivity_and_cycles.rst | 1 + docs/source/sources.txt | 1 + rustworkx/__init__.pyi | 1 + rustworkx/rustworkx.pyi | 1 + src/connectivity/mod.rs | 39 ++++++++++++++++-- src/lib.rs | 1 + tests/digraph/test_strongly_connected.py | 40 +++++++++++++++++++ 7 files changed, 81 insertions(+), 3 deletions(-) diff --git a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst index ad8a12d646..80b17c0b1b 100644 --- a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst +++ b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst @@ -11,6 +11,7 @@ Connectivity and Cycles rustworkx.node_connected_component rustworkx.is_connected rustworkx.strongly_connected_components + rustworkx.is_strongly_connected rustworkx.number_weakly_connected_components rustworkx.weakly_connected_components rustworkx.is_weakly_connected diff --git a/docs/source/sources.txt b/docs/source/sources.txt index eb6a5f0743..1607ca48cb 100644 --- a/docs/source/sources.txt +++ b/docs/source/sources.txt @@ -143,6 +143,7 @@ rustworkx.is_isomorphic.html rustworkx.is_isomorphic_node_match.html rustworkx.is_matching.html rustworkx.is_maximal_matching.html +rustworkx.is_strongly_connected.html rustworkx.is_subgraph_isomorphic.html rustworkx.is_weakly_connected.html rustworkx.k_shortest_path_lengths.html diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index a61bc8374b..e08ffde134 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -78,6 +78,7 @@ from .rustworkx import graph_misra_gries_edge_color as graph_misra_gries_edge_co from .rustworkx import graph_bipartite_edge_color as graph_bipartite_edge_color from .rustworkx import connected_components as connected_components from .rustworkx import is_connected as is_connected +from .rustworkx import is_strongly_connected as is_strongly_connected from .rustworkx import is_weakly_connected as is_weakly_connected from .rustworkx import is_semi_connected as is_semi_connected from .rustworkx import number_connected_components as number_connected_components diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi index 39ab64a79e..5e1f2286e2 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -207,6 +207,7 @@ def graph_bipartite_edge_color(graph: PyGraph, /) -> dict[int, int]: ... def connected_components(graph: PyGraph, /) -> list[set[int]]: ... def is_connected(graph: PyGraph, /) -> bool: ... +def is_strongly_connected(graph: PyDiGraph, /) -> bool: ... def is_weakly_connected(graph: PyDiGraph, /) -> bool: ... def is_semi_connected(graph: PyDiGraph, /) -> bool: ... def number_connected_components(graph: PyGraph, /) -> int: ... diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 76ac6096d8..f1e2c1bfdf 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -131,6 +131,38 @@ pub fn strongly_connected_components(graph: &digraph::PyDiGraph) -> Vec>> G = rx.PyDiGraph() +/// >>> G.extend_from_edge_list([(0, 1), (1, 2), (3, 4)]) +/// >>> rx.is_strongly_connected(G) +/// False +/// +/// See also [is_weakly_connected] and [is_semi_connected]. +/// +/// If ``rx.is_strongly_connected(G) is True`` then `rx.number_strongly_connected_components(G) == 1``. +/// +/// For undirected graphs see [is_connected]. +/// +/// :param PyGraph graph: An undirected graph to check for strong connectivity +/// +/// :returns: Whether the graph is strongly connected or not +/// :rtype: bool +/// +/// :raises NullGraph: If an empty graph is passed in +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn is_strongly_connected(graph: &digraph::PyDiGraph) -> PyResult { + if graph.graph.node_count() == 0 { + return Err(NullGraph::new_err("Invalid operation on a NullGraph")); + } + Ok(algo::kosaraju_scc(&graph.graph).len() == 1) +} + /// Return the first cycle encountered during DFS of a given PyDiGraph, /// empty list is returned if no cycle is found /// @@ -250,7 +282,8 @@ pub fn node_connected_component(graph: &graph::PyGraph, node: usize) -> PyResult /// /// If ``rx.is_connected(G) is True`` then `rx.number_connected_components(G) == 1``. /// -/// For directed graphs see [is_weakly_connected]. +/// For directed graphs see [is_weakly_connected], [is_semi_connected], +/// and [is_strongly_connected]. /// /// :param PyGraph graph: An undirected graph to check for connectivity /// @@ -364,7 +397,7 @@ pub fn weakly_connected_components(graph: &digraph::PyDiGraph) -> Vec PyResult { /// >>> rx.is_semi_connected(G) /// False /// -/// See also [is_weakly_connected]. +/// See also [is_weakly_connected] and [is_strongly_connected]. /// /// For undirected graphs see [is_connected]. /// diff --git a/src/lib.rs b/src/lib.rs index f0c549e272..637db66a8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -572,6 +572,7 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(cycle_basis))?; m.add_wrapped(wrap_pyfunction!(simple_cycles))?; m.add_wrapped(wrap_pyfunction!(strongly_connected_components))?; + m.add_wrapped(wrap_pyfunction!(is_strongly_connected))?; m.add_wrapped(wrap_pyfunction!(digraph_dfs_edges))?; m.add_wrapped(wrap_pyfunction!(graph_dfs_edges))?; m.add_wrapped(wrap_pyfunction!(digraph_find_cycle))?; diff --git a/tests/digraph/test_strongly_connected.py b/tests/digraph/test_strongly_connected.py index dbe2e73dcd..56b3bbd56a 100644 --- a/tests/digraph/test_strongly_connected.py +++ b/tests/digraph/test_strongly_connected.py @@ -65,3 +65,43 @@ def test_number_strongly_connected_big(self): node = G.add_node(i) G.add_child(node, str(i), {}) self.assertEqual(len(rustworkx.strongly_connected_components(G)), 200000) + + def test_is_strongly_connected_false(self): + graph = rustworkx.PyDiGraph() + graph.extend_from_edge_list( + [ + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (2, 4), + (4, 5), + (5, 6), + (6, 7), + (7, 4), + ] + ) + self.assertFalse(rustworkx.is_strongly_connected(graph)) + + def test_is_strongly_connected_true(self): + graph = rustworkx.PyDiGraph() + graph.extend_from_edge_list( + [ + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (2, 4), + (4, 2), # <- missing in the test_is_strongly_connected_false + (4, 5), + (5, 6), + (6, 7), + (7, 4), + ] + ) + self.assertTrue(rustworkx.is_strongly_connected(graph)) + + def test_is_strongly_connected_null_graph(self): + graph = rustworkx.PyDiGraph() + with self.assertRaises(rustworkx.NullGraph): + rustworkx.is_strongly_connected(graph) From 689b51310060a35c4657ffaa1c7e60869e69fb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sat, 22 Mar 2025 07:03:31 +0100 Subject: [PATCH 2/4] Add number_strongly_connected_components method --- .../connectivity_and_cycles.rst | 1 + docs/source/sources.txt | 1 + rustworkx/__init__.pyi | 1 + rustworkx/rustworkx.pyi | 1 + src/connectivity/mod.rs | 29 +++++++++++++++++++ src/lib.rs | 1 + tests/digraph/test_strongly_connected.py | 15 ++++------ 7 files changed, 39 insertions(+), 10 deletions(-) diff --git a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst index 80b17c0b1b..d1ecb165e1 100644 --- a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst +++ b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst @@ -10,6 +10,7 @@ Connectivity and Cycles rustworkx.connected_components rustworkx.node_connected_component rustworkx.is_connected + rustworkx.number_strongly_connected_components rustworkx.strongly_connected_components rustworkx.is_strongly_connected rustworkx.number_weakly_connected_components diff --git a/docs/source/sources.txt b/docs/source/sources.txt index 1607ca48cb..65b8449472 100644 --- a/docs/source/sources.txt +++ b/docs/source/sources.txt @@ -165,6 +165,7 @@ rustworkx.NoPathFound.html rustworkx.NoSuitableNeighbors.html rustworkx.NullGraph.html rustworkx.number_connected_components.html +rustworkx.number_strongly_connected_components.html rustworkx.number_weakly_connected_components.html rustworkx.num_shortest_paths_unweighted.html rustworkx.PathLengthMapping.html diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index e08ffde134..fc342c643c 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -82,6 +82,7 @@ from .rustworkx import is_strongly_connected as is_strongly_connected from .rustworkx import is_weakly_connected as is_weakly_connected from .rustworkx import is_semi_connected as is_semi_connected from .rustworkx import number_connected_components as number_connected_components +from .rustworkx import number_strongly_connected_components as number_strongly_connected_components from .rustworkx import number_weakly_connected_components as number_weakly_connected_components from .rustworkx import node_connected_component as node_connected_component from .rustworkx import strongly_connected_components as strongly_connected_components diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi index 5e1f2286e2..d2a0156c20 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -211,6 +211,7 @@ def is_strongly_connected(graph: PyDiGraph, /) -> bool: ... def is_weakly_connected(graph: PyDiGraph, /) -> bool: ... def is_semi_connected(graph: PyDiGraph, /) -> bool: ... def number_connected_components(graph: PyGraph, /) -> int: ... +def number_strongly_connected_components(graph: PyDiGraph, /) -> bool: ... def number_weakly_connected_components(graph: PyDiGraph, /) -> bool: ... def node_connected_component(graph: PyGraph, node: int, /) -> set[int]: ... def strongly_connected_components(graph: PyDiGraph, /) -> list[list[int]]: ... diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index f1e2c1bfdf..822304ad6e 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -100,6 +100,35 @@ pub fn simple_cycles( johnson_simple_cycles::PySimpleCycleIter::new(py, graph) } +/// Find the number of strongly connected components in a directed graph +/// +/// A strongly connected component (SCC) is a maximal subset of vertices +/// such that every vertex is reachable from every other vertex +/// within that subset. +/// +/// >>> G = rx.PyDiGraph() +/// >>> G.extend_from_edge_list([(0, 1), (1, 2), (3, 4)]) +/// >>> rx.number_strongly_connected_components(G) +/// 2 +/// +/// To get these components, see [strongly_connected_components]. +/// +/// If ``rx.number_strongly_connected_components(G) == 1``, +/// then ``rx.is_strongly_connected(G) is True``. +/// +/// For undirected graphs, see [number_connected_components]. +/// +/// :param PyDiGraph graph: The directed graph to find the number +/// of strongly connected components in +/// +/// :returns: The number of strongly connected components in the graph +/// :rtype: int +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn number_strongly_connected_components(graph: &digraph::PyDiGraph) -> usize { + algo::kosaraju_scc(&graph.graph).len() +} + /// Find the strongly connected components in a directed graph /// /// A strongly connected component (SCC) is a maximal subset of vertices diff --git a/src/lib.rs b/src/lib.rs index 637db66a8c..5218074e2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -571,6 +571,7 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(undirected_random_bipartite_graph))?; m.add_wrapped(wrap_pyfunction!(cycle_basis))?; m.add_wrapped(wrap_pyfunction!(simple_cycles))?; + m.add_wrapped(wrap_pyfunction!(number_strongly_connected_components))?; m.add_wrapped(wrap_pyfunction!(strongly_connected_components))?; m.add_wrapped(wrap_pyfunction!(is_strongly_connected))?; m.add_wrapped(wrap_pyfunction!(digraph_dfs_edges))?; diff --git a/tests/digraph/test_strongly_connected.py b/tests/digraph/test_strongly_connected.py index 56b3bbd56a..f47dadc662 100644 --- a/tests/digraph/test_strongly_connected.py +++ b/tests/digraph/test_strongly_connected.py @@ -20,21 +20,16 @@ def test_number_strongly_connected_all_strong(self): G = rustworkx.PyDiGraph() node_a = G.add_node(1) node_b = G.add_child(node_a, 2, {}) - node_c = G.add_child(node_b, 3, {}) - self.assertEqual( - rustworkx.strongly_connected_components(G), - [[node_c], [node_b], [node_a]], - ) + G.add_child(node_b, 3, {}) + self.assertEqual(rustworkx.number_strongly_connected_components(G), 3) def test_number_strongly_connected(self): G = rustworkx.PyDiGraph() node_a = G.add_node(1) node_b = G.add_child(node_a, 2, {}) - node_c = G.add_node(3) - self.assertEqual( - rustworkx.strongly_connected_components(G), - [[node_c], [node_b], [node_a]], - ) + G.add_edge(node_b, node_a, {}) + G.add_node(3) + self.assertEqual(rustworkx.number_strongly_connected_components(G), 2) def test_strongly_connected_no_linear(self): G = rustworkx.PyDiGraph() From 9273d1820b8a4c04ac94999a3e973d13ca83f325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:15:05 +0100 Subject: [PATCH 3/4] Fix type annotation of output of two PyDiGraph methods --- rustworkx/rustworkx.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi index d2a0156c20..57b335cfe6 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -211,8 +211,8 @@ def is_strongly_connected(graph: PyDiGraph, /) -> bool: ... def is_weakly_connected(graph: PyDiGraph, /) -> bool: ... def is_semi_connected(graph: PyDiGraph, /) -> bool: ... def number_connected_components(graph: PyGraph, /) -> int: ... -def number_strongly_connected_components(graph: PyDiGraph, /) -> bool: ... -def number_weakly_connected_components(graph: PyDiGraph, /) -> bool: ... +def number_strongly_connected_components(graph: PyDiGraph, /) -> int: ... +def number_weakly_connected_components(graph: PyDiGraph, /) -> int: ... def node_connected_component(graph: PyGraph, node: int, /) -> set[int]: ... def strongly_connected_components(graph: PyDiGraph, /) -> list[list[int]]: ... def weakly_connected_components(graph: PyDiGraph, /) -> list[set[int]]: ... From d28323460854c8d3c6b469c44c768c316dc68c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:24:03 +0100 Subject: [PATCH 4/4] Add release note about is_strongly_connected and number_strongly_connected_components --- ...-method-is-strongly-connected-4f2918fa15b46d8f.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 releasenotes/notes/add-method-is-strongly-connected-4f2918fa15b46d8f.yaml diff --git a/releasenotes/notes/add-method-is-strongly-connected-4f2918fa15b46d8f.yaml b/releasenotes/notes/add-method-is-strongly-connected-4f2918fa15b46d8f.yaml new file mode 100644 index 0000000000..fdb60ebd59 --- /dev/null +++ b/releasenotes/notes/add-method-is-strongly-connected-4f2918fa15b46d8f.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added two new functions: :func:`~rustworkx.is_strongly_connected` and + :func:`~rustworkx.number_strongly_connected_components` + to the ``rustworkx.PyDiGraph`` class. + These functions check whether the directed graph is strongly connected, + and the number of such components, respectively. + They are the “strongly-connected” pendants for the already existing + “weakly-connected” methods. \ No newline at end of file