Skip to content

Commit b4bd378

Browse files
authored
Add is_strongly_connected and number_strongly_connected_components methods (#1400)
* Add is_strongly_connected method * Add number_strongly_connected_components method * Fix type annotation of output of two PyDiGraph methods * Add release note about is_strongly_connected and number_strongly_connected_components
1 parent 5f10121 commit b4bd378

File tree

8 files changed

+131
-14
lines changed

8 files changed

+131
-14
lines changed

docs/source/api/algorithm_functions/connectivity_and_cycles.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ Connectivity and Cycles
1010
rustworkx.connected_components
1111
rustworkx.node_connected_component
1212
rustworkx.is_connected
13+
rustworkx.number_strongly_connected_components
1314
rustworkx.strongly_connected_components
15+
rustworkx.is_strongly_connected
1416
rustworkx.number_weakly_connected_components
1517
rustworkx.weakly_connected_components
1618
rustworkx.is_weakly_connected

docs/source/sources.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ rustworkx.is_isomorphic.html
143143
rustworkx.is_isomorphic_node_match.html
144144
rustworkx.is_matching.html
145145
rustworkx.is_maximal_matching.html
146+
rustworkx.is_strongly_connected.html
146147
rustworkx.is_subgraph_isomorphic.html
147148
rustworkx.is_weakly_connected.html
148149
rustworkx.k_shortest_path_lengths.html
@@ -164,6 +165,7 @@ rustworkx.NoPathFound.html
164165
rustworkx.NoSuitableNeighbors.html
165166
rustworkx.NullGraph.html
166167
rustworkx.number_connected_components.html
168+
rustworkx.number_strongly_connected_components.html
167169
rustworkx.number_weakly_connected_components.html
168170
rustworkx.num_shortest_paths_unweighted.html
169171
rustworkx.PathLengthMapping.html
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
features:
3+
- |
4+
Added two new functions: :func:`~rustworkx.is_strongly_connected` and
5+
:func:`~rustworkx.number_strongly_connected_components`
6+
to the ``rustworkx.PyDiGraph`` class.
7+
These functions check whether the directed graph is strongly connected,
8+
and the number of such components, respectively.
9+
They are the “strongly-connected” pendants for the already existing
10+
“weakly-connected” methods.

rustworkx/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ from .rustworkx import graph_misra_gries_edge_color as graph_misra_gries_edge_co
7878
from .rustworkx import graph_bipartite_edge_color as graph_bipartite_edge_color
7979
from .rustworkx import connected_components as connected_components
8080
from .rustworkx import is_connected as is_connected
81+
from .rustworkx import is_strongly_connected as is_strongly_connected
8182
from .rustworkx import is_weakly_connected as is_weakly_connected
8283
from .rustworkx import is_semi_connected as is_semi_connected
8384
from .rustworkx import number_connected_components as number_connected_components
85+
from .rustworkx import number_strongly_connected_components as number_strongly_connected_components
8486
from .rustworkx import number_weakly_connected_components as number_weakly_connected_components
8587
from .rustworkx import node_connected_component as node_connected_component
8688
from .rustworkx import strongly_connected_components as strongly_connected_components

rustworkx/rustworkx.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,12 @@ def graph_bipartite_edge_color(graph: PyGraph, /) -> dict[int, int]: ...
207207

208208
def connected_components(graph: PyGraph, /) -> list[set[int]]: ...
209209
def is_connected(graph: PyGraph, /) -> bool: ...
210+
def is_strongly_connected(graph: PyDiGraph, /) -> bool: ...
210211
def is_weakly_connected(graph: PyDiGraph, /) -> bool: ...
211212
def is_semi_connected(graph: PyDiGraph, /) -> bool: ...
212213
def number_connected_components(graph: PyGraph, /) -> int: ...
213-
def number_weakly_connected_components(graph: PyDiGraph, /) -> bool: ...
214+
def number_strongly_connected_components(graph: PyDiGraph, /) -> int: ...
215+
def number_weakly_connected_components(graph: PyDiGraph, /) -> int: ...
214216
def node_connected_component(graph: PyGraph, node: int, /) -> set[int]: ...
215217
def strongly_connected_components(graph: PyDiGraph, /) -> list[list[int]]: ...
216218
def weakly_connected_components(graph: PyDiGraph, /) -> list[set[int]]: ...

src/connectivity/mod.rs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,35 @@ pub fn simple_cycles(
100100
johnson_simple_cycles::PySimpleCycleIter::new(py, graph)
101101
}
102102

103+
/// Find the number of strongly connected components in a directed graph
104+
///
105+
/// A strongly connected component (SCC) is a maximal subset of vertices
106+
/// such that every vertex is reachable from every other vertex
107+
/// within that subset.
108+
///
109+
/// >>> G = rx.PyDiGraph()
110+
/// >>> G.extend_from_edge_list([(0, 1), (1, 2), (3, 4)])
111+
/// >>> rx.number_strongly_connected_components(G)
112+
/// 2
113+
///
114+
/// To get these components, see [strongly_connected_components].
115+
///
116+
/// If ``rx.number_strongly_connected_components(G) == 1``,
117+
/// then ``rx.is_strongly_connected(G) is True``.
118+
///
119+
/// For undirected graphs, see [number_connected_components].
120+
///
121+
/// :param PyDiGraph graph: The directed graph to find the number
122+
/// of strongly connected components in
123+
///
124+
/// :returns: The number of strongly connected components in the graph
125+
/// :rtype: int
126+
#[pyfunction]
127+
#[pyo3(text_signature = "(graph, /)")]
128+
pub fn number_strongly_connected_components(graph: &digraph::PyDiGraph) -> usize {
129+
algo::kosaraju_scc(&graph.graph).len()
130+
}
131+
103132
/// Find the strongly connected components in a directed graph
104133
///
105134
/// A strongly connected component (SCC) is a maximal subset of vertices
@@ -131,6 +160,38 @@ pub fn strongly_connected_components(graph: &digraph::PyDiGraph) -> Vec<Vec<usiz
131160
.collect()
132161
}
133162

163+
/// Check if a directed graph is strongly connected
164+
///
165+
/// A strongly connected component (SCC) is a maximal subset of vertices
166+
/// such that every vertex is reachable from every other vertex
167+
/// within that subset.
168+
///
169+
/// >>> G = rx.PyDiGraph()
170+
/// >>> G.extend_from_edge_list([(0, 1), (1, 2), (3, 4)])
171+
/// >>> rx.is_strongly_connected(G)
172+
/// False
173+
///
174+
/// See also [is_weakly_connected] and [is_semi_connected].
175+
///
176+
/// If ``rx.is_strongly_connected(G) is True`` then `rx.number_strongly_connected_components(G) == 1``.
177+
///
178+
/// For undirected graphs see [is_connected].
179+
///
180+
/// :param PyGraph graph: An undirected graph to check for strong connectivity
181+
///
182+
/// :returns: Whether the graph is strongly connected or not
183+
/// :rtype: bool
184+
///
185+
/// :raises NullGraph: If an empty graph is passed in
186+
#[pyfunction]
187+
#[pyo3(text_signature = "(graph, /)")]
188+
pub fn is_strongly_connected(graph: &digraph::PyDiGraph) -> PyResult<bool> {
189+
if graph.graph.node_count() == 0 {
190+
return Err(NullGraph::new_err("Invalid operation on a NullGraph"));
191+
}
192+
Ok(algo::kosaraju_scc(&graph.graph).len() == 1)
193+
}
194+
134195
/// Return the first cycle encountered during DFS of a given PyDiGraph,
135196
/// empty list is returned if no cycle is found
136197
///
@@ -250,7 +311,8 @@ pub fn node_connected_component(graph: &graph::PyGraph, node: usize) -> PyResult
250311
///
251312
/// If ``rx.is_connected(G) is True`` then `rx.number_connected_components(G) == 1``.
252313
///
253-
/// For directed graphs see [is_weakly_connected].
314+
/// For directed graphs see [is_weakly_connected], [is_semi_connected],
315+
/// and [is_strongly_connected].
254316
///
255317
/// :param PyGraph graph: An undirected graph to check for connectivity
256318
///
@@ -364,7 +426,7 @@ pub fn weakly_connected_components(graph: &digraph::PyDiGraph) -> Vec<HashSet<us
364426
///
365427
/// :param PyGraph graph: An undirected graph to check for weak connectivity
366428
///
367-
/// :returns: Whether the graph is connected or not
429+
/// :returns: Whether the graph is weakly connected or not
368430
/// :rtype: bool
369431
///
370432
/// :raises NullGraph: If an empty graph is passed in
@@ -389,7 +451,7 @@ pub fn is_weakly_connected(graph: &digraph::PyDiGraph) -> PyResult<bool> {
389451
/// >>> rx.is_semi_connected(G)
390452
/// False
391453
///
392-
/// See also [is_weakly_connected].
454+
/// See also [is_weakly_connected] and [is_strongly_connected].
393455
///
394456
/// For undirected graphs see [is_connected].
395457
///

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,9 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
571571
m.add_wrapped(wrap_pyfunction!(undirected_random_bipartite_graph))?;
572572
m.add_wrapped(wrap_pyfunction!(cycle_basis))?;
573573
m.add_wrapped(wrap_pyfunction!(simple_cycles))?;
574+
m.add_wrapped(wrap_pyfunction!(number_strongly_connected_components))?;
574575
m.add_wrapped(wrap_pyfunction!(strongly_connected_components))?;
576+
m.add_wrapped(wrap_pyfunction!(is_strongly_connected))?;
575577
m.add_wrapped(wrap_pyfunction!(digraph_dfs_edges))?;
576578
m.add_wrapped(wrap_pyfunction!(graph_dfs_edges))?;
577579
m.add_wrapped(wrap_pyfunction!(digraph_find_cycle))?;

tests/digraph/test_strongly_connected.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,16 @@ def test_number_strongly_connected_all_strong(self):
2020
G = rustworkx.PyDiGraph()
2121
node_a = G.add_node(1)
2222
node_b = G.add_child(node_a, 2, {})
23-
node_c = G.add_child(node_b, 3, {})
24-
self.assertEqual(
25-
rustworkx.strongly_connected_components(G),
26-
[[node_c], [node_b], [node_a]],
27-
)
23+
G.add_child(node_b, 3, {})
24+
self.assertEqual(rustworkx.number_strongly_connected_components(G), 3)
2825

2926
def test_number_strongly_connected(self):
3027
G = rustworkx.PyDiGraph()
3128
node_a = G.add_node(1)
3229
node_b = G.add_child(node_a, 2, {})
33-
node_c = G.add_node(3)
34-
self.assertEqual(
35-
rustworkx.strongly_connected_components(G),
36-
[[node_c], [node_b], [node_a]],
37-
)
30+
G.add_edge(node_b, node_a, {})
31+
G.add_node(3)
32+
self.assertEqual(rustworkx.number_strongly_connected_components(G), 2)
3833

3934
def test_strongly_connected_no_linear(self):
4035
G = rustworkx.PyDiGraph()
@@ -65,3 +60,43 @@ def test_number_strongly_connected_big(self):
6560
node = G.add_node(i)
6661
G.add_child(node, str(i), {})
6762
self.assertEqual(len(rustworkx.strongly_connected_components(G)), 200000)
63+
64+
def test_is_strongly_connected_false(self):
65+
graph = rustworkx.PyDiGraph()
66+
graph.extend_from_edge_list(
67+
[
68+
(0, 1),
69+
(1, 2),
70+
(2, 3),
71+
(3, 0),
72+
(2, 4),
73+
(4, 5),
74+
(5, 6),
75+
(6, 7),
76+
(7, 4),
77+
]
78+
)
79+
self.assertFalse(rustworkx.is_strongly_connected(graph))
80+
81+
def test_is_strongly_connected_true(self):
82+
graph = rustworkx.PyDiGraph()
83+
graph.extend_from_edge_list(
84+
[
85+
(0, 1),
86+
(1, 2),
87+
(2, 3),
88+
(3, 0),
89+
(2, 4),
90+
(4, 2), # <- missing in the test_is_strongly_connected_false
91+
(4, 5),
92+
(5, 6),
93+
(6, 7),
94+
(7, 4),
95+
]
96+
)
97+
self.assertTrue(rustworkx.is_strongly_connected(graph))
98+
99+
def test_is_strongly_connected_null_graph(self):
100+
graph = rustworkx.PyDiGraph()
101+
with self.assertRaises(rustworkx.NullGraph):
102+
rustworkx.is_strongly_connected(graph)

0 commit comments

Comments
 (0)