diff --git a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst index ad8a12d646..d1ecb165e1 100644 --- a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst +++ b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst @@ -10,7 +10,9 @@ 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 rustworkx.weakly_connected_components rustworkx.is_weakly_connected diff --git a/docs/source/sources.txt b/docs/source/sources.txt index eb6a5f0743..65b8449472 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 @@ -164,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/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 diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi index a61bc8374b..fc342c643c 100644 --- a/rustworkx/__init__.pyi +++ b/rustworkx/__init__.pyi @@ -78,9 +78,11 @@ 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 +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 39ab64a79e..57b335cfe6 100644 --- a/rustworkx/rustworkx.pyi +++ b/rustworkx/rustworkx.pyi @@ -207,10 +207,12 @@ 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: ... -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]]: ... diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 76ac6096d8..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 @@ -131,6 +160,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 +311,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 +426,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..5218074e2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -571,7 +571,9 @@ 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))?; 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..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() @@ -65,3 +60,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)