From ad990785de4a49c49924aeee3b3daed5a4786a0b Mon Sep 17 00:00:00 2001 From: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:53:50 -0400 Subject: [PATCH] Handle self-loops in all_simple_paths (#1619) (cherry picked from commit 8a0cf2dabd771bcdcf5b5072b360ebb02d82dc46) --- ...ple-paths-self-loops-4d5e9a247d39d6a4.yaml | 7 ++++ src/connectivity/mod.rs | 32 ++++++++++++++++-- tests/digraph/test_all_simple_paths.py | 33 +++++++++++++++++++ tests/graph/test_all_simple_paths.py | 7 ++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/all-simple-paths-self-loops-4d5e9a247d39d6a4.yaml diff --git a/releasenotes/notes/all-simple-paths-self-loops-4d5e9a247d39d6a4.yaml b/releasenotes/notes/all-simple-paths-self-loops-4d5e9a247d39d6a4.yaml new file mode 100644 index 0000000000..3c2bd06cba --- /dev/null +++ b/releasenotes/notes/all-simple-paths-self-loops-4d5e9a247d39d6a4.yaml @@ -0,0 +1,7 @@ +fixes: + - | + Fixed :func:`~rustworkx.all_simple_paths` so a self-loop is returned as a + two-node path when its source is also the requested target. This restores + the behavior from 0.17.1 for both :class:`~rustworkx.PyGraph` and + :class:`~rustworkx.PyDiGraph`. Refer to + `#1617 `__ for more details. diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index b6d05e1006..154e1c79cf 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -1086,6 +1086,16 @@ pub fn graph_all_simple_paths( )); } + // Preserve the legacy behavior of emitting a self-loop as a + // two-node path. petgraph's iterator filters the target because + // it is already visited before it can yield this path. + if from_index == to_index && min_intermediate_nodes == 0 { + let self_loop_count = graph.graph.edges_connecting(from_index, to_index).count(); + if self_loop_count > 0 { + return Ok(vec![vec![origin, origin]; self_loop_count]); + } + } + let result: Vec> = algo::all_simple_paths::, _, foldhash::fast::RandomState>( &graph.graph, @@ -1099,7 +1109,7 @@ pub fn graph_all_simple_paths( Ok(result) } TargetNodes::Multiple(target_set) => { - let result: Vec> = + let mut result: Vec> = algo::all_simple_paths_multi::, _, foldhash::fast::RandomState>( &graph.graph, from_index, @@ -1109,6 +1119,10 @@ pub fn graph_all_simple_paths( ) .map(|v: Vec| v.into_iter().map(|i| i.index()).collect()) .collect(); + if min_intermediate_nodes == 0 && target_set.contains(&from_index) { + let self_loop_count = graph.graph.edges_connecting(from_index, from_index).count(); + result.extend(vec![vec![origin, origin]; self_loop_count]); + } Ok(result) } } @@ -1159,6 +1173,16 @@ pub fn digraph_all_simple_paths( )); } + // Preserve the legacy behavior of emitting a self-loop as a + // two-node path. petgraph's iterator filters the target because + // it is already visited before it can yield this path. + if from_index == to_index && min_intermediate_nodes == 0 { + let self_loop_count = graph.graph.edges_connecting(from_index, to_index).count(); + if self_loop_count > 0 { + return Ok(vec![vec![origin, origin]; self_loop_count]); + } + } + let result: Vec> = algo::all_simple_paths::, _, foldhash::fast::RandomState>( &graph.graph, @@ -1172,7 +1196,7 @@ pub fn digraph_all_simple_paths( Ok(result) } TargetNodes::Multiple(target_set) => { - let result: Vec> = + let mut result: Vec> = algo::all_simple_paths_multi::, _, foldhash::fast::RandomState>( &graph.graph, from_index, @@ -1182,6 +1206,10 @@ pub fn digraph_all_simple_paths( ) .map(|v: Vec| v.into_iter().map(|i| i.index()).collect()) .collect(); + if min_intermediate_nodes == 0 && target_set.contains(&from_index) { + let self_loop_count = graph.graph.edges_connecting(from_index, from_index).count(); + result.extend(vec![vec![origin, origin]; self_loop_count]); + } Ok(result) } } diff --git a/tests/digraph/test_all_simple_paths.py b/tests/digraph/test_all_simple_paths.py index 002fc257d2..f1cf39dd7e 100644 --- a/tests/digraph/test_all_simple_paths.py +++ b/tests/digraph/test_all_simple_paths.py @@ -118,6 +118,39 @@ def test_all_simple_path_no_path(self): dag.add_node(1) self.assertEqual([], rustworkx.digraph_all_simple_paths(dag, 0, 1)) + def test_all_simple_paths_self_loop(self): + graph = rustworkx.PyDiGraph() + node = graph.add_node("A") + graph.add_edge(node, node, {"label": "self"}) + + self.assertEqual([[node, node]], rustworkx.digraph_all_simple_paths(graph, node, node)) + + def test_all_simple_paths_self_loop_is_not_revisited(self): + graph = rustworkx.PyDiGraph() + source = graph.add_node("A") + target = graph.add_node("B") + graph.add_edges_from_no_data([(source, target), (target, target)]) + + self.assertEqual( + [[source, target]], rustworkx.digraph_all_simple_paths(graph, source, target) + ) + + def test_all_simple_paths_self_loop_respects_min_depth(self): + graph = rustworkx.PyDiGraph() + node = graph.add_node("A") + graph.add_edge(node, node, {"label": "self"}) + + self.assertEqual([], rustworkx.digraph_all_simple_paths(graph, node, node, min_depth=3)) + + def test_all_simple_paths_multiple_targets_includes_self_loop(self): + graph = rustworkx.PyDiGraph() + source = graph.add_node("A") + target = graph.add_node("B") + graph.add_edges_from_no_data([(source, source), (source, target)]) + + paths = rustworkx.digraph_all_simple_paths(graph, source, [source, target]) + self.assertCountEqual([[source, source], [source, target]], paths) + def test_all_simple_path_invalid_node_index(self): dag = rustworkx.PyDAG() dag.add_node(0) diff --git a/tests/graph/test_all_simple_paths.py b/tests/graph/test_all_simple_paths.py index d1ba33e326..5dfe9ac752 100644 --- a/tests/graph/test_all_simple_paths.py +++ b/tests/graph/test_all_simple_paths.py @@ -219,6 +219,13 @@ def test_all_simple_path_no_path(self): dag.add_node(1) self.assertEqual([], rustworkx.graph_all_simple_paths(dag, 0, 1)) + def test_all_simple_paths_self_loop(self): + graph = rustworkx.PyGraph() + node = graph.add_node("A") + graph.add_edge(node, node, {"label": "self"}) + + self.assertEqual([[node, node]], rustworkx.graph_all_simple_paths(graph, node, node)) + def test_all_simple_path_invalid_node_index(self): dag = rustworkx.PyGraph() dag.add_node(0)