Repository: oscarhiggott/PyMatching (language: python)

Issue:
Rename `retworkx`
The package previously known as `retworkx` is now `rustworkx`. All references in PyMatching should be updated.


Discussion / hints:
I agree that retworkx should be replaced with rustworkx as a dependency, but the `Matching.load_from_retworkx` and `Matching.to_retworkx` methods should be kept for backward compatibility (they can try importing retworkx and use rustworkx if that fails). Then methods `Matching.load_from_rustworkx` and `Matching.to_rustworkx` could be added for rustworkx.

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 62909aa5..cf6a27dc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -41,32 +41,36 @@ jobs:
       matrix:
         os_dist: [
           # macosx x86_64
-          {os: macos-latest, dist: cp36-macosx_x86_64},
+          # {os: macos-latest, dist: cp36-macosx_x86_64},
           {os: macos-latest, dist: cp37-macosx_x86_64},
           {os: macos-latest, dist: cp38-macosx_x86_64},
           {os: macos-latest, dist: cp39-macosx_x86_64},
           {os: macos-latest, dist: cp310-macosx_x86_64},
           {os: macos-latest, dist: cp311-macosx_x86_64},
+          {os: macos-latest, dist: cp312-macosx_x86_64},
           # macosx arm64
           {os: macos-latest, dist: cp38-macosx_arm64},
           {os: macos-latest, dist: cp39-macosx_arm64},
           {os: macos-latest, dist: cp310-macosx_arm64},
           {os: macos-latest, dist: cp311-macosx_arm64},
+          {os: macos-latest, dist: cp312-macosx_arm64},
           # macosx universal2
           {os: macos-latest, dist: cp38-macosx_universal2},
           {os: macos-latest, dist: cp39-macosx_universal2},
           {os: macos-latest, dist: cp310-macosx_universal2},
           {os: macos-latest, dist: cp311-macosx_universal2},
+          {os: macos-latest, dist: cp312-macosx_universal2},
 
           # windows amd64
-          {os: windows-latest, dist: cp36-win_amd64},
+          # {os: windows-latest, dist: cp36-win_amd64},
           {os: windows-latest, dist: cp37-win_amd64},
           {os: windows-latest, dist: cp38-win_amd64},
           {os: windows-latest, dist: cp39-win_amd64},
           {os: windows-latest, dist: cp310-win_amd64},
           {os: windows-latest, dist: cp311-win_amd64},
+          {os: windows-latest, dist: cp312-win_amd64},
           # windows win32
-          {os: windows-latest, dist: cp36-win32},
+          # {os: windows-latest, dist: cp36-win32},
           {os: windows-latest, dist: cp37-win32},
           # scipy install fails
 #          {os: windows-latest, dist: cp38-win32},
@@ -79,14 +83,15 @@ jobs:
 #          {os: windows-latest, dist: cp311-win_arm64},
 
           # ubuntu x86_64
-          {os: ubuntu-latest, dist: cp36-manylinux_x86_64},
+          # {os: ubuntu-latest, dist: cp36-manylinux_x86_64},
           {os: ubuntu-latest, dist: cp37-manylinux_x86_64},
           {os: ubuntu-latest, dist: cp38-manylinux_x86_64},
           {os: ubuntu-latest, dist: cp39-manylinux_x86_64},
           {os: ubuntu-latest, dist: cp310-manylinux_x86_64},
           {os: ubuntu-latest, dist: cp311-manylinux_x86_64},
+          {os: ubuntu-latest, dist: cp312-manylinux_x86_64},
           # ubuntu i686
-          {os: ubuntu-latest, dist: cp36-manylinux_i686},
+          # {os: ubuntu-latest, dist: cp36-manylinux_i686},
           {os: ubuntu-latest, dist: cp37-manylinux_i686},
           # scipy built distribution not available and build fails on manylinux_i686 for python 3.8 up
 #          {os: ubuntu-latest, dist: cp38-manylinux_i686},
@@ -118,7 +123,7 @@ jobs:
         sudo apt update
         sudo apt install gcc-10 g++-10
 
-    - uses: pypa/cibuildwheel@v2.11.1
+    - uses: pypa/cibuildwheel@v2.16.4
 
     - name: Verify clean directory
       run: git diff --exit-code
diff --git a/.gitignore b/.gitignore
index d1dcac6a..a741a3f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+cov.xml
 .coverage
 *.ipynb_checkpoints
 .idea
@@ -8,6 +9,7 @@ Testing/
 _deps/
 *.cmake
 ENV
+env
 circuits
 notes
 dist
@@ -30,3 +32,4 @@ build.ninja
 .ninja_deps
 .ninja_log
 .clwb
+.vscode
diff --git a/README.md b/README.md
index e2940e66..a1d4752d 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,7 @@ pip install pymatching --upgrade
 ## Usage
 
 PyMatching can load matching graphs from a check matrix, a `stim.DetectorErrorModel`, a `networkx.Graph`, a 
-`retworkx.PyGraph` or by adding edges individually with `pymatching.Matching.add_edge` and 
+`rustworkx.PyGraph` or by adding edges individually with `pymatching.Matching.add_edge` and 
 `pymatching.Matching.add_boundary_edge`.
 
 ### Decoding Stim circuits
@@ -287,7 +287,7 @@ Instead of using a check matrix, the Matching object can also be constructed usi
 the [`Matching.add_edge`](https://pymatching.readthedocs.io/en/stable/api.html#pymatching.matching.Matching.add_edge)
 and 
 [`Matching.add_boundary_edge`](https://pymatching.readthedocs.io/en/stable/api.html#pymatching.matching.Matching.add_boundary_edge) 
-methods, or by loading from a NetworkX or retworkx graph. 
+methods, or by loading from a NetworkX or rustworkx graph. 
 
 For more details on how to use PyMatching,
 see [the documentation](https://pymatching.readthedocs.io).
diff --git a/setup.py b/setup.py
index 21ba75f0..7154a15f 100644
--- a/setup.py
+++ b/setup.py
@@ -152,8 +152,8 @@ def build_extension(self, ext):
     entry_points={
         'console_scripts': ['pymatching=pymatching._cli_argv:cli_argv'],
     },
-    python_requires=">=3.6",
-    install_requires=['scipy', 'numpy', 'networkx', 'retworkx>=0.11.0', 'matplotlib'],
+    python_requires=">=3.7",
+    install_requires=['scipy', 'numpy', 'networkx', 'rustworkx', 'matplotlib'],
     # Needed on Windows to avoid the default `build` colliding with Bazel's `BUILD`.
     options={'build': {'build_base': 'python_build_stim'}},
 )
diff --git a/src/pymatching/matching.py b/src/pymatching/matching.py
index b0cfbe08..b8aaa928 100644
--- a/src/pymatching/matching.py
+++ b/src/pymatching/matching.py
@@ -17,7 +17,7 @@
 
 import numpy as np
 import networkx as nx
-import retworkx as rx
+import rustworkx as rx
 import pymatching
 from scipy.sparse import csc_matrix, spmatrix
 import matplotlib.cbook
@@ -33,7 +33,7 @@ class Matching:
     A class for constructing matching graphs and decoding using the minimum-weight perfect matching decoder.
     The matching graph can be constructed using the `Matching.add_edge` and `Matching.add_boundary_edge`
     methods. Alternatively, it can be loaded from a parity check matrix (a `scipy.sparse` matrix or `numpy.ndarray`
-    with one or two non-zero elements in each column), a NetworkX or retworkx graph, or from
+    with one or two non-zero elements in each column), a NetworkX or rustworkx graph, or from
     a `stim.DetectorErrorModel`.
     """
 
@@ -54,8 +54,8 @@ def __init__(self,
         graph : `scipy.spmatrix` or `numpy.ndarray` or `networkx.Graph` or `stim.DetectorErrorModel`, optional
             The matching graph to be decoded with minimum-weight perfect
             matching, given either as a binary parity check matrix (scipy sparse
-            matrix or numpy.ndarray), a NetworkX or retworkx graph, or a Stim DetectorErrorModel.
-            Each edge in the NetworkX or retworkx graph can have optional
+            matrix or numpy.ndarray), a NetworkX or rustworkx graph, or a Stim DetectorErrorModel.
+            Each edge in the NetworkX or rustworkx graph can have optional
             attributes ``fault_ids``, ``weight`` and ``error_probability``.
             ``fault_ids`` should be an int or a set of ints.
             Each fault id corresponds to a self-inverse fault that is flipped when the
@@ -139,7 +139,7 @@ def __init__(self,
         if isinstance(graph, nx.Graph):
             self.load_from_networkx(graph)
         elif isinstance(graph, rx.PyGraph):
-            self.load_from_retworkx(graph)
+            self.load_from_rustworkx(graph)
         elif type(graph).__name__ == "DetectorErrorModel":
             self._load_from_detector_error_model(graph)
         else:
@@ -147,7 +147,7 @@ def __init__(self,
                 graph = csc_matrix(graph)
             except TypeError:
                 raise TypeError("The type of the input graph is not recognised. `graph` must be "
-                                "a scipy.sparse or numpy matrix, networkx or retworkx graph, or "
+                                "a scipy.sparse or numpy matrix, networkx or rustworkx graph, or "
                                 "stim.DetectorErrorModel.")
             self.load_from_check_matrix(graph, weights, error_probabilities,
                                         repetitions, timelike_weights, measurement_error_probabilities,
@@ -1476,12 +1476,21 @@ def load_from_networkx(self, graph: nx.Graph, *, min_num_fault_ids: int = None)
 
     def load_from_retworkx(self, graph: rx.PyGraph, *, min_num_fault_ids: int = None) -> None:
         r"""
-        Load a matching graph from a retworkX graph
+        Load a matching graph from a retworkX graph. This method is deprecated since the retworkx package has been
+        renamed to rustworkx. Please use ``pymatching.Matching.load_from_rustworkx`` instead.
+        """
+        warnings.warn("`pymatching.Matching.load_from_retworkx` is now deprecated since the `retworkx` library has been "
+                      "renamed to `rustworkx`. Please use `pymatching.Matching.load_from_rustworkx` instead.", DeprecationWarning, stacklevel=2)
+        self.load_from_rustworkx(graph=graph, min_num_fault_ids=min_num_fault_ids)
+
+    def load_from_rustworkx(self, graph: rx.PyGraph, *, min_num_fault_ids: int = None) -> None:
+        r"""
+        Load a matching graph from a rustworkX graph
 
         Parameters
         ----------
-        graph : retworkx.PyGraph
-            Each edge in the retworkx graph can have dictionary payload with keys
+        graph : rustworkx.PyGraph
+            Each edge in the rustworkx graph can have dictionary payload with keys
             ``fault_ids``, ``weight`` and ``error_probability``. ``fault_ids`` should be
             an int or a set of ints. Each fault id corresponds to a self-inverse fault
             that is flipped when the corresponding edge is flipped. These self-inverse
@@ -1504,7 +1513,7 @@ def load_from_retworkx(self, graph: rx.PyGraph, *, min_num_fault_ids: int = None
         Examples
         --------
         >>> import pymatching
-        >>> import retworkx as rx
+        >>> import rustworkx as rx
         >>> import math
         >>> g = rx.PyGraph()
         >>> matching = g.add_nodes_from([{} for _ in range(3)])
@@ -1517,7 +1526,7 @@ def load_from_retworkx(self, graph: rx.PyGraph, *, min_num_fault_ids: int = None
         <pymatching.Matching object with 1 detector, 2 boundary nodes, and 2 edges>
         """
         if not isinstance(graph, rx.PyGraph):
-            raise TypeError("G must be a retworkx graph")
+            raise TypeError("G must be a rustworkx graph")
         boundary = {i for i in graph.node_indices() if graph[i].get("is_boundary", False)}
         num_nodes = len(graph)
         num_fault_ids = 0 if min_num_fault_ids is None else min_num_fault_ids
@@ -1546,7 +1555,7 @@ def load_from_retworkx(self, graph: rx.PyGraph, *, min_num_fault_ids: int = None
                         " (or convertible to a set), not {}".format(fault_ids))
             weight = attr.get("weight", 1)  # Default weight is 1 if not provided
             e_prob = attr.get("error_probability", -1)
-            # Note: retworkx graphs do not support parallel edges (merge strategy is redundant)
+            # Note: rustworkx graphs do not support parallel edges (merge strategy is redundant)
             g.add_edge(u, v, fault_ids, weight, e_prob, merge_strategy="smallest-weight")
         self._matching_graph = g
 
@@ -1579,16 +1588,26 @@ def to_networkx(self) -> nx.Graph:
         return graph
 
     def to_retworkx(self) -> rx.PyGraph:
-        """Convert to retworkx graph
-        Returns a retworkx graph object corresponding to the matching graph. Each edge
+        """Deprecated, use ``pymatching.Matching.to_rustworkx`` instead (since the `retworkx` package has been renamed to `rustworkx`).
+        This method just calls ``pymatching.Matching.to_rustworkx`` and returns a ``rustworkx.PyGraph``, which is now just the preferred name for
+         ``retworkx.PyGraph``. Note that in the future, only the `rustworkx` package name will be supported,
+         see: https://pypi.org/project/retworkx/.
+        """
+        warnings.warn("`pymatching.Matching.to_retworkx` is now deprecated since the `retworkx` library has been "
+                      "renamed to `rustworkx`. Please use `pymatching.Matching.to_rustworkx` instead.", DeprecationWarning, stacklevel=2)
+        return self.to_rustworkx()
+
+    def to_rustworkx(self) -> rx.PyGraph:
+        """Convert to rustworkx graph
+        Returns a rustworkx graph object corresponding to the matching graph. Each edge
         payload is a ``dict`` with keys `fault_ids`, `weight` and `error_probability` and
         each node has a ``dict`` payload with the key ``is_boundary`` and the value is
         a boolean.
 
         Returns
         -------
-        retworkx.PyGraph
-            retworkx graph corresponding to the matching graph
+        rustworkx.PyGraph
+            rustworkx graph corresponding to the matching graph
         """
         graph = rx.PyGraph(multigraph=False)
         num_nodes = self.num_nodes


Test patch:
diff --git a/tests/matching/load_from_retworkx_test.py b/tests/matching/load_from_rustworkx_test.py
similarity index 92%
rename from tests/matching/load_from_retworkx_test.py
rename to tests/matching/load_from_rustworkx_test.py
index 68e8794d..85afa946 100644
--- a/tests/matching/load_from_retworkx_test.py
+++ b/tests/matching/load_from_rustworkx_test.py
@@ -13,14 +13,14 @@
 # limitations under the License.
 
 import numpy as np
-import retworkx as rx
+import rustworkx as rx
 import pytest
 
 from pymatching import Matching
 from pymatching._cpp_pymatching import MatchingGraph
 
 
-def test_boundary_from_retworkx():
+def test_boundary_from_rustworkx():
     g = rx.PyGraph()
     g.add_nodes_from([{} for _ in range(5)])
     g.add_edge(4, 0, dict(fault_ids=0))
@@ -37,7 +37,7 @@ def test_boundary_from_retworkx():
     assert np.array_equal(m.decode(np.array([0, 0, 1, 0])), np.array([0, 0, 0, 1, 1]))
 
 
-def test_boundaries_from_retworkx():
+def test_boundaries_from_rustworkx():
     g = rx.PyGraph()
     g.add_nodes_from([{} for _ in range(6)])
     g.add_edge(0, 1, dict(fault_ids=0))
@@ -56,7 +56,7 @@ def test_boundaries_from_retworkx():
     assert np.array_equal(m.decode(np.array([0, 0, 0, 1, 0])), np.array([0, 0, 0, 1, 1]))
 
 
-def test_unweighted_stabiliser_graph_from_retworkx():
+def test_unweighted_stabiliser_graph_from_rustworkx():
     w = rx.PyGraph()
     w.add_nodes_from([{} for _ in range(6)])
     w.add_edge(0, 1, dict(fault_ids=0, weight=7.0))
@@ -89,7 +89,7 @@ def test_unweighted_stabiliser_graph_from_retworkx():
     )
 
 
-def test_mwpm_from_retworkx():
+def test_mwpm_from_rustworkx():
     g = rx.PyGraph()
     g.add_nodes_from([{} for _ in range(3)])
     g.add_edge(0, 1, dict(fault_ids=0))
@@ -121,7 +121,7 @@ def test_mwpm_from_retworkx():
     assert (m.num_fault_ids == 0)
 
 
-def test_matching_edges_from_retworkx():
+def test_matching_edges_from_rustworkx():
     g = rx.PyGraph()
     g.add_nodes_from([{} for _ in range(4)])
     g.add_edge(0, 1, dict(fault_ids=0, weight=1.1, error_probability=0.1))
@@ -142,7 +142,7 @@ def test_matching_edges_from_retworkx():
     assert es == expected_edges
 
 
-def test_qubit_id_accepted_via_retworkx():
+def test_qubit_id_accepted_via_rustworkx():
     g = rx.PyGraph()
     g.add_nodes_from([{} for _ in range(4)])
     g.add_edge(0, 1, dict(qubit_id=0, weight=1.1, error_probability=0.1))
@@ -162,29 +162,29 @@ def test_qubit_id_accepted_via_retworkx():
     assert es == expected_edges
 
 
-def test_load_from_retworkx_raises_value_error_if_qubit_id_and_fault_ids_both_supplied():
+def test_load_from_rustworkx_raises_value_error_if_qubit_id_and_fault_ids_both_supplied():
     with pytest.raises(ValueError):
         g = rx.PyGraph()
         g.add_nodes_from([{} for _ in range(3)])
         g.add_edge(0, 1, dict(qubit_id=0, fault_ids=0))
         g.add_edge(1, 2, dict(qubit_id=1, fault_ids=1))
         m = Matching()
-        m.load_from_retworkx(g)
+        m.load_from_rustworkx(g)
 
 
-def test_load_from_retworkx_type_errors_raised():
+def test_load_from_rustworkx_type_errors_raised():
     with pytest.raises(TypeError):
         m = Matching()
-        m.load_from_retworkx("A")
+        m.load_from_rustworkx("A")
     with pytest.raises(TypeError):
         g = rx.PyGraph()
         g.add_nodes_from([{} for _ in range(2)])
         g.add_edge(0, 1, dict(fault_ids={0, "a"}))
         m = Matching()
-        m.load_from_retworkx(g)
+        m.load_from_rustworkx(g)
     with pytest.raises(TypeError):
         g = rx.PyGraph()
         g.add_nodes_from([{} for _ in range(2)])
         g.add_edge(0, 1, dict(fault_ids=[[0], [2]]))
         m = Matching()
-        m.load_from_retworkx(g)
+        m.load_from_rustworkx(g)
diff --git a/tests/matching/output_graph_test.py b/tests/matching/output_graph_test.py
index b85e9acf..33a30b95 100644
--- a/tests/matching/output_graph_test.py
+++ b/tests/matching/output_graph_test.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 import networkx as nx
-import retworkx as rx
+import rustworkx as rx
 
 from pymatching import Matching
 
@@ -59,7 +59,7 @@ def test_matching_to_networkx():
     assert list(g.nodes(data=True)) == [(0, {"is_boundary": False}), (1, {"is_boundary": False})]
 
 
-def test_matching_to_retworkx():
+def test_matching_to_rustworkx():
     g = rx.PyGraph()
     g.add_nodes_from([{} for _ in range(4)])
     g.add_edge(0, 1, dict(fault_ids={0}, weight=1.1, error_probability=0.1))
@@ -76,7 +76,7 @@ def test_matching_to_retworkx():
     g[1]['is_boundary'] = False
     g[2]['is_boundary'] = False
 
-    g2 = m.to_retworkx()
+    g2 = m.to_rustworkx()
 
     assert g.node_indices() == g2.node_indices()
     gedges = [({s, t}, d) for (s, t, d) in g.weighted_edge_list()]
@@ -87,7 +87,7 @@ def test_matching_to_retworkx():
     m.add_boundary_edge(0, weight=2)
     m.add_edge(0, 1, weight=3)
     m.add_edge(1, 2, weight=4)
-    g = m.to_retworkx()
+    g = m.to_rustworkx()
     es = list(g.weighted_edge_list())
     assert es == [(0, 3, {"weight": 2.0, "error_probability": -1, "fault_ids": set()}),
                   (0, 1, {"weight": 3.0, "error_probability": -1, "fault_ids": set()}),
@@ -97,7 +97,7 @@ def test_matching_to_retworkx():
 
     m = Matching()
     m.add_edge(0, 1)
-    g = m.to_retworkx()
+    g = m.to_rustworkx()
     assert list(g.weighted_edge_list()) == [(0, 1, {"weight": 1.0, "error_probability": -1, "fault_ids": set()})]
     assert list(g.nodes()) == [{"is_boundary": False}, {"is_boundary": False}]
 
diff --git a/tests/matching/properties_test.py b/tests/matching/properties_test.py
index e8b81ebd..715dd24a 100644
--- a/tests/matching/properties_test.py
+++ b/tests/matching/properties_test.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 import networkx as nx
-import retworkx as rx
+import rustworkx as rx
 
 from pymatching.matching import Matching
 
@@ -51,12 +51,12 @@ def test_set_min_num_fault_ids():
     g = rx.PyGraph()
     g.add_nodes_from([{} for _ in range(2)])
     g.add_edge(0, 1, dict(fault_ids=3))
-    m.load_from_retworkx(g)
+    m.load_from_rustworkx(g)
     assert m.num_fault_ids == 4
     assert m.decode([1, 1]).shape[0] == 4
-    m.load_from_retworkx(g, min_num_fault_ids=7)
+    m.load_from_rustworkx(g, min_num_fault_ids=7)
     assert m.num_fault_ids == 7
     assert m.decode([1, 1]).shape[0] == 7
-    m.load_from_retworkx(g, min_num_fault_ids=2)
+    m.load_from_rustworkx(g, min_num_fault_ids=2)
     assert m.num_fault_ids == 4
     assert m.decode([1, 1]).shape[0] == 4
diff --git a/tests/matching/retworkx_test.py b/tests/matching/retworkx_test.py
new file mode 100644
index 00000000..06b1cc59
--- /dev/null
+++ b/tests/matching/retworkx_test.py
@@ -0,0 +1,23 @@
+import pytest
+
+from pymatching import Matching
+import rustworkx as rx
+
+
+def test_load_from_retworkx_deprecated():
+    with pytest.deprecated_call():
+        g = rx.PyGraph()
+        g.add_nodes_from([{} for _ in range(3)])
+        g.add_edge(0, 1, dict(fault_ids=0))
+        g.add_edge(0, 2, dict(fault_ids=1))
+        g.add_edge(1, 2, dict(fault_ids=2))
+        m = Matching()
+        m.load_from_retworkx(g)
+
+
+def test_to_retworkx_deprecated():
+    with pytest.deprecated_call():
+        m = Matching()
+        m.add_edge(0, 1, {0})
+        m.add_edge(1, 2, {1})
+        m.to_retworkx()