From c4bfddb2371055e84a7c05f45f650049648a8eea Mon Sep 17 00:00:00 2001 From: JuanPBedoya Date: Wed, 15 Jul 2026 01:01:11 -0500 Subject: [PATCH 1/2] Return neighbors in a deterministic order PyGraph.neighbors, PyDiGraph.neighbors and PyDiGraph.neighbors_undirected deduplicated their results through a HashSet and returned them via `drain()`. The HashSet iteration order is randomized, so the order of the returned node indices could change between runs and even between calls. Deduplicate while preserving the graph's edge iteration order by filtering through a `seen` set instead of draining a HashSet. This keeps the multigraph deduplication behaviour but makes the ordering stable. Also update the now-deterministic neighbors_undirected docstring example and add regression tests asserting a stable order and correct dedup. Fixes #1501 --- ...-deterministic-order-3c8e1a9f4b2d7e05.yaml | 12 ++++++++++ src/digraph.rs | 10 ++++---- src/graph.rs | 4 ++-- tests/digraph/test_neighbors.py | 23 ++++++++++++++++++ tests/graph/test_neighbors.py | 24 +++++++++++++++++++ 5 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml diff --git a/releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml b/releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml new file mode 100644 index 0000000000..cd56953a31 --- /dev/null +++ b/releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml @@ -0,0 +1,12 @@ +--- +fixes: + - | + Fixed :meth:`.PyGraph.neighbors`, :meth:`.PyDiGraph.neighbors` and + :meth:`.PyDiGraph.neighbors_undirected` to return the neighboring node + indices in a stable, deterministic order. Previously the results were + deduplicated through a ``HashSet`` whose iteration order was randomized, + so the order of the returned indices could change between runs (and even + between calls). The methods now preserve the graph's edge iteration order + while still removing duplicate neighbors in multigraphs. Refer to + `#1501 `__ for more + details. diff --git a/src/digraph.rs b/src/digraph.rs index f454dee2f9..1e32997bde 100644 --- a/src/digraph.rs +++ b/src/digraph.rs @@ -1844,13 +1844,13 @@ impl PyDiGraph { /// :rtype: NodeIndices #[pyo3(text_signature = "(self, node, /)")] pub fn neighbors(&self, node: usize) -> NodeIndices { + let mut seen = HashSet::new(); NodeIndices { nodes: self .graph .neighbors(NodeIndex::new(node)) .map(|node| node.index()) - .collect::>() - .drain() + .filter(|node| seen.insert(*node)) .collect(), } } @@ -1866,7 +1866,7 @@ impl PyDiGraph { /// >>> G.neighbors_undirected(node) /// NodeIndices[3, 1] /// >>> G.to_undirected().neighbors(node) - /// NodeIndices[1, 3] + /// NodeIndices[3, 1] /// /// For direction-aware neighbors, see :func:`~predecessors` and :func:`~successors`. /// @@ -1876,13 +1876,13 @@ impl PyDiGraph { /// :rtype: NodeIndices #[pyo3(text_signature = "(self, node, /)")] pub fn neighbors_undirected(&self, node: usize) -> NodeIndices { + let mut seen = HashSet::new(); NodeIndices { nodes: self .graph .neighbors_undirected(NodeIndex::new(node)) .map(|node| node.index()) - .collect::>() - .drain() + .filter(|node| seen.insert(*node)) .collect(), } } diff --git a/src/graph.rs b/src/graph.rs index d9cd17611d..55ac187bed 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1181,13 +1181,13 @@ impl PyGraph { /// :rtype: NodeIndices #[pyo3(text_signature = "(self, node, /)")] pub fn neighbors(&self, node: usize) -> NodeIndices { + let mut seen = HashSet::new(); NodeIndices { nodes: self .graph .neighbors(NodeIndex::new(node)) .map(|node| node.index()) - .collect::>() - .drain() + .filter(|node| seen.insert(*node)) .collect(), } } diff --git a/tests/digraph/test_neighbors.py b/tests/digraph/test_neighbors.py index acd0ee590c..99348dfa2c 100644 --- a/tests/digraph/test_neighbors.py +++ b/tests/digraph/test_neighbors.py @@ -79,3 +79,26 @@ def test_undirected_neighbors_cycle(self): undirected_neighbors = dag.neighbors_undirected(node) expected_neighbors = undirected_dag.neighbors(node) self.assertEqual(sorted(undirected_neighbors), sorted(expected_neighbors)) + + def test_neighbors_deterministic_order(self): + # Regression test for gh-1501: deterministic, stable ordering. + dag = rustworkx.PyDiGraph() + dag.add_nodes_from(range(6)) + dag.add_edges_from_no_data([(0, 1), (0, 2), (0, 3)]) + dag.add_edges_from_no_data([(4, 0), (5, 0)]) + for _ in range(20): + self.assertEqual([3, 2, 1], list(dag.neighbors(0))) + self.assertEqual([3, 2, 1, 5, 4], list(dag.neighbors_undirected(0))) + + def test_neighbors_dedup_deterministic_order(self): + # Parallel edges are deduplicated with a stable order. + dag = rustworkx.PyDiGraph() + dag.add_nodes_from(["a", "b", "c"]) + dag.add_edge(0, 1, None) + dag.add_edge(0, 1, None) + dag.add_edge(0, 2, None) + first = list(dag.neighbors(0)) + self.assertCountEqual([1, 2], first) + self.assertEqual(2, len(first)) + for _ in range(20): + self.assertEqual(first, list(dag.neighbors(0))) diff --git a/tests/graph/test_neighbors.py b/tests/graph/test_neighbors.py index dca67c2556..e77eb21dcf 100644 --- a/tests/graph/test_neighbors.py +++ b/tests/graph/test_neighbors.py @@ -41,3 +41,27 @@ def test_no_neighbor(self): graph = rustworkx.PyGraph() node_a = graph.add_node("a") self.assertEqual([], graph.neighbors(node_a)) + + def test_neighbors_deterministic_order(self): + # Regression test for gh-1501: neighbors must be returned in a stable, + # deterministic order (graph edge iteration order) rather than a + # randomized HashSet order. + graph = rustworkx.PyGraph() + graph.add_nodes_from(range(6)) + graph.add_edges_from_no_data([(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)]) + expected = [5, 4, 3, 2, 1] + for _ in range(20): + self.assertEqual(expected, list(graph.neighbors(0))) + + def test_neighbors_dedup_deterministic_order(self): + # Parallel edges are deduplicated, and the surviving order is stable. + graph = rustworkx.PyGraph() + graph.add_nodes_from(["a", "b", "c"]) + graph.add_edge(0, 1, None) + graph.add_edge(0, 1, None) + graph.add_edge(0, 2, None) + first = list(graph.neighbors(0)) + self.assertCountEqual([1, 2], first) + self.assertEqual(2, len(first)) + for _ in range(20): + self.assertEqual(first, list(graph.neighbors(0))) From 0988952cd68af8fa1ad17dcd9b9566582e6ef06c Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Wed, 15 Jul 2026 12:24:26 -0400 Subject: [PATCH 2/2] Use IndexSet for deterministic ordering --- ...hbors-deterministic-order-3c8e1a9f4b2d7e05.yaml | 14 +++++--------- src/digraph.rs | 8 ++++---- src/graph.rs | 4 ++-- tests/digraph/test_neighbors.py | 3 ++- tests/graph/test_neighbors.py | 5 ++--- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml b/releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml index cd56953a31..b3a221a58a 100644 --- a/releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml +++ b/releasenotes/notes/fix-neighbors-deterministic-order-3c8e1a9f4b2d7e05.yaml @@ -1,12 +1,8 @@ --- fixes: - | - Fixed :meth:`.PyGraph.neighbors`, :meth:`.PyDiGraph.neighbors` and - :meth:`.PyDiGraph.neighbors_undirected` to return the neighboring node - indices in a stable, deterministic order. Previously the results were - deduplicated through a ``HashSet`` whose iteration order was randomized, - so the order of the returned indices could change between runs (and even - between calls). The methods now preserve the graph's edge iteration order - while still removing duplicate neighbors in multigraphs. Refer to - `#1501 `__ for more - details. + Fixed :meth:`.PyGraph.neighbors`, :meth:`.PyDiGraph.neighbors`, and + :meth:`.PyDiGraph.neighbors_undirected` to return neighbors in a + deterministic order. Previously, calls to the functions could return + nodes in an arbitrary order. See + `#1501 `__. diff --git a/src/digraph.rs b/src/digraph.rs index 1e32997bde..fd601d7b90 100644 --- a/src/digraph.rs +++ b/src/digraph.rs @@ -1844,13 +1844,13 @@ impl PyDiGraph { /// :rtype: NodeIndices #[pyo3(text_signature = "(self, node, /)")] pub fn neighbors(&self, node: usize) -> NodeIndices { - let mut seen = HashSet::new(); NodeIndices { nodes: self .graph .neighbors(NodeIndex::new(node)) .map(|node| node.index()) - .filter(|node| seen.insert(*node)) + .collect::>() + .drain(..) .collect(), } } @@ -1876,13 +1876,13 @@ impl PyDiGraph { /// :rtype: NodeIndices #[pyo3(text_signature = "(self, node, /)")] pub fn neighbors_undirected(&self, node: usize) -> NodeIndices { - let mut seen = HashSet::new(); NodeIndices { nodes: self .graph .neighbors_undirected(NodeIndex::new(node)) .map(|node| node.index()) - .filter(|node| seen.insert(*node)) + .collect::>() + .drain(..) .collect(), } } diff --git a/src/graph.rs b/src/graph.rs index 55ac187bed..ed54430fc0 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1181,13 +1181,13 @@ impl PyGraph { /// :rtype: NodeIndices #[pyo3(text_signature = "(self, node, /)")] pub fn neighbors(&self, node: usize) -> NodeIndices { - let mut seen = HashSet::new(); NodeIndices { nodes: self .graph .neighbors(NodeIndex::new(node)) .map(|node| node.index()) - .filter(|node| seen.insert(*node)) + .collect::>() + .drain(..) .collect(), } } diff --git a/tests/digraph/test_neighbors.py b/tests/digraph/test_neighbors.py index 99348dfa2c..4be041d85d 100644 --- a/tests/digraph/test_neighbors.py +++ b/tests/digraph/test_neighbors.py @@ -81,7 +81,8 @@ def test_undirected_neighbors_cycle(self): self.assertEqual(sorted(undirected_neighbors), sorted(expected_neighbors)) def test_neighbors_deterministic_order(self): - # Regression test for gh-1501: deterministic, stable ordering. + # Regression test for https://github.com/Qiskit/rustworkx/issues/1501: + # neighbors must be returned in a deterministic order. dag = rustworkx.PyDiGraph() dag.add_nodes_from(range(6)) dag.add_edges_from_no_data([(0, 1), (0, 2), (0, 3)]) diff --git a/tests/graph/test_neighbors.py b/tests/graph/test_neighbors.py index e77eb21dcf..e3a81872c9 100644 --- a/tests/graph/test_neighbors.py +++ b/tests/graph/test_neighbors.py @@ -43,9 +43,8 @@ def test_no_neighbor(self): self.assertEqual([], graph.neighbors(node_a)) def test_neighbors_deterministic_order(self): - # Regression test for gh-1501: neighbors must be returned in a stable, - # deterministic order (graph edge iteration order) rather than a - # randomized HashSet order. + # Regression test for https://github.com/Qiskit/rustworkx/issues/1501: + # neighbors must be returned in a deterministic order. graph = rustworkx.PyGraph() graph.add_nodes_from(range(6)) graph.add_edges_from_no_data([(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)])