Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/source/sources.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
eumiro marked this conversation as resolved.
rustworkx.is_subgraph_isomorphic.html
rustworkx.is_weakly_connected.html
rustworkx.k_shortest_path_lengths.html
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions rustworkx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion rustworkx/rustworkx.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Comment thread
eumiro marked this conversation as resolved.
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]]: ...
Expand Down
68 changes: 65 additions & 3 deletions src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -131,6 +160,38 @@ pub fn strongly_connected_components(graph: &digraph::PyDiGraph) -> Vec<Vec<usiz
.collect()
}

/// Check if a directed graph is strongly connected
///
/// 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.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<bool> {
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
///
Expand Down Expand Up @@ -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
///
Expand Down Expand Up @@ -364,7 +426,7 @@ pub fn weakly_connected_components(graph: &digraph::PyDiGraph) -> Vec<HashSet<us
///
/// :param PyGraph graph: An undirected graph to check for weak connectivity
///
/// :returns: Whether the graph is connected or not
/// :returns: Whether the graph is weakly connected or not
/// :rtype: bool
///
/// :raises NullGraph: If an empty graph is passed in
Expand All @@ -389,7 +451,7 @@ pub fn is_weakly_connected(graph: &digraph::PyDiGraph) -> PyResult<bool> {
/// >>> 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].
///
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,9 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> 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))?;
Expand Down
55 changes: 45 additions & 10 deletions tests/digraph/test_strongly_connected.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)