Repository: TeamGraphix/graphix

Issue:
[Bug]: `rustworkx` fails to handle non-contiguous node numbering
The following code raises `KeyError` in `rxgraphviews.get_node_index` when `use_rustworkx` is `True`:
```python
    @pytest.mark.parametrize("use_rustworkx", [False, True])
    def test_pauli_non_contiguous(self, use_rustworkx) -> None:
        pattern = Pattern(input_nodes=[0])
        pattern.extend([
            N(node=2, state=PlanarState(plane=Plane.XY, angle=0.0)),
            E(nodes=(0, 2)),
            M(node=0, plane=Plane.XY, angle=0.0, s_domain=set(), t_domain=set())
        ])
        pattern.perform_pauli_measurements(use_rustworkx=use_rustworkx)
```

The bug seems to be between `RXGraphState.neighbors`, `RXGraphState.subgraphs` and `RXGraphState.local_complement`: the indexes returned by `neighbors` are in the rustworkx numbering, whereas `local_complement` calls `subgraphs` on these nodes, which expects them to be in the graphix numbering.

A fix could be to add a `idx_to_num` mapping in `rxgraphviews` and convert neighbors in the graphix numbering.

Related issues: #206 (problem with `rxgraphviews`).

Discussion:
EarlMilktea: @thierry-martinez 

I merged `master` to run CI against this PR.
thierry-martinez: Thanks. Everything is green: may I merge?

Patch:
diff --git a/graphix/graphsim/rxgraphstate.py b/graphix/graphsim/rxgraphstate.py
index f310b79..d0d67c1 100644
--- a/graphix/graphsim/rxgraphstate.py
+++ b/graphix/graphsim/rxgraphstate.py
@@ -75,13 +75,13 @@ class RXGraphState(BaseGraphState):
             ret.append((n, degree))
         return iter(ret)
 
-    def neighbors(self, node) -> Iterator:
+    def neighbors(self, node) -> Iterator[int]:
         """Return an iterator over all neighbors of node n.
 
         See :meth:`BaseGraphState.neighbors`.
         """
         nidx = self.nodes.get_node_index(node)
-        return iter(self._graph.neighbors(nidx))
+        return (self.nodes.idx_to_num[idx] for idx in self._graph.neighbors(nidx))
 
     def subgraph(self, nodes: list) -> rx.PyGraph:
         """Return a subgraph of the graph.
diff --git a/graphix/graphsim/rxgraphviews.py b/graphix/graphsim/rxgraphviews.py
index 0bdffff..9a9c42a 100644
--- a/graphix/graphsim/rxgraphviews.py
+++ b/graphix/graphsim/rxgraphviews.py
@@ -31,6 +31,7 @@ class NodeList:
         self.nodes = set(node_nums)
         self.num_to_data = {nnum: node_datas[nidx] for nidx, nnum in zip(node_indices, node_nums)}
         self.num_to_idx = {nnum: nidx for nidx, nnum in zip(node_indices, node_nums)}
+        self.idx_to_num = {nidx: nnum for nidx, nnum in zip(node_indices, node_nums)}
 
     def __contains__(self, nnum: int) -> bool:
         """Return `True` if the node `nnum` belongs to the list, `False` otherwise."""
@@ -64,6 +65,7 @@ class NodeList:
         self.nodes.add(nnum)
         self.num_to_data[nnum] = ndata
         self.num_to_idx[nnum] = nidx
+        self.idx_to_num[nidx] = nnum
 
     def add_nodes_from(self, node_nums: list[int], node_datas: list[dict], node_indices: list[int]) -> None:
         """Add nodes to the list."""
@@ -80,7 +82,8 @@ class NodeList:
             raise ValueError(f"Node {nnum} does not exist")
         self.nodes.remove(nnum)
         del self.num_to_data[nnum]
-        del self.num_to_idx[nnum]
+        idx = self.num_to_idx.pop(nnum)
+        del self.idx_to_num[idx]
 
     def remove_nodes_from(self, node_nums: list[int]) -> None:
         """Remove nodes from the list."""


Test patch:
diff --git a/tests/test_pattern.py b/tests/test_pattern.py
index 9b069b1..6679abc 100644
--- a/tests/test_pattern.py
+++ b/tests/test_pattern.py
@@ -78,6 +78,18 @@ class TestPattern:
         state_mbqc = pattern.simulate_pattern(rng=fx_rng)
         assert np.abs(np.dot(state_mbqc.flatten().conjugate(), state.flatten())) == pytest.approx(1)
 
+    @pytest.mark.parametrize("use_rustworkx", [False, True])
+    def test_pauli_non_contiguous(self, use_rustworkx: bool) -> None:
+        pattern = Pattern(input_nodes=[0])
+        pattern.extend(
+            [
+                N(node=2, state=PlanarState(plane=Plane.XY, angle=0.0)),
+                E(nodes=(0, 2)),
+                M(node=0, plane=Plane.XY, angle=0.0, s_domain=set(), t_domain=set()),
+            ]
+        )
+        pattern.perform_pauli_measurements(use_rustworkx=use_rustworkx)
+
     @pytest.mark.parametrize("jumps", range(1, 11))
     def test_minimize_space_with_gflow(self, fx_bg: PCG64, jumps: int, use_rustworkx: bool = True) -> None:
         rng = Generator(fx_bg.jumped(jumps))