From 0b917f860a0c1ccbc37dc893465128dc88aa94fe Mon Sep 17 00:00:00 2001
From: Jochem Nelen <78348388+Jnelen@users.noreply.github.com>
Date: Thu, 7 Mar 2024 00:05:13 +0100
Subject: [PATCH 1/8] Add functionality to select backend (#107)

---
 .github/workflows/pytest.yml                  |   4 +-
 .pre-commit-config.yaml                       |   1 +
 CHANGELOG.md                                  |  10 +-
 CITATION.cff                                  |   3 +
 .../conda-envs/spyrmsd-test-rdkit-all.yaml    |  28 ++++
 spyrmsd/__init__.py                           |   6 +
 spyrmsd/graph.py                              | 146 ++++++++++++++----
 spyrmsd/molecule.py                           |  18 ++-
 tests/test_graph.py                           |  27 ++++
 tests/test_molecule.py                        |  35 ++++-
 10 files changed, 235 insertions(+), 43 deletions(-)
 create mode 100644 devtools/conda-envs/spyrmsd-test-rdkit-all.yaml

diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 74c963c..10caace 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -26,10 +26,12 @@ jobs:
         os: [macOS-latest, ubuntu-latest, windows-latest]
         python-version: ["3.9", "3.10", "3.11", "3.12"]
         chemlib: [obabel, rdkit]
-        graphlib: [nx, gt]
+        graphlib: [nx, gt, all]
         exclude:
         # graph-tools does not work on Windows
         - {os: "windows-latest", graphlib: "gt"}
+        - {os: "windows-latest", graphlib: "all"}
+        - {graphlib: "all", chemlib: "obabel"}
         include:
         - {os: "macOS-14", graphlib: "gt", chemlib: "obabel", python-version: "3.12"}
         - {os: "macOS-14", graphlib: "nx", chemlib: "rdkit", python-version: "3.12"}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 176a260..6da5c7c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -25,6 +25,7 @@ repos:
     rev: 5.12.0
     hooks:
     -   id: isort
+        args: ["--profile", "black"]
 -   repo: https://github.com/pre-commit/mirrors-mypy
     rev: v1.5.1
     hooks:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bf0dc7..1894a41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,15 @@
 ## Version X.Y.Z
 
 Date:            XX/YY/ZZZZ
-Contributors:    @RMeli, @takluyver
+Contributors:    @RMeli, @takluyver, @Jnelen
+
+### Added
+
+* Functionality to manually select the backend [PR  #107 | @Jnelen]
+
+### Changed
+
+* Molecular graphs are now cached per backend using a dictionary [PR  #107 | @Jnelen]
 
 ### Fixed
 
diff --git a/CITATION.cff b/CITATION.cff
index d1d1a7c..1006b56 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -4,6 +4,9 @@ authors:
 - family-names: "Meli"
   given-names: "Rocco"
   orcid: "https://orcid.org/0000-0002-2845-3410"
+- family-names: "Nelen"
+  given-names: "Jochem"
+  orcid: "https://orcid.org/0000-0002-9970-4950"
 title: "spyrmsd"
 version: 0.6.0
 doi: 10.5281/zenodo.3631876
diff --git a/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml b/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
new file mode 100644
index 0000000..9212381
--- /dev/null
+++ b/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
@@ -0,0 +1,28 @@
+name: spyrmsd
+channels:
+  - conda-forge
+  - rdkit
+dependencies:
+  # Base
+  - python
+  - setuptools
+
+  # Maths
+  - numpy
+  - scipy
+  - graph-tool
+  - networkx>=2
+
+  # Chemistry
+  - rdkit
+
+  # Testing
+  - pytest
+  - pytest-cov
+  - pytest-benchmark
+
+  # Dev
+  - mypy
+  - flake8
+  - black
+  - codecov
diff --git a/spyrmsd/__init__.py b/spyrmsd/__init__.py
index b38f4f9..36cb386 100644
--- a/spyrmsd/__init__.py
+++ b/spyrmsd/__init__.py
@@ -4,6 +4,12 @@
 
 from .due import Doi, due
 
+# Make the backend related functions available from base spyrmsd
+# Add noqa to avoid flake8 error
+from .graph import _available_backends as available_backends  # noqa: F401
+from .graph import _get_backend as get_backend  # noqa: F401
+from .graph import _set_backend as set_backend  # noqa: F401
+
 __version__ = "0.7.0-dev"
 
 # This will print latest Zenodo version
diff --git a/spyrmsd/graph.py b/spyrmsd/graph.py
index 0235a25..3d27a16 100644
--- a/spyrmsd/graph.py
+++ b/spyrmsd/graph.py
@@ -1,42 +1,124 @@
+import warnings
+
+import numpy as np
+
+from spyrmsd import constants
+
+_available_backends = []
+_current_backend = None
+
+## Backend aliases
+_graph_tool_aliases = ["graph_tool", "graphtool", "graph-tool", "graph tool", "gt"]
+_networkx_aliases = ["networkx", "nx"]
+
+## Construct the alias dictionary
+_alias_to_backend = {}
+for alias in _graph_tool_aliases:
+    _alias_to_backend[alias.lower()] = "graph-tool"
+for alias in _networkx_aliases:
+    _alias_to_backend[alias.lower()] = "networkx"
+
+
+def _dummy(*args, **kwargs):
+    """
+    Dummy function for backend not set.
+    """
+    raise NotImplementedError("No backend is set.")
+
+
+## Assigning the properties/methods associated with a backend to a temporary dummy function
+cycle = _dummy
+graph_from_adjacency_matrix = _dummy
+lattice = _dummy
+match_graphs = _dummy
+num_edges = _dummy
+num_vertices = _dummy
+vertex_property = _dummy
+
 try:
+    from spyrmsd.graphs.gt import cycle as gt_cycle
     from spyrmsd.graphs.gt import (
-        cycle,
-        graph_from_adjacency_matrix,
-        lattice,
-        match_graphs,
-        num_edges,
-        num_vertices,
-        vertex_property,
+        graph_from_adjacency_matrix as gt_graph_from_adjacency_matrix,
     )
+    from spyrmsd.graphs.gt import lattice as gt_lattice
+    from spyrmsd.graphs.gt import match_graphs as gt_match_graphs
+    from spyrmsd.graphs.gt import num_edges as gt_num_edges
+    from spyrmsd.graphs.gt import num_vertices as gt_num_vertices
+    from spyrmsd.graphs.gt import vertex_property as gt_vertex_property
 
+    _available_backends.append("graph-tool")
 except ImportError:
-    try:
-        from spyrmsd.graphs.nx import (
-            cycle,
-            graph_from_adjacency_matrix,
-            lattice,
-            match_graphs,
-            num_edges,
-            num_vertices,
-            vertex_property,
-        )
-    except ImportError:
-        raise ImportError("graph_tool or NetworkX libraries not found.")
-
-__all__ = [
-    "graph_from_adjacency_matrix",
-    "match_graphs",
-    "vertex_property",
-    "num_vertices",
-    "num_edges",
-    "lattice",
-    "cycle",
-    "adjacency_matrix_from_atomic_coordinates",
-]
+    warnings.warn("The graph-tool backend does not seem to be installed.")
 
-import numpy as np
+try:
+    from spyrmsd.graphs.nx import cycle as nx_cycle
+    from spyrmsd.graphs.nx import (
+        graph_from_adjacency_matrix as nx_graph_from_adjacency_matrix,
+    )
+    from spyrmsd.graphs.nx import lattice as nx_lattice
+    from spyrmsd.graphs.nx import match_graphs as nx_match_graphs
+    from spyrmsd.graphs.nx import num_edges as nx_num_edges
+    from spyrmsd.graphs.nx import num_vertices as nx_num_vertices
+    from spyrmsd.graphs.nx import vertex_property as nx_vertex_property
 
-from spyrmsd import constants
+    _available_backends.append("networkx")
+except ImportError:
+    warnings.warn("The networkx backend does not seem to be installed.")
+
+
+def _validate_backend(backend):
+    standardized_backend = _alias_to_backend.get(backend.lower())
+    if standardized_backend is None:
+        raise ValueError(f"The {backend} backend is not recognized or supported")
+    if standardized_backend not in _available_backends:
+        raise ImportError(f"The {backend} backend doesn't seem to be installed")
+    return standardized_backend
+
+
+def _set_backend(backend):
+    global _current_backend
+    backend = _validate_backend(backend)
+
+    ## Check if we actually need to switch backends
+    if backend == _current_backend:
+        warnings.warn(f"The backend is already {backend}.")
+        return
+
+    global cycle, graph_from_adjacency_matrix, lattice, match_graphs, num_edges, num_vertices, vertex_property
+
+    if backend == "graph-tool":
+        cycle = gt_cycle
+        graph_from_adjacency_matrix = gt_graph_from_adjacency_matrix
+        lattice = gt_lattice
+        match_graphs = gt_match_graphs
+        num_edges = gt_num_edges
+        num_vertices = gt_num_vertices
+        vertex_property = gt_vertex_property
+
+    elif backend == "networkx":
+        cycle = nx_cycle
+        graph_from_adjacency_matrix = nx_graph_from_adjacency_matrix
+        lattice = nx_lattice
+        match_graphs = nx_match_graphs
+        num_edges = nx_num_edges
+        num_vertices = nx_num_vertices
+        vertex_property = nx_vertex_property
+
+    _current_backend = backend
+
+
+if len(_available_backends) == 0:
+    raise ImportError(
+        "No valid backends found. Please ensure that either graph-tool or NetworkX are installed."
+    )
+else:
+    if _current_backend is None:
+        ## Set the backend to the first available (preferred) backend
+        _set_backend(backend=_available_backends[0])
+
+
+def _get_backend():
+    return _current_backend
 
 
 def adjacency_matrix_from_atomic_coordinates(
diff --git a/spyrmsd/molecule.py b/spyrmsd/molecule.py
index 3d593a0..b2ab87c 100644
--- a/spyrmsd/molecule.py
+++ b/spyrmsd/molecule.py
@@ -1,5 +1,5 @@
 import warnings
-from typing import List, Optional, Union
+from typing import Dict, List, Optional, Union
 
 import numpy as np
 
@@ -50,7 +50,7 @@ def __init__(
             self.adjacency_matrix: np.ndarray = np.asarray(adjacency_matrix, dtype=int)
 
         # Molecular graph
-        self.G = None
+        self.G: Dict[str, object] = {}
 
         self.masses: Optional[List[float]] = None
 
@@ -182,7 +182,7 @@ def strip(self) -> None:
                 self.adjacency_matrix = self.adjacency_matrix[np.ix_(idx, idx)]
 
             # Reset molecular graph when stripping
-            self.G = None
+            self.G = {}
 
             self.stripped = True
 
@@ -200,11 +200,13 @@ def to_graph(self):
         If the molecule does not have an associated adjacency matrix, a simple
         bond perception is used.
 
-        The molecular graph is cached.
+        The molecular graph is cached per backend.
         """
-        if self.G is None:
+        _current_backend = graph._current_backend
+
+        if _current_backend not in self.G.keys():
             try:
-                self.G = graph.graph_from_adjacency_matrix(
+                self.G[_current_backend] = graph.graph_from_adjacency_matrix(
                     self.adjacency_matrix, self.atomicnums
                 )
             except AttributeError:
@@ -218,11 +220,11 @@ def to_graph(self):
                     self.atomicnums, self.coordinates
                 )
 
-                self.G = graph.graph_from_adjacency_matrix(
+                self.G[_current_backend] = graph.graph_from_adjacency_matrix(
                     self.adjacency_matrix, self.atomicnums
                 )
 
-        return self.G
+        return self.G[_current_backend]
 
     def __len__(self) -> int:
         """
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 029e604..88c9643 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -1,6 +1,7 @@
 import numpy as np
 import pytest
 
+import spyrmsd
 from spyrmsd import constants, graph, io, molecule
 from spyrmsd.exceptions import NonIsomorphicGraphs
 from spyrmsd.graphs import _common as gc
@@ -153,3 +154,29 @@ def test_build_graph_node_features_unsupported() -> None:
 
     with pytest.raises(ValueError, match="Unsupported property type:"):
         _ = graph.graph_from_adjacency_matrix(A, property)
+
+
+@pytest.mark.skipif(
+    len(spyrmsd.available_backends) < 2,
+    reason="Not all of the required backends are installed",
+)
+def test_set_backend() -> None:
+    import graph_tool as gt
+    import networkx as nx
+
+    A = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]])
+
+    spyrmsd.set_backend("networkx")
+    assert spyrmsd.get_backend() == "networkx"
+
+    Gnx = graph.graph_from_adjacency_matrix(A)
+    assert isinstance(Gnx, nx.Graph)
+
+    spyrmsd.set_backend("graph-tool")
+    assert spyrmsd.get_backend() == "graph-tool"
+
+    Ggt = graph.graph_from_adjacency_matrix(A)
+    assert isinstance(Ggt, gt.Graph)
+
+    with pytest.raises(ValueError, match="backend is not recognized or supported"):
+        spyrmsd.set_backend("unknown")
diff --git a/tests/test_molecule.py b/tests/test_molecule.py
index 43efda4..450f453 100644
--- a/tests/test_molecule.py
+++ b/tests/test_molecule.py
@@ -6,6 +6,7 @@
 import numpy as np
 import pytest
 
+import spyrmsd
 from spyrmsd import constants, graph, io, molecule, utils
 from tests import molecules
 
@@ -167,7 +168,7 @@ def test_graph_from_atomic_coordinates_perception(
     m = copy.deepcopy(mol)
 
     delattr(m, "adjacency_matrix")
-    m.G = None
+    m.G = {}
 
     with pytest.warns(UserWarning):
         # Uses automatic bond perception
@@ -236,3 +237,35 @@ def test_from_rdmol(adjacency):
             with pytest.raises(AttributeError):
                 # No adjacency_matrix attribute
                 mol.adjacency_matrix
+
+
+@pytest.mark.skipif(
+    len(spyrmsd.available_backends) < 2,
+    reason="Not all of the required backends are installed",
+)
+@pytest.mark.parametrize(
+    "mol", [(molecules.benzene), (molecules.ethanol), (molecules.dialanine)]
+)
+def test_molecule_graph_cache(mol) -> None:
+    import graph_tool as gt
+    import networkx as nx
+
+    ## Graph cache persists from previous tests, manually reset them
+    mol.G = {}
+    spyrmsd.set_backend("networkx")
+    mol.to_graph()
+
+    assert isinstance(mol.G["networkx"], nx.Graph)
+    assert "graph-tool" not in mol.G.keys()
+
+    spyrmsd.set_backend("graph-tool")
+    mol.to_graph()
+
+    ## Make sure both backends (still) have a cache
+    assert isinstance(mol.G["networkx"], nx.Graph)
+    assert isinstance(mol.G["graph-tool"], gt.Graph)
+
+    ## Strip the molecule to ensure the cache is reset
+    mol.strip()
+
+    assert len(mol.G.items()) == 0

From fbff4a38ebc94813945ba8c83640303c6f754785 Mon Sep 17 00:00:00 2001
From: Rocco Meli <r.meli@bluemail.ch>
Date: Thu, 7 Mar 2024 23:51:54 +0100
Subject: [PATCH 2/8] Documentation and CLI for backend selection (#112)

---
 CHANGELOG.md                         |  17 ++--
 README.md                            |  38 ++++----
 docs/source/tutorials/tutorial.ipynb | 103 ++++++++++++++++++++-
 docs/source/tutorials/tutorial.rst   |  62 ++++++++++++-
 spyrmsd/__main__.py                  |  14 +++
 spyrmsd/graph.py                     | 128 ++++++++++++++++++---------
 tests/test_graph.py                  |  13 +--
 tests/test_molecule.py               |   7 +-
 8 files changed, 294 insertions(+), 88 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1894a41..f87aa4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,30 +10,25 @@ Contributors:    @RMeli, @takluyver, @Jnelen
 
 ### Added
 
+* Functionality to manually select the backend from CLI [PR #108 | @RMeli]
 * Functionality to manually select the backend [PR  #107 | @Jnelen]
+* Python `3.12` to CI [PR  #102 | @RMeli]
+* macOS M1 (`macoOS-14`) to CI [PR  #102 | @RMeli]
 
 ### Changed
 
-* Molecular graphs are now cached per backend using a dictionary [PR  #107 | @Jnelen]
+* Molecular graphs cache to cache by backend [PR  #107 | @Jnelen]
+* Python build system to use flit_core directly [PR #103 | @takluyver]
+* Minimum version of Python to `3.9` (to reduce CI matrix) [PR  #102 | @RMeli]
 
 ### Fixed
 
 * Failing tests with `pytest=8.0.0` [PR #101 | @RMeli]
 
-### Changed
-
-* Updated Python build system to use flit_core directly [PR #103 | @takluyver]
-* Minimum version of Python to `3.9` (to reduce CI matrix) [PR  #102 | @RMeli]
-
 ### Improved
 
 * Messages for `NotImplementedError` exceptions [PR #90 | @RMeli]
 
-### Added
-
-* Python `3.12` to CI [PR  #102 | @RMeli]
-* macOS M1 (`macoOS-14`) to CI [PR  #102 | @RMeli]
-
 ### Removed
 
 * `.gitattributes` and `.lgtm.yaml` stale files
diff --git a/README.md b/README.md
index 701a1e6..8a926bb 100644
--- a/README.md
+++ b/README.md
@@ -89,27 +89,12 @@ Additionally, one of the following packages is required to use `spyrmsd` as a st
 
 ### Standalone Tool
 
+`spyrmsd` provides a convenient CLI tool. See `spyrmsd`'s `--help` for the usage:
+
 ```bash
 python -m spyrmsd -h
 ```
 
-```text
-usage: python -m spyrmsd [-h] [-m] [-c] [--hydrogens] [-n] reference molecules [molecules ...]
-
-Symmetry-corrected RMSD calculations in Python.
-
-positional arguments:
-  reference       Reference file
-  molecules       Input file(s)
-
-optional arguments:
-  -h, --help      show this help message and exit
-  -m, --minimize  Minimize (fit)
-  -c, --center    Center molecules at origin
-  --hydrogens     Keep hydrogen atoms
-  -n, --nosymm    No graph isomorphism
-```
-
 ### Module
 
 ```python
@@ -159,6 +144,25 @@ def symmrmsd(
 > [!NOTE]
 > Atomic properties (`aprops`) can be any Python object when using [NetworkX](https://networkx.github.io/), or integers, floats, or strings when using [graph-tool](https://graph-tool.skewed.de/).
 
+#### Select Backend
+
+`spyrmsd` supports both [NetworkX](https://networkx.github.io/) and [graph-tool](https://graph-tool.skewed.de/) for the calculation of graph isomorphisms. You can check which backend is being used with
+
+```python
+spyrmsd.get_backend()
+```
+
+You can also manually select your preferred backend with
+
+```python
+spyrmsd.set_backend("networkx")
+# spyrmsd uses NetworkX
+spyrmsd.set_backend("graph_tool")
+# spyrmsd uses graph_tool
+```
+
+The available backends (which depend on the installed dependencies) are stored in `spyrmsd.available_backends`.
+
 ## Development
 
 To ensure code quality and consistency the following tools are used during development:
diff --git a/docs/source/tutorials/tutorial.ipynb b/docs/source/tutorials/tutorial.ipynb
index 5b626ea..1cf200e 100644
--- a/docs/source/tutorials/tutorial.ipynb
+++ b/docs/source/tutorials/tutorial.ipynb
@@ -14,6 +14,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
+    "import spyrmsd\n",
     "from spyrmsd import io, rmsd"
    ]
   },
@@ -107,8 +108,8 @@
      "output_type": "stream",
      "text": [
       "<frozen importlib._bootstrap>:241: RuntimeWarning: to-Python converter for std::__1::pair<double, double> already registered; second conversion method ignored.\n",
-      "[18:44:03] Molecule does not have explicit Hs. Consider calling AddHs()\n",
-      "[18:44:03] Molecule does not have explicit Hs. Consider calling AddHs()\n"
+      "[21:58:01] Molecule does not have explicit Hs. Consider calling AddHs()\n",
+      "[21:58:01] Molecule does not have explicit Hs. Consider calling AddHs()\n"
      ]
     },
     {
@@ -289,6 +290,102 @@
     "\n",
     "print(RMSD)"
    ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Change Backend"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "`spyrmsd` supports multiple backends. You see which backends are available by looking at the `available_backends` attribute:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['graph_tool', 'networkx']"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "spyrmsd.available_backends"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The available backends are a subset of the supported backends. Only the backends that are installed will be available."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can check the current backend with"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'graph_tool'"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "spyrmsd.get_backend()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can switch the backend using"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'networkx'"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "spyrmsd.set_backend(\"networkx\")\n",
+    "spyrmsd.get_backend()"
+   ]
   }
  ],
  "metadata": {
@@ -307,7 +404,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.4"
+   "version": "3.11.8"
   }
  },
  "nbformat": 4,
diff --git a/docs/source/tutorials/tutorial.rst b/docs/source/tutorials/tutorial.rst
index 10dcfb6..0c37d23 100644
--- a/docs/source/tutorials/tutorial.rst
+++ b/docs/source/tutorials/tutorial.rst
@@ -3,6 +3,7 @@ Tutorial
 
 .. code:: ipython3
 
+    import spyrmsd
     from spyrmsd import io, rmsd
 
 OpenBabel or RDKit
@@ -68,8 +69,8 @@ constructors.
 .. parsed-literal::
 
     <frozen importlib._bootstrap>:241: RuntimeWarning: to-Python converter for std::__1::pair<double, double> already registered; second conversion method ignored.
-    [18:44:03] Molecule does not have explicit Hs. Consider calling AddHs()
-    [18:44:03] Molecule does not have explicit Hs. Consider calling AddHs()
+    [21:58:01] Molecule does not have explicit Hs. Consider calling AddHs()
+    [21:58:01] Molecule does not have explicit Hs. Consider calling AddHs()
 
 
 
@@ -95,7 +96,7 @@ Hydrogen atoms can be removed with the ``strip()`` function:
         mol.strip()
 
 Symmetry-Corrected RMSD
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 
 ``spyrmsd`` only needs atomic coordinates, atomic number and the
 molecular adjacency matrix to compute the standard RMSD with
@@ -137,7 +138,7 @@ reference molecule and all other molecules:
 
 
 Minimum RMSD
-~~~~~~~~~~~~
+------------
 
 We can also compute the minimum RMSD obtained by superimposing the
 molecular structures:
@@ -160,3 +161,56 @@ molecular structures:
 .. parsed-literal::
 
     [1.2012368667355435, 1.0533413220699535, 1.153253104575529, 1.036542688936588, 0.8407673221224187, 1.1758143217869736, 0.7817315189656655, 1.0933314311267845, 1.0260767175206462, 0.9586369647000478]
+
+
+
+Change Backend
+~~~~~~~~~~~~~~
+
+``spyrmsd`` supports multiple backends. You see which backends are
+available by looking at the ``available_backends`` attribute:
+
+.. code:: ipython3
+
+    spyrmsd.available_backends
+
+
+
+
+.. parsed-literal::
+
+    ['graph_tool', 'networkx']
+
+
+
+The available backends are a subset of the supported backends. Only the
+backends that are installed will be available.
+
+You can check the current backend with
+
+.. code:: ipython3
+
+    spyrmsd.get_backend()
+
+
+
+
+.. parsed-literal::
+
+    'graph_tool'
+
+
+
+You can switch the backend using
+
+.. code:: ipython3
+
+    spyrmsd.set_backend("networkx")
+    spyrmsd.get_backend()
+
+
+
+
+.. parsed-literal::
+
+    'networkx'
diff --git a/spyrmsd/__main__.py b/spyrmsd/__main__.py
index b18dcac..e61563d 100644
--- a/spyrmsd/__main__.py
+++ b/spyrmsd/__main__.py
@@ -6,7 +6,9 @@
     import argparse as ap
     import importlib.util
     import sys
+    import warnings
 
+    import spyrmsd
     from spyrmsd import io
     from spyrmsd.rmsd import rmsdwrapper
 
@@ -25,6 +27,13 @@
     parser.add_argument(
         "-n", "--nosymm", action="store_false", help="No graph isomorphism"
     )
+    parser.add_argument(
+        "-g",
+        "--graph-backend",
+        type=str,
+        default=None,
+        help="Graph library (backend)",
+    )
 
     args = parser.parse_args()
 
@@ -49,6 +58,11 @@
         print("ERROR: Molecule file(s) not found.", file=sys.stderr)
         exit(-1)
 
+    if args.graph_backend is not None:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            spyrmsd.set_backend(args.graph_backend)
+
     # Loop over molecules within fil
     RMSDlist = rmsdwrapper(
         ref,
diff --git a/spyrmsd/graph.py b/spyrmsd/graph.py
index 3d27a16..b788992 100644
--- a/spyrmsd/graph.py
+++ b/spyrmsd/graph.py
@@ -1,32 +1,33 @@
+import importlib.util
 import warnings
 
 import numpy as np
 
 from spyrmsd import constants
 
+_supported_backends = ("graph_tool", "networkx")
+
 _available_backends = []
 _current_backend = None
 
-## Backend aliases
-_graph_tool_aliases = ["graph_tool", "graphtool", "graph-tool", "graph tool", "gt"]
-_networkx_aliases = ["networkx", "nx"]
+_backend_to_alias = {
+    "graph_tool": ["graph_tool", "graphtool", "graph-tool", "graph tool", "gt"],
+    "networkx": ["networkx", "nx"],
+}
 
-## Construct the alias dictionary
 _alias_to_backend = {}
-for alias in _graph_tool_aliases:
-    _alias_to_backend[alias.lower()] = "graph-tool"
-for alias in _networkx_aliases:
-    _alias_to_backend[alias.lower()] = "networkx"
+for backend, aliases in _backend_to_alias.items():
+    for alias in aliases:
+        _alias_to_backend[alias] = backend
 
 
 def _dummy(*args, **kwargs):
     """
     Dummy function for backend not set.
     """
-    raise NotImplementedError("No backend is set.")
+    raise NotImplementedError("No backend is set for spyrmsd.")
 
 
-## Assigning the properties/methods associated with a backend to a temporary dummy function
 cycle = _dummy
 graph_from_adjacency_matrix = _dummy
 lattice = _dummy
@@ -35,58 +36,84 @@ def _dummy(*args, **kwargs):
 num_vertices = _dummy
 vertex_property = _dummy
 
-try:
-    from spyrmsd.graphs.gt import cycle as gt_cycle
-    from spyrmsd.graphs.gt import (
-        graph_from_adjacency_matrix as gt_graph_from_adjacency_matrix,
-    )
-    from spyrmsd.graphs.gt import lattice as gt_lattice
-    from spyrmsd.graphs.gt import match_graphs as gt_match_graphs
-    from spyrmsd.graphs.gt import num_edges as gt_num_edges
-    from spyrmsd.graphs.gt import num_vertices as gt_num_vertices
-    from spyrmsd.graphs.gt import vertex_property as gt_vertex_property
-
-    _available_backends.append("graph-tool")
-except ImportError:
-    warnings.warn("The graph-tool backend does not seem to be installed.")
-
-try:
-    from spyrmsd.graphs.nx import cycle as nx_cycle
-    from spyrmsd.graphs.nx import (
-        graph_from_adjacency_matrix as nx_graph_from_adjacency_matrix,
-    )
-    from spyrmsd.graphs.nx import lattice as nx_lattice
-    from spyrmsd.graphs.nx import match_graphs as nx_match_graphs
-    from spyrmsd.graphs.nx import num_edges as nx_num_edges
-    from spyrmsd.graphs.nx import num_vertices as nx_num_vertices
-    from spyrmsd.graphs.nx import vertex_property as nx_vertex_property
-
-    _available_backends.append("networkx")
-except ImportError:
-    warnings.warn("The networkx backend does not seem to be installed.")
+# Check which supported backend is available
+for backend in _supported_backends:
+    if importlib.util.find_spec(backend) is not None:
+        _available_backends.append(backend)
 
 
 def _validate_backend(backend):
+    """
+    Validate backend.
+
+    Check if a backend is supported and installed (available).
+
+    Parameters
+    ----------
+    backend: str
+        Backend to validate
+    return: str
+        Standardized backend name
+
+    Raises
+    ------
+    ValueError
+        If the backend is not recognized or supported
+    ImportError
+        If the backend is not installed
+
+    Notes
+    -----
+    This function is case-insensitive.
+    """
     standardized_backend = _alias_to_backend.get(backend.lower())
+
     if standardized_backend is None:
         raise ValueError(f"The {backend} backend is not recognized or supported")
+
     if standardized_backend not in _available_backends:
         raise ImportError(f"The {backend} backend doesn't seem to be installed")
+
     return standardized_backend
 
 
 def _set_backend(backend):
+    """
+    Set backend to use for graph operations.
+
+    Parameters
+    ----------
+    backend: str
+        Backend to use
+
+    Notes
+    -----
+    This function sets the :code:`_current_backend` variable with a validated backend.
+
+    This function modifies the global (module) variables.
+    """
+    # Global (module) variables modified by this function
     global _current_backend
+    global cycle, graph_from_adjacency_matrix, lattice, match_graphs, num_edges, num_vertices, vertex_property
+
     backend = _validate_backend(backend)
 
-    ## Check if we actually need to switch backends
+    # Check if we actually need to switch backends
     if backend == _current_backend:
         warnings.warn(f"The backend is already {backend}.")
         return
 
-    global cycle, graph_from_adjacency_matrix, lattice, match_graphs, num_edges, num_vertices, vertex_property
+    if backend == "graph_tool":
+        from spyrmsd.graphs.gt import cycle as gt_cycle
+        from spyrmsd.graphs.gt import (
+            graph_from_adjacency_matrix as gt_graph_from_adjacency_matrix,
+        )
+        from spyrmsd.graphs.gt import lattice as gt_lattice
+        from spyrmsd.graphs.gt import match_graphs as gt_match_graphs
+        from spyrmsd.graphs.gt import num_edges as gt_num_edges
+        from spyrmsd.graphs.gt import num_vertices as gt_num_vertices
+        from spyrmsd.graphs.gt import vertex_property as gt_vertex_property
 
-    if backend == "graph-tool":
         cycle = gt_cycle
         graph_from_adjacency_matrix = gt_graph_from_adjacency_matrix
         lattice = gt_lattice
@@ -96,6 +123,16 @@ def _set_backend(backend):
         vertex_property = gt_vertex_property
 
     elif backend == "networkx":
+        from spyrmsd.graphs.nx import cycle as nx_cycle
+        from spyrmsd.graphs.nx import (
+            graph_from_adjacency_matrix as nx_graph_from_adjacency_matrix,
+        )
+        from spyrmsd.graphs.nx import lattice as nx_lattice
+        from spyrmsd.graphs.nx import match_graphs as nx_match_graphs
+        from spyrmsd.graphs.nx import num_edges as nx_num_edges
+        from spyrmsd.graphs.nx import num_vertices as nx_num_vertices
+        from spyrmsd.graphs.nx import vertex_property as nx_vertex_property
+
         cycle = nx_cycle
         graph_from_adjacency_matrix = nx_graph_from_adjacency_matrix
         lattice = nx_lattice
@@ -109,15 +146,18 @@ def _set_backend(backend):
 
 if len(_available_backends) == 0:
     raise ImportError(
-        "No valid backends found. Please ensure that either graph-tool or NetworkX are installed."
+        "No valid backends found. Please ensure that one of the supported backends is installed."
+        + f"\nSupported backends: {_supported_backends}"
     )
 else:
     if _current_backend is None:
-        ## Set the backend to the first available (preferred) backend
         _set_backend(backend=_available_backends[0])
 
 
 def _get_backend():
+    """
+    Get the current backend.
+    """
     return _current_backend
 
 
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 88c9643..cbd607f 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -143,11 +143,11 @@ def test_build_graph_node_features(property) -> None:
     assert graph.num_edges(G) == 3
 
 
+@pytest.mark.skipif(
+    spyrmsd.get_backend() != "graph_tool",
+    reason="NetworkX supports all Python objects as node properties.",
+)
 def test_build_graph_node_features_unsupported() -> None:
-    pytest.importorskip(
-        "graph_tool", reason="NetworkX supports all Python objects as node properties."
-    )
-
     A = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]])
 
     property = [True, False, True]
@@ -157,7 +157,8 @@ def test_build_graph_node_features_unsupported() -> None:
 
 
 @pytest.mark.skipif(
-    len(spyrmsd.available_backends) < 2,
+    # Run test if all supported backends are installed
+    not set(spyrmsd.graph._supported_backends) <= set(spyrmsd.available_backends),
     reason="Not all of the required backends are installed",
 )
 def test_set_backend() -> None:
@@ -173,7 +174,7 @@ def test_set_backend() -> None:
     assert isinstance(Gnx, nx.Graph)
 
     spyrmsd.set_backend("graph-tool")
-    assert spyrmsd.get_backend() == "graph-tool"
+    assert spyrmsd.get_backend() == "graph_tool"
 
     Ggt = graph.graph_from_adjacency_matrix(A)
     assert isinstance(Ggt, gt.Graph)
diff --git a/tests/test_molecule.py b/tests/test_molecule.py
index 450f453..4e2fcbd 100644
--- a/tests/test_molecule.py
+++ b/tests/test_molecule.py
@@ -240,7 +240,8 @@ def test_from_rdmol(adjacency):
 
 
 @pytest.mark.skipif(
-    len(spyrmsd.available_backends) < 2,
+    # Run test if all supported backends are installed
+    not set(spyrmsd.graph._supported_backends) <= set(spyrmsd.available_backends),
     reason="Not all of the required backends are installed",
 )
 @pytest.mark.parametrize(
@@ -256,14 +257,14 @@ def test_molecule_graph_cache(mol) -> None:
     mol.to_graph()
 
     assert isinstance(mol.G["networkx"], nx.Graph)
-    assert "graph-tool" not in mol.G.keys()
+    assert "graph_tool" not in mol.G.keys()
 
     spyrmsd.set_backend("graph-tool")
     mol.to_graph()
 
     ## Make sure both backends (still) have a cache
     assert isinstance(mol.G["networkx"], nx.Graph)
-    assert isinstance(mol.G["graph-tool"], gt.Graph)
+    assert isinstance(mol.G["graph_tool"], gt.Graph)
 
     ## Strip the molecule to ensure the cache is reset
     mol.strip()

From f250c3ecd880b51f80859ae5dcaa67d592acd891 Mon Sep 17 00:00:00 2001
From: Rocco Meli <r.meli@bluemail.ch>
Date: Mon, 11 Mar 2024 23:41:11 +0100
Subject: [PATCH 3/8] Add rustworkx backend (#111)

* Update graph.py to support set_backend function
I also made a fuction to see the available backends and get the current backend

* Apply private _available_backends suggestions from code review

This commit contains the suggested changes regarding making the available_backends variable private, and refactoring the get_available_backends method to available_backends

Co-authored-by: Rocco Meli <r.meli@bluemail.ch>

* Print warning when backend is already set + make sure we use _available_backends everywhere where possible

* remove reliance on environment variables

* make _validate_backend function
print warning when a certain backend isn't installed

* Use precommit hooks
add __all__ back
refactor _alias_backendDict to _alias_to_backend

* Update ValueError message

Co-authored-by: Rocco Meli <r.meli@bluemail.ch>

* Don't return the backend when setting it

Co-authored-by: Rocco Meli <r.meli@bluemail.ch>

* Add dummy function to make mypy happy
remove __all__ assignment

* fist play with rustworkx backend

* add documentation for backend selection

* add cli backend selection

* cleanup

* changelog

* apply @Jnelen suggestion and add warning filter

* make molecule test also more robust

* add rx to ci

* add rustworkx to all backends tests

* add back graphtool

* add rx to test all backends and add verbose mode

* Update test_molecule.py

---------

Co-authored-by: jnelen <jnelen@ucam.edu>
Co-authored-by: Jochem Nelen <78348388+Jnelen@users.noreply.github.com>
---
 .github/workflows/pytest.yml                  |   2 +-
 CHANGELOG.md                                  |   1 +
 README.md                                     |  27 ++-
 .../conda-envs/spyrmsd-test-obabel-rx.yaml    |  26 +++
 .../conda-envs/spyrmsd-test-rdkit-all.yaml    |   1 +
 .../conda-envs/spyrmsd-test-rdkit-rx.yaml     |  27 +++
 docs/source/conf.py                           |   2 +-
 spyrmsd/__main__.py                           |   6 +
 spyrmsd/graph.py                              |  22 +-
 spyrmsd/graphs/rx.py                          | 190 ++++++++++++++++++
 tests/test_graph.py                           |   7 +
 tests/test_molecule.py                        |   7 +-
 12 files changed, 307 insertions(+), 11 deletions(-)
 create mode 100644 devtools/conda-envs/spyrmsd-test-obabel-rx.yaml
 create mode 100644 devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml
 create mode 100644 spyrmsd/graphs/rx.py

diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 10caace..6dc9e06 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -26,7 +26,7 @@ jobs:
         os: [macOS-latest, ubuntu-latest, windows-latest]
         python-version: ["3.9", "3.10", "3.11", "3.12"]
         chemlib: [obabel, rdkit]
-        graphlib: [nx, gt, all]
+        graphlib: [nx, gt, rx, all]
         exclude:
         # graph-tools does not work on Windows
         - {os: "windows-latest", graphlib: "gt"}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f87aa4d..aac3eb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ Contributors:    @RMeli, @takluyver, @Jnelen
 
 ### Added
 
+* Support for `rustworkx` graph library [PR 111 | @RMeli]
 * Functionality to manually select the backend from CLI [PR #108 | @RMeli]
 * Functionality to manually select the backend [PR  #107 | @Jnelen]
 * Python `3.12` to CI [PR  #102 | @RMeli]
diff --git a/README.md b/README.md
index 8a926bb..526424a 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ If you find `spyrmsd` useful, please consider citing the following paper:
 `spyrmsd` is available on [PyPI](https://pypi.org/project/spyrmsd/) and [conda-forge](https://github.com/conda-forge/spyrmsd-feedstock) and can be easily installed from source. See [Dependencies](###Dependencies) for a description of all the dependencies.
 
 > [!NOTE]
-> `spyrmsd` will install [NetworkX](https://networkx.github.io/) (multi-platform). You can install [graph-tool](https://graph-tool.skewed.de/) on macOS and Linux for higher performance.
+> `spyrmsd` will install [NetworkX](https://networkx.github.io/) (multi-platform). You can install the other backends for higher performance.
 
 > [!WARNING]
 > If `spyrmsd` is used as a standalone tool, it is required to install either [RDKit](https://rdkit.org/) or [Open Babel](http://openbabel.org/). Neither is automatically installed with `pip` nor `conda`.
@@ -71,12 +71,16 @@ pip install .
 
 The following packages are required to use `spyrmsd` as a module:
 
-* [graph-tool](https://graph-tool.skewed.de/) or [NetworkX](https://networkx.github.io/)
 * [numpy](https://numpy.org/)
 * [scipy](https://www.scipy.org/)
 
+One of the following graph libraries is required:
+* [graph-tool]
+* [NetworkX]
+* [rustworkx]
+
 > [!NOTE]
-> `spyrmsd` uses [graph-tool](https://graph-tool.skewed.de/) by default but will fall back to [NetworkX](https://networkx.github.io/) if the former is not installed (e.g. on Windows). However, in order to support cross-platform installation [NetworkX](https://networkx.github.io/) is installed by default, and [graph-tool](https://graph-tool.skewed.de/) needs to be installed manually.
+> `spyrmsd` uses the following priority when multiple graph libraries are present: [graph-tool], [NetworkX], [rustworkx]. *This order might change. Use `set_backend` to ensure you are always using the same backend, if needed.* However, in order to support cross-platform installation [NetworkX](https://networkx.github.io/) is installed by default, and the other graph library need to be installed manually.
 
 #### Standalone Tool
 
@@ -118,7 +122,7 @@ def rmsd(
 ```
 
 > [!NOTE]
-> Atomic properties (`aprops`) can be any Python object when using [NetworkX](https://networkx.github.io/), or integers, floats, or strings when using [graph-tool](https://graph-tool.skewed.de/).
+> Atomic properties (`aprops`) can be any Python object when using [NetworkX] and [rustworkx], or integers, floats, or strings when using [graph-tool].
 
 #### Symmetry-Corrected RMSD
 
@@ -142,11 +146,16 @@ def symmrmsd(
 ```
 
 > [!NOTE]
-> Atomic properties (`aprops`) can be any Python object when using [NetworkX](https://networkx.github.io/), or integers, floats, or strings when using [graph-tool](https://graph-tool.skewed.de/).
+> Atomic properties (`aprops`) can be any Python object when using [NetworkX] and [rustworkx], or integers, floats, or strings when using [graph-tool](https://graph-tool.skewed.de/).
 
 #### Select Backend
 
-`spyrmsd` supports both [NetworkX](https://networkx.github.io/) and [graph-tool](https://graph-tool.skewed.de/) for the calculation of graph isomorphisms. You can check which backend is being used with
+`spyrmsd` supports the following graph libraries for the calculation of graph isomorphisms:
+* [graph-tool]
+* [NetworkX]
+* [rustworkx]
+
+ You can check which backend is being used with
 
 ```python
 spyrmsd.get_backend()
@@ -176,7 +185,7 @@ Pre-commit `git` hooks can be installed with [pre-commit](https://pre-commit.com
 
 ## Copyright
 
-Copyright (c) 2019-2021, Rocco Meli.
+Copyright (c) 2019-2024, Rocco Meli.
 
 ## References
 
@@ -185,3 +194,7 @@ References are tracked with [duecredit](https://github.com/duecredit/duecredit/)
 ### Acknowledgements
 
 Project based on the [Computational Molecular Science Python Cookiecutter](https://github.com/molssi/cookiecutter-cms) version `1.1`.
+
+[rustworkx]: https://www.rustworkx.org
+[NetworkX]: https://networkx.github.io/
+[graph-tool]: https://graph-tool.skewed.de/
diff --git a/devtools/conda-envs/spyrmsd-test-obabel-rx.yaml b/devtools/conda-envs/spyrmsd-test-obabel-rx.yaml
new file mode 100644
index 0000000..af3cf83
--- /dev/null
+++ b/devtools/conda-envs/spyrmsd-test-obabel-rx.yaml
@@ -0,0 +1,26 @@
+name: spyrmsd
+channels:
+  - conda-forge
+dependencies:
+  # Base
+  - python
+  - setuptools
+
+  # Maths
+  - numpy
+  - scipy
+  - rustworkx
+
+  # Chemistry
+  - openbabel
+
+  # Testing
+  - pytest
+  - pytest-cov
+  - pytest-benchmark
+
+  # Dev
+  - mypy
+  - flake8
+  - black
+  - codecov
diff --git a/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml b/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
index 9212381..d1d7ced 100644
--- a/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
+++ b/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
@@ -12,6 +12,7 @@ dependencies:
   - scipy
   - graph-tool
   - networkx>=2
+  - rustworkx
 
   # Chemistry
   - rdkit
diff --git a/devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml b/devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml
new file mode 100644
index 0000000..e752f97
--- /dev/null
+++ b/devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml
@@ -0,0 +1,27 @@
+name: spyrmsd
+channels:
+  - conda-forge
+  - rdkit
+dependencies:
+  # Base
+  - python
+  - setuptools
+
+  # Maths
+  - numpy
+  - scipy
+  - rustworkx
+
+  # Chemistry
+  - rdkit
+
+  # Testing
+  - pytest
+  - pytest-cov
+  - pytest-benchmark
+
+  # Dev
+  - mypy
+  - flake8
+  - black
+  - codecov
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 1be2b02..eb02994 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -23,7 +23,7 @@
 
 project = "spyrmsd"
 copyright = (
-    "2019-2021, Rocco Meli. Project structure based on the "
+    "2019-2024, Rocco Meli. Project structure based on the "
     "Computational Molecular Science Python Cookiecutter version 1.1"
 )
 author = "Rocco Meli"
diff --git a/spyrmsd/__main__.py b/spyrmsd/__main__.py
index e61563d..4e8ba96 100644
--- a/spyrmsd/__main__.py
+++ b/spyrmsd/__main__.py
@@ -34,6 +34,9 @@
         default=None,
         help="Graph library (backend)",
     )
+    parser.add_argument(
+        "-v", "--verbose", action="store_true", help="Enable verbose mode"
+    )
 
     args = parser.parse_args()
 
@@ -63,6 +66,9 @@
             warnings.simplefilter("ignore")
             spyrmsd.set_backend(args.graph_backend)
 
+            if args.verbose:
+                print(f"Graph library: {spyrmsd.get_backend()}")
+
     # Loop over molecules within fil
     RMSDlist = rmsdwrapper(
         ref,
diff --git a/spyrmsd/graph.py b/spyrmsd/graph.py
index b788992..f777eed 100644
--- a/spyrmsd/graph.py
+++ b/spyrmsd/graph.py
@@ -5,7 +5,7 @@
 
 from spyrmsd import constants
 
-_supported_backends = ("graph_tool", "networkx")
+_supported_backends = ("graph_tool", "networkx", "rustworkx")
 
 _available_backends = []
 _current_backend = None
@@ -13,6 +13,7 @@
 _backend_to_alias = {
     "graph_tool": ["graph_tool", "graphtool", "graph-tool", "graph tool", "gt"],
     "networkx": ["networkx", "nx"],
+    "rustworkx": ["rustworkx", "rx"],
 }
 
 _alias_to_backend = {}
@@ -141,6 +142,25 @@ def _set_backend(backend):
         num_vertices = nx_num_vertices
         vertex_property = nx_vertex_property
 
+    elif backend == "rustworkx":
+        from spyrmsd.graphs.rx import cycle as rx_cycle
+        from spyrmsd.graphs.rx import (
+            graph_from_adjacency_matrix as rx_graph_from_adjacency_matrix,
+        )
+        from spyrmsd.graphs.rx import lattice as rx_lattice
+        from spyrmsd.graphs.rx import match_graphs as rx_match_graphs
+        from spyrmsd.graphs.rx import num_edges as rx_num_edges
+        from spyrmsd.graphs.rx import num_vertices as rx_num_vertices
+        from spyrmsd.graphs.rx import vertex_property as rx_vertex_property
+
+        cycle = rx_cycle
+        graph_from_adjacency_matrix = rx_graph_from_adjacency_matrix
+        lattice = rx_lattice
+        match_graphs = rx_match_graphs
+        num_edges = rx_num_edges
+        num_vertices = rx_num_vertices
+        vertex_property = rx_vertex_property
+
     _current_backend = backend
 
 
diff --git a/spyrmsd/graphs/rx.py b/spyrmsd/graphs/rx.py
new file mode 100644
index 0000000..673293a
--- /dev/null
+++ b/spyrmsd/graphs/rx.py
@@ -0,0 +1,190 @@
+import warnings
+from typing import Any, List, Optional, Tuple, Union
+
+import numpy as np
+import rustworkx as rx
+
+from spyrmsd.exceptions import NonIsomorphicGraphs
+from spyrmsd.graphs._common import (
+    error_non_isomorphic_graphs,
+    warn_disconnected_graph,
+    warn_no_atomic_properties,
+)
+
+
+def graph_from_adjacency_matrix(
+    adjacency_matrix: Union[np.ndarray, List[List[int]]],
+    aprops: Optional[Union[np.ndarray, List[Any]]] = None,
+) -> rx.PyGraph:
+    """
+    Graph from adjacency matrix.
+
+    Parameters
+    ----------
+    adjacency_matrix: Union[np.ndarray, List[List[int]]]
+        Adjacency matrix
+    aprops: Union[np.ndarray, List[Any]], optional
+        Atomic properties
+
+    Returns
+    -------
+    Graph
+        Molecular graph
+
+    Notes
+    -----
+    It the atomic numbers are passed, they are used as node attributes.
+    """
+
+    G = rx.PyGraph.from_adjacency_matrix(np.asarray(adjacency_matrix, dtype=np.float64))
+
+    if not rx.is_connected(G):
+        warnings.warn(warn_disconnected_graph)
+
+    if aprops is not None:
+        for i in G.node_indices():
+            G[i] = aprops[i]
+
+    return G
+
+
+def match_graphs(G1, G2) -> List[Tuple[List[int], List[int]]]:
+    """
+    Compute graph isomorphisms.
+
+    Parameters
+    ----------
+    G1:
+        Graph 1
+    G2:
+        Graph 2
+
+    Returns
+    -------
+    List[Tuple[List[int],List[int]]]
+        All possible mappings between nodes of graph 1 and graph 2 (isomorphisms)
+
+    Raises
+    ------
+    NonIsomorphicGraphs
+        If the graphs `G1` and `G2` are not isomorphic
+    """
+
+    def match_aprops(node1, node2):
+        """
+        Check if atomic properties for two nodes match.
+        """
+        return node1 == node2
+
+    if G1[0] is None or G2[0] is None:
+        # Nodes without atomic number information
+        # No node-matching check
+        node_match = None
+
+        warnings.warn(warn_no_atomic_properties)
+
+    else:
+        node_match = match_aprops
+
+    GM = rx.vf2_mapping(G1, G2, node_match)
+
+    isomorphisms = [
+        (list(isomorphism.keys()), list(isomorphism.values())) for isomorphism in GM
+    ]
+
+    # Check if graphs are actually isomorphic
+    if len(isomorphisms) == 0:
+        raise NonIsomorphicGraphs(error_non_isomorphic_graphs)
+
+    return isomorphisms
+
+
+def vertex_property(G, vproperty: str, idx: int) -> Any:
+    """
+    Get vertex (node) property from graph
+
+    Parameters
+    ----------
+    G:
+        Graph
+    vproperty: str
+        Vertex property name
+    idx: int
+        Vertex index
+
+    Returns
+    -------
+    Any
+        Vertex property value
+    """
+    return G[idx]
+
+
+def num_vertices(G) -> int:
+    """
+    Number of vertices
+
+    Parameters
+    ----------
+    G:
+        Graph
+
+    Returns
+    -------
+    int
+        Number of vertices (nodes)
+    """
+    return G.num_nodes()
+
+
+def num_edges(G) -> int:
+    """
+    Number of edges
+
+    Parameters
+    ----------
+    G:
+        Graph
+
+    Returns
+    -------
+    int
+        Number of edges
+    """
+    return G.num_edges()
+
+
+def lattice(n1, n2):
+    """
+    Build 2D lattice graph
+
+    Parameters
+    ----------
+    n1: int
+        Number of nodes in dimension 1
+    n2: int
+        Number of nodes in dimension 2
+
+    Returns
+    -------
+    Graph
+        Lattice graph
+    """
+    return rx.generators.grid_graph(rows=n1, cols=n2, multigraph=False)
+
+
+def cycle(n):
+    """
+    Build cycle graph
+
+    Parameters
+    ----------
+    n: int
+        Number of nodes
+
+    Returns
+    -------
+    Graph
+        Cycle graph
+    """
+    return rx.generators.cycle_graph(n, multigraph=False)
diff --git a/tests/test_graph.py b/tests/test_graph.py
index cbd607f..c25bf5a 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -164,6 +164,7 @@ def test_build_graph_node_features_unsupported() -> None:
 def test_set_backend() -> None:
     import graph_tool as gt
     import networkx as nx
+    import rustworkx as rx
 
     A = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]])
 
@@ -179,5 +180,11 @@ def test_set_backend() -> None:
     Ggt = graph.graph_from_adjacency_matrix(A)
     assert isinstance(Ggt, gt.Graph)
 
+    spyrmsd.set_backend("rustworkx")
+    assert spyrmsd.get_backend() == "rustworkx"
+
+    Grx = graph.graph_from_adjacency_matrix(A)
+    assert isinstance(Grx, rx.PyGraph)
+
     with pytest.raises(ValueError, match="backend is not recognized or supported"):
         spyrmsd.set_backend("unknown")
diff --git a/tests/test_molecule.py b/tests/test_molecule.py
index 4e2fcbd..fde466b 100644
--- a/tests/test_molecule.py
+++ b/tests/test_molecule.py
@@ -250,6 +250,7 @@ def test_from_rdmol(adjacency):
 def test_molecule_graph_cache(mol) -> None:
     import graph_tool as gt
     import networkx as nx
+    import rustworkx as rx
 
     ## Graph cache persists from previous tests, manually reset them
     mol.G = {}
@@ -262,9 +263,13 @@ def test_molecule_graph_cache(mol) -> None:
     spyrmsd.set_backend("graph-tool")
     mol.to_graph()
 
-    ## Make sure both backends (still) have a cache
+    spyrmsd.set_backend("rustworkx")
+    mol.to_graph()
+
+    ## Make sure all backends (still) have a cache
     assert isinstance(mol.G["networkx"], nx.Graph)
     assert isinstance(mol.G["graph_tool"], gt.Graph)
+    assert isinstance(mol.G["rustworkx"], rx.PyGraph)
 
     ## Strip the molecule to ensure the cache is reset
     mol.strip()

From 08764e1c3a9eb4b636c14ddf083e1cbbe50014f9 Mon Sep 17 00:00:00 2001
From: Rocco Meli <r.meli@bluemail.ch>
Date: Tue, 12 Mar 2024 00:04:44 +0100
Subject: [PATCH 4/8] Update .readthedocs.yml (#114)

* Update .readthedocs.yml

* rm duplicate key
---
 .readthedocs.yml | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/.readthedocs.yml b/.readthedocs.yml
index 91db81d..bed6774 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,16 +1,20 @@
-# .readthedocs.yml
-# Read the Docs configuration file
+# Read the Docs configuration file for Sphinx projects
 # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
 
 version: 2
 
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "mambaforge-22.9"
+
 sphinx:
   builder: html
   configuration: docs/source/conf.py
 
 formats:
-  - epub
   - pdf
+  - epub
 
 conda:
   environment: devtools/conda-envs/spyrmsd-docs.yaml

From 4ecf817adc57b7c5a56f012401d70bc2f65f4ec0 Mon Sep 17 00:00:00 2001
From: Rocco Meli <r.meli@bluemail.ch>
Date: Fri, 5 Apr 2024 22:07:21 +0200
Subject: [PATCH 5/8] prepare release 0.7.0

---
 CHANGELOG.md        | 4 ++--
 CITATION.cff        | 2 +-
 README.md           | 2 +-
 spyrmsd/__init__.py | 2 +-
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index aac3eb4..67f0996 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,9 +3,9 @@
 
 ------------------------------------------------------------------------------
 
-## Version X.Y.Z
+## Version 0.7.0
 
-Date:            XX/YY/ZZZZ
+Date:            05/04/2024
 Contributors:    @RMeli, @takluyver, @Jnelen
 
 ### Added
diff --git a/CITATION.cff b/CITATION.cff
index 1006b56..07a57a1 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -8,7 +8,7 @@ authors:
   given-names: "Jochem"
   orcid: "https://orcid.org/0000-0002-9970-4950"
 title: "spyrmsd"
-version: 0.6.0
+version: 0.7.0
 doi: 10.5281/zenodo.3631876
 date-released: 2021-06-21
 url: "https://github.com/RMeli/spyrmsd"
diff --git a/README.md b/README.md
index 526424a..e2e5bb8 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
 [![Documentation Status](https://readthedocs.org/projects/spyrmsd/badge/?version=develop)](https://spyrmsd.readthedocs.io/en/develop/?badge=develop)
 
 [![License](https://img.shields.io/github/license/RMeli/pyrmsd?color=%2333BBFF)](https://opensource.org/licenses/MIT)
-[![PyPI](https://img.shields.io/badge/PyPI-v0.6.0%20-ff69b4)](https://pypi.org/project/spyrmsd/)
+[![PyPI](https://img.shields.io/badge/PyPI-v0.7.0%20-ff69b4)](https://pypi.org/project/spyrmsd/)
 [![Conda Version](https://img.shields.io/conda/vn/conda-forge/spyrmsd.svg)](https://anaconda.org/conda-forge/spyrmsd)
 
 [![J. Cheminform.](https://img.shields.io/badge/J.%20Cheminform.-10.1186%2Fs13321--020--00455--2-blue)](https://doi.org/10.1186/s13321-020-00455-2)
diff --git a/spyrmsd/__init__.py b/spyrmsd/__init__.py
index 36cb386..b2f849b 100644
--- a/spyrmsd/__init__.py
+++ b/spyrmsd/__init__.py
@@ -10,7 +10,7 @@
 from .graph import _get_backend as get_backend  # noqa: F401
 from .graph import _set_backend as set_backend  # noqa: F401
 
-__version__ = "0.7.0-dev"
+__version__ = "0.7.0"
 
 # This will print latest Zenodo version
 due.cite(

From aa1cd7a635929860b83adb2f476b8250e2a2771f Mon Sep 17 00:00:00 2001
From: Rocco Meli <r.meli@bluemail.ch>
Date: Sat, 6 Apr 2024 00:02:33 +0200
Subject: [PATCH 6/8] v0.8.0-dev

---
 CHANGELOG.md        | 5 +++++
 spyrmsd/__init__.py | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67f0996..440d13c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,11 @@
 
 ------------------------------------------------------------------------------
 
+## Version X.Y.Z
+
+Date:            XX/YY/ZZZ
+Contributors:    @RMeli
+
 ## Version 0.7.0
 
 Date:            05/04/2024
diff --git a/spyrmsd/__init__.py b/spyrmsd/__init__.py
index b2f849b..4af9ed7 100644
--- a/spyrmsd/__init__.py
+++ b/spyrmsd/__init__.py
@@ -10,7 +10,7 @@
 from .graph import _get_backend as get_backend  # noqa: F401
 from .graph import _set_backend as set_backend  # noqa: F401
 
-__version__ = "0.7.0"
+__version__ = "0.8.0-dev"
 
 # This will print latest Zenodo version
 due.cite(

From da6be7fc0b5f9171a61605024610dcfdcfd3d899 Mon Sep 17 00:00:00 2001
From: Rocco Meli <r.meli@bluemail.ch>
Date: Sat, 6 Apr 2024 04:36:54 +0200
Subject: [PATCH 7/8] Use IDs in tests (#117)

---
 CHANGELOG.md             | 10 ++++--
 tests/test_benchmarks.py |  2 +-
 tests/test_graph.py      |  1 +
 tests/test_io.py         | 11 ++++++
 tests/test_large.py      |  6 ++--
 tests/test_molecule.py   | 10 +++++-
 tests/test_qcp.py        |  1 +
 tests/test_rmsd.py       | 72 ++++++++++++++++++++++++++++++++++------
 tests/test_utils.py      |  1 +
 9 files changed, 96 insertions(+), 18 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 440d13c..829dc7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,13 @@
 
 ## Version X.Y.Z
 
-Date:            XX/YY/ZZZ
+Date:            XX/YY/ZZZZ
 Contributors:    @RMeli
 
+### Improved
+
+* Test IDs [PR #117 | @RMeli]
+
 ## Version 0.7.0
 
 Date:            05/04/2024
@@ -15,7 +19,7 @@ Contributors:    @RMeli, @takluyver, @Jnelen
 
 ### Added
 
-* Support for `rustworkx` graph library [PR 111 | @RMeli]
+* Support for `rustworkx` graph library [PR #111 | @RMeli]
 * Functionality to manually select the backend from CLI [PR #108 | @RMeli]
 * Functionality to manually select the backend [PR  #107 | @Jnelen]
 * Python `3.12` to CI [PR  #102 | @RMeli]
@@ -24,7 +28,7 @@ Contributors:    @RMeli, @takluyver, @Jnelen
 ### Changed
 
 * Molecular graphs cache to cache by backend [PR  #107 | @Jnelen]
-* Python build system to use flit_core directly [PR #103 | @takluyver]
+* Python build system to use `flit_core` directly [PR #103 | @takluyver]
 * Minimum version of Python to `3.9` (to reduce CI matrix) [PR  #102 | @RMeli]
 
 ### Fixed
diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py
index 6690450..c9ec6f7 100644
--- a/tests/test_benchmarks.py
+++ b/tests/test_benchmarks.py
@@ -40,7 +40,7 @@ def molecules(request):
 
 
 @pytest.mark.benchmark
-@pytest.mark.parametrize("cache", [True, False])
+@pytest.mark.parametrize("cache", [True, False], ids=["cache", "no_cache"])
 def test_benchmark_symmrmsd(cache, molecules, benchmark):
     ref, mols, system = molecules
 
diff --git a/tests/test_graph.py b/tests/test_graph.py
index c25bf5a..1d68cef 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -31,6 +31,7 @@ def test_adjacency_matrix_from_atomic_coordinates_distance() -> None:
 @pytest.mark.parametrize(
     "mol, n_bonds",
     [(molecules.benzene, 12), (molecules.ethanol, 8), (molecules.dialanine, 22)],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_adjacency_matrix_from_atomic_coordinates(
     mol: molecule.Molecule, n_bonds: int
diff --git a/tests/test_io.py b/tests/test_io.py
index f06e2a2..1200d3f 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -11,6 +11,7 @@
 @pytest.mark.parametrize(
     "molfile, natoms, nbonds",
     [("benzene.sdf", 12, 12), ("ethanol.sdf", 9, 8), ("dialanine.sdf", 23, 22)],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_load_sdf(molfile, natoms: int, nbonds: int) -> None:
     m = io.load(os.path.join(molpath, molfile))
@@ -22,6 +23,7 @@ def test_load_sdf(molfile, natoms: int, nbonds: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms, nbonds",
     [("1cbr_ligand.mol2", 49, 49)],
+    ids=["1cbr_ligand"],
 )
 def test_load_mol2(molfile, natoms: int, nbonds: int) -> None:
     m = io.load(os.path.join(molpath, molfile))
@@ -33,6 +35,7 @@ def test_load_mol2(molfile, natoms: int, nbonds: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms, nbonds",
     [("trp0.pdb", 217, 224), ("trp1.pdb", 217, 224), ("trp2.pdb", 217, 224)],
+    ids=["trp0", "trp1", "trp2"],
 )
 def test_load_pdb(molfile, natoms: int, nbonds: int) -> None:
     m = io.load(os.path.join(molpath, molfile))
@@ -44,6 +47,7 @@ def test_load_pdb(molfile, natoms: int, nbonds: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms, nbonds",
     [("1cbr_docking.sdf", 22, 22)],
+    ids=["1cbr_docking"],
 )
 def test_loadall_sdf(molfile, natoms: int, nbonds: int) -> None:
     ms = io.loadall(os.path.join(molpath, molfile))
@@ -58,6 +62,7 @@ def test_loadall_sdf(molfile, natoms: int, nbonds: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms, nbonds",
     [("1cbr_docking.mol2", 22, 22)],
+    ids=["1cbr_docking"],
 )
 def test_loadall_mol2(molfile, natoms: int, nbonds: int) -> None:
     try:
@@ -75,6 +80,7 @@ def test_loadall_mol2(molfile, natoms: int, nbonds: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms, nbonds",
     [("1cbr_docking.pdb", 22, 22)],
+    ids=["1cbr_docking"],
 )
 def test_loadall_pdb(molfile, natoms: int, nbonds: int) -> None:
     try:
@@ -110,6 +116,7 @@ def test_loadall_pdb_single_model() -> None:
 @pytest.mark.parametrize(
     "molfile, natoms",
     [("benzene.sdf", 12), ("ethanol.sdf", 9), ("dialanine.sdf", 23)],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_loadmol_sdf(molfile, natoms: int) -> None:
     m = io.loadmol(os.path.join(molpath, molfile))
@@ -120,6 +127,7 @@ def test_loadmol_sdf(molfile, natoms: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms",
     [("benzene.mol2", 12), ("1cbr_ligand.mol2", 49)],
+    ids=["benzene", "1cbr_ligand"],
 )
 def test_loadmol_mol2(molfile, natoms: int) -> None:
     m = io.loadmol(os.path.join(molpath, molfile))
@@ -130,6 +138,7 @@ def test_loadmol_mol2(molfile, natoms: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms",
     [("1cbr_docking.sdf", 22)],
+    ids=["1cbr_docking"],
 )
 def test_loadallmols_sdf(molfile, natoms: int) -> None:
     ms = io.loadallmols(os.path.join(molpath, molfile))
@@ -143,6 +152,7 @@ def test_loadallmols_sdf(molfile, natoms: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms",
     [("benzene.sdf.gz", 12), ("benzene.mol2.gz", 12), ("1a99_ligand.pdb.gz", 20)],
+    ids=["benzene", "benzene_mol2", "1a99_ligand"],
 )
 def test_loadmol_gz(molfile, natoms: int) -> None:
     m = io.loadmol(os.path.join(molpath, molfile))
@@ -153,6 +163,7 @@ def test_loadmol_gz(molfile, natoms: int) -> None:
 @pytest.mark.parametrize(
     "molfile, natoms",
     [("1cbr_docking.sdf.gz", 22)],
+    ids=["1cbr_docking"],
 )
 def test_loadallmols_sdf_gz(molfile, natoms: int) -> None:
     ms = io.loadallmols(os.path.join(molpath, molfile))
diff --git a/tests/test_large.py b/tests/test_large.py
index b84a183..f1893ae 100644
--- a/tests/test_large.py
+++ b/tests/test_large.py
@@ -8,7 +8,9 @@
 from spyrmsd import io, qcp, rmsd
 
 
-@pytest.fixture(autouse=True, params=[True, False])
+@pytest.fixture(
+    autouse=True, params=[True, False], ids=["lamnda_max_fast", "lambda_max_fallback"]
+)
 def lambda_max_failure(monkeypatch, request):
     """
     Monkey patch fixture for :code:`lambda_max` function to simulate convergence
@@ -95,7 +97,7 @@ def test_dowload(download, path):
 
 
 @pytest.mark.large
-@pytest.mark.parametrize("minimize", [True, False])
+@pytest.mark.parametrize("minimize", [True, False], ids=["minimize", "no_minimize"])
 def test_rmsd(idx, download, path, minimize):
     id = download[idx]
 
diff --git a/tests/test_molecule.py b/tests/test_molecule.py
index fde466b..b0f6064 100644
--- a/tests/test_molecule.py
+++ b/tests/test_molecule.py
@@ -19,6 +19,7 @@
         (molecules.ethanol, [(1, 6), (6, 2), (8, 1)]),
         (molecules.dialanine, [(1, 12), (6, 6), (7, 2), (8, 3)]),
     ],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_load(mol: molecule.Molecule, atoms: List[Tuple[int, int]]) -> None:
     n = sum([n_atoms for _, n_atoms in atoms])
@@ -133,6 +134,7 @@ def test_molecule_center_of_mass_HF() -> None:
         (molecules.ethanol, 9, 6),
         (molecules.dialanine, 23, 12),
     ],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_molecule_strip(mol: molecule.Molecule, n_atoms: int, stripped: int) -> None:
     m = copy.deepcopy(mol)
@@ -147,6 +149,7 @@ def test_molecule_strip(mol: molecule.Molecule, n_atoms: int, stripped: int) ->
 @pytest.mark.parametrize(
     "mol, n_bonds",
     [(molecules.benzene, 12), (molecules.ethanol, 8), (molecules.dialanine, 22)],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_graph_from_adjacency_matrix(mol: molecule.Molecule, n_bonds: int) -> None:
     G = mol.to_graph()
@@ -161,6 +164,7 @@ def test_graph_from_adjacency_matrix(mol: molecule.Molecule, n_bonds: int) -> No
 @pytest.mark.parametrize(
     "mol, n_bonds",
     [(molecules.benzene, 12), (molecules.ethanol, 8), (molecules.dialanine, 22)],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_graph_from_atomic_coordinates_perception(
     mol: molecule.Molecule, n_bonds: int
@@ -184,6 +188,7 @@ def test_graph_from_atomic_coordinates_perception(
 @pytest.mark.parametrize(
     "adjacency",
     [True, False],
+    ids=["adjacency", "no_adjacency"],
 )
 def test_from_obmol(adjacency):
     pytest.importorskip("openbabel")
@@ -213,6 +218,7 @@ def test_from_obmol(adjacency):
 @pytest.mark.parametrize(
     "adjacency",
     [True, False],
+    ids=["adjacency", "no_adjacency"],
 )
 def test_from_rdmol(adjacency):
     pytest.importorskip("rdkit")
@@ -245,7 +251,9 @@ def test_from_rdmol(adjacency):
     reason="Not all of the required backends are installed",
 )
 @pytest.mark.parametrize(
-    "mol", [(molecules.benzene), (molecules.ethanol), (molecules.dialanine)]
+    "mol",
+    [(molecules.benzene), (molecules.ethanol), (molecules.dialanine)],
+    ids=["benzene", "ethanol", "dialanine"],
 )
 def test_molecule_graph_cache(mol) -> None:
     import graph_tool as gt
diff --git a/tests/test_qcp.py b/tests/test_qcp.py
index 0470c07..3556eb0 100644
--- a/tests/test_qcp.py
+++ b/tests/test_qcp.py
@@ -55,6 +55,7 @@ def test_K_mtx(mol: molecule.Molecule) -> None:
         ((-1, -1, -5, 0, 4), -1),  # f(x) = x^4 - 5 * x^2 + 4; x_0 = -1/2
         ((-3, -3, -5, 0, 4), -2),  # f(x) = x^4 - 5 * x^2 + 4; x_0 = -3
     ],
+    ids=["f1", "f2", "f3", "f4", "f5", "f6"],
 )
 def test_lambda_max(
     input: Tuple[float, float, float, float, float], result: float
diff --git a/tests/test_rmsd.py b/tests/test_rmsd.py
index dc54c24..f442d73 100644
--- a/tests/test_rmsd.py
+++ b/tests/test_rmsd.py
@@ -8,7 +8,9 @@
 from tests import molecules
 
 
-@pytest.fixture(autouse=True, params=[True, False])
+@pytest.fixture(
+    autouse=True, params=[True, False], ids=["lambda_max_fast", "lambda_max_fallback"]
+)
 def lambda_max_failure(monkeypatch, request):
     """
     Monkey patch fixture for :code:`lambda_max` function to simulate convergence
@@ -37,7 +39,9 @@ def lambda_max_failure(Ga, Gb, c2, c1, c0):
         monkeypatch.setattr(qcp, "lambda_max", lambda_max_failure)
 
 
-@pytest.mark.parametrize("t, RMSD", [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)])
+@pytest.mark.parametrize(
+    "t, RMSD", [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)], ids=["t0", "t1", "t2"]
+)
 def test_rmsd_benzene(t: float, RMSD: float) -> None:
     mol1 = copy.deepcopy(molecules.benzene)
     mol2 = copy.deepcopy(molecules.benzene)
@@ -52,7 +56,9 @@ def test_rmsd_benzene(t: float, RMSD: float) -> None:
 # Results obtained with PyTraj
 #   pytraj.analysis.rmsd.rmsd(i, ref=j, nofit=True)
 @pytest.mark.parametrize(
-    "i, j, result", [(1, 2, 2.60065218), (1, 3, 9.94411523), (2, 3, 9.4091711)]
+    "i, j, result",
+    [(1, 2, 2.60065218), (1, 3, 9.94411523), (2, 3, 9.4091711)],
+    ids=["1-2", "1-3", "2-3"],
 )
 def test_rmsd_2viz(i: int, j: int, result: float) -> None:
     moli = copy.deepcopy(molecules.docking_2viz[i])
@@ -66,7 +72,9 @@ def test_rmsd_2viz(i: int, j: int, result: float) -> None:
 # Results obtained with PyTraj
 #   pytraj.analysis.rmsd.rmsd(i, mask="!@H=", ref=j, ref_mask="!@H=", nofit=True)
 @pytest.mark.parametrize(
-    "i, j, result", [(1, 2, 2.65327362), (1, 3, 10.11099065), (2, 3, 9.57099612)]
+    "i, j, result",
+    [(1, 2, 2.65327362), (1, 3, 10.11099065), (2, 3, 9.57099612)],
+    ids=["1-2", "1-3", "2-3"],
 )
 def test_rmsd_2viz_stripped(i: int, j: int, result: float) -> None:
     moli = copy.deepcopy(molecules.docking_2viz[i])
@@ -139,7 +147,9 @@ def test_rmsd_minimize(mol: molecule.Molecule) -> None:
 # Results obtained with PyTraj
 #   pytraj.analysis.rmsd.rmsd(i, ref=j)
 @pytest.mark.parametrize(
-    "i, j, result", [(1, 2, 1.95277757), (1, 3, 3.11801105), (2, 3, 2.98609758)]
+    "i, j, result",
+    [(1, 2, 1.95277757), (1, 3, 3.11801105), (2, 3, 2.98609758)],
+    ids=["1-2", "1-3", "2-3"],
 )
 def test_rmsd_qcp_2viz(i: int, j: int, result: float) -> None:
     moli = copy.deepcopy(molecules.docking_2viz[i])
@@ -157,7 +167,9 @@ def test_rmsd_qcp_2viz(i: int, j: int, result: float) -> None:
 # Results obtained with PyTraj
 #   pytraj.analysis.rmsd.rmsd(i, "!@H=", ref=j, ref_mask="!@H=")
 @pytest.mark.parametrize(
-    "i, j, result", [(1, 2, 1.98171656), (1, 3, 3.01799306), (2, 3, 2.82917355)]
+    "i, j, result",
+    [(1, 2, 1.98171656), (1, 3, 3.01799306), (2, 3, 2.82917355)],
+    ids=["1-2", "1-3", "2-3"],
 )
 def test_rmsd_qcp_2viz_stripped(i: int, j: int, result: float) -> None:
     moli = copy.deepcopy(molecules.docking_2viz[i])
@@ -194,6 +206,7 @@ def test_rmsd_qcp_2viz_stripped(i: int, j: int, result: float) -> None:
         (4, 9.772939589989000, 2.1234944939308220),
         (5, 8.901837608843241, 2.4894805175766606),
     ],
+    ids=["1", "2", "3", "4", "5"],
 )
 def test_rmsd_qcp_protein(i: int, rmsd_dummy: float, rmsd_min: float):
     mol0 = copy.deepcopy(molecules.trp[0])
@@ -213,7 +226,9 @@ def test_rmsd_qcp_protein(i: int, rmsd_dummy: float, rmsd_min: float):
 
 
 @pytest.mark.parametrize(
-    "angle, tol", [(60, 1e-4), (120, 1e-4), (180, 1e-4), (240, 1e-4), (300, 1e-4)]
+    "angle, tol",
+    [(60, 1e-4), (120, 1e-4), (180, 1e-4), (240, 1e-4), (300, 1e-4)],
+    ids=["60", "120", "180", "240", "300"],
 )
 def test_rmsd_hungarian_benzene_rotated(angle: float, tol: float) -> None:
     mol1 = copy.deepcopy(molecules.benzene)
@@ -239,9 +254,13 @@ def test_rmsd_hungarian_benzene_rotated(angle: float, tol: float) -> None:
     ) == pytest.approx(0, abs=tol)
 
 
-@pytest.mark.parametrize("d", [-0.5, 0.0, 0.5, 1.0, 1.5])
 @pytest.mark.parametrize(
-    "angle, tol", [(60, 1e-4), (120, 1e-4), (180, 1e-4), (240, 1e-4), (300, 1e-4)]
+    "d", [-0.5, 0.0, 0.5, 1.0, 1.5], ids=["t1", "t2", "t3", "t4", "t5"]
+)
+@pytest.mark.parametrize(
+    "angle, tol",
+    [(60, 1e-4), (120, 1e-4), (180, 1e-4), (240, 1e-4), (300, 1e-4)],
+    ids=["60", "120", "180", "240", "300"],
 )
 def test_rmsd_hungarian_benzene_shifted_rotated(
     d: float, angle: float, tol: float
@@ -443,6 +462,28 @@ def test_symmrmsd_atomicnums_matching_pyridine_stripped() -> None:
         (9, 0.965387, True),
         (10, 1.37842, True),
     ],
+    ids=[
+        "1-no_minimize",
+        "2-no_minimize",
+        "3-no_minimize",
+        "4-no_minimize",
+        "5-no_minimize",
+        "6-no_minimize",
+        "7-no_minimize",
+        "8-no_minimize",
+        "9-no_minimize",
+        "10-no_minimize",
+        "1-minimize",
+        "2-minimize",
+        "3-minimize",
+        "4-minimize",
+        "5-minimize",
+        "6-minimize",
+        "7-minimize",
+        "8-minimize",
+        "9-minimize",
+        "10-minimize",
+    ],
 )
 def test_rmsd_symmrmsd(index: int, RMSD: float, minimize: bool) -> None:
     molc = copy.deepcopy(molecules.docking_1cbr[0])
@@ -508,6 +549,7 @@ def test_rmsd_symmrmsd_disconnected_node() -> None:
             ],
         ),
     ],
+    ids=["no_minimize", "minimize"],
 )
 def test_multi_spyrmsd(minimize: bool, referenceRMSDs: List[float]) -> None:
     molc = copy.deepcopy(molecules.docking_1cbr[0])
@@ -567,6 +609,7 @@ def test_multi_spyrmsd(minimize: bool, referenceRMSDs: List[float]) -> None:
             ],
         ),
     ],
+    ids=["no_minimize", "minimize"],
 )
 def test_symmrmsd_cache(minimize: bool, referenceRMSDs: List[float]) -> None:
     molc = copy.deepcopy(molecules.docking_1cbr[0])
@@ -709,7 +752,9 @@ def test_issue_35_2():
 
 
 @pytest.mark.parametrize(
-    "i, j, result", [(1, 2, 1.95277757), (1, 3, 3.11801105), (2, 3, 2.98609758)]
+    "i, j, result",
+    [(1, 2, 1.95277757), (1, 3, 3.11801105), (2, 3, 2.98609758)],
+    ids=["1-2", "1-3", "2-3"],
 )
 def test_rmsd_atol(i: int, j: int, result: float):
     """
@@ -744,7 +789,9 @@ def test_rmsd_atol(i: int, j: int, result: float):
 
 
 # Results obtained with OpenBabel
-@pytest.mark.parametrize("i, reference", [(1, 0.476858), (2, 1.68089), (3, 1.50267)])
+@pytest.mark.parametrize(
+    "i, reference", [(1, 0.476858), (2, 1.68089), (3, 1.50267)], ids=["1", "2", "3"]
+)
 def test_symmrmsd_atol(i: bool, reference: float) -> None:
     moli = copy.deepcopy(molecules.docking_1cbr[0])
     molj = copy.deepcopy(molecules.docking_1cbr[i])
@@ -852,6 +899,7 @@ def test_symmrmsd_atol_multi() -> None:
             ],
         ),
     ],
+    ids=["no_minimize", "minimize"],
 )
 def test_rmsdwrapper_nosymm_protein(minimize: bool, referenceRMSDs: List[float]):
     mol0 = copy.deepcopy(molecules.trp[0])
@@ -898,6 +946,7 @@ def test_rmsdwrapper_nosymm_protein(minimize: bool, referenceRMSDs: List[float])
             ],
         ),
     ],
+    ids=["minimize", "no_minimize"],
 )
 def test_rmsdwrapper_isomorphic(minimize: bool, referenceRMSDs: List[float]) -> None:
     molref = copy.deepcopy(molecules.docking_1cbr[0])
@@ -913,6 +962,7 @@ def test_rmsdwrapper_isomorphic(minimize: bool, referenceRMSDs: List[float]) ->
     # Reference results obtained with OpenBabel
     "minimize, referenceRMSD",
     [(True, 0.476858), (False, 0.592256)],
+    ids=["minimize", "no_minimize"],
 )
 def test_rmsdwrapper_single_molecule(minimize: bool, referenceRMSD: float) -> None:
     molref = copy.deepcopy(molecules.docking_1cbr[0])
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 34d0552..38df6ee 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -25,6 +25,7 @@ def test_molformat(extin: str, extout: str) -> None:
 @pytest.mark.parametrize(
     "deg, rad",
     [(0, 0), (90, np.pi / 2), (180, np.pi), (270, 3 * np.pi / 2), (360, 2 * np.pi)],
+    ids=["0", "90", "180", "270", "360"],
 )
 def test_deg_to_rad(deg: float, rad: float) -> None:
     assert utils.deg_to_rad(deg) == pytest.approx(rad)

From 62c5df31a5926cebfc7ae5bb4ab3a661e37443f2 Mon Sep 17 00:00:00 2001
From: Rocco Meli <r.meli@bluemail.ch>
Date: Mon, 8 Apr 2024 22:28:14 +0200
Subject: [PATCH 8/8] Test all backends together in CI (#118)

* move backend tests

* avoid cached parametrization

* remove conda configs

* make all tests work

* remove mols in favour of fixtures

* make molecules fixtures

* avoid graph-tool on windows

* simplify ci

* mention pip explicitly

* reset backend for good measure

* try no space

* windows

* changelog

* reset backend for every test

* add warning test

* Apply suggestions from code review

* format
---
 .github/workflows/pytest.yml                  |  15 +-
 CHANGELOG.md                                  |  14 ++
 ...l-gt.yaml => spyrmsd-test-obabel-all.yaml} |   3 +
 ...-nx.yaml => spyrmsd-test-obabel-nogt.yaml} |   2 +
 .../conda-envs/spyrmsd-test-obabel-rx.yaml    |  26 ---
 .../conda-envs/spyrmsd-test-rdkit-all.yaml    |   1 +
 .../conda-envs/spyrmsd-test-rdkit-gt.yaml     |  27 ---
 ...t-nx.yaml => spyrmsd-test-rdkit-nogt.yaml} |   2 +
 .../conda-envs/spyrmsd-test-rdkit-rx.yaml     |  27 ---
 tests/conftest.py                             |  97 +++++++++
 tests/molecules.py                            |  80 --------
 tests/test_backends.py                        | 103 ++++++++++
 tests/test_graph.py                           | 150 ++++++--------
 tests/test_hungarian.py                       |  17 +-
 tests/test_molecule.py                        | 186 +++++-------------
 tests/test_qcp.py                             |  24 +--
 tests/test_rmsd.py                            | 158 +++++++--------
 17 files changed, 447 insertions(+), 485 deletions(-)
 rename devtools/conda-envs/{spyrmsd-test-obabel-gt.yaml => spyrmsd-test-obabel-all.yaml} (87%)
 rename devtools/conda-envs/{spyrmsd-test-obabel-nx.yaml => spyrmsd-test-obabel-nogt.yaml} (92%)
 delete mode 100644 devtools/conda-envs/spyrmsd-test-obabel-rx.yaml
 delete mode 100644 devtools/conda-envs/spyrmsd-test-rdkit-gt.yaml
 rename devtools/conda-envs/{spyrmsd-test-rdkit-nx.yaml => spyrmsd-test-rdkit-nogt.yaml} (92%)
 delete mode 100644 devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml
 delete mode 100644 tests/molecules.py
 create mode 100644 tests/test_backends.py

diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 6dc9e06..4fbbf94 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -26,15 +26,16 @@ jobs:
         os: [macOS-latest, ubuntu-latest, windows-latest]
         python-version: ["3.9", "3.10", "3.11", "3.12"]
         chemlib: [obabel, rdkit]
-        graphlib: [nx, gt, rx, all]
+        graphlib: [nogt, all]
         exclude:
-        # graph-tools does not work on Windows
-        - {os: "windows-latest", graphlib: "gt"}
-        - {os: "windows-latest", graphlib: "all"}
-        - {graphlib: "all", chemlib: "obabel"}
+          # graph-tool does not work on Windows
+          - {os: "windows-latest", graphlib: "all"}
+          # Don't run without graph-tool on Ubuntu and macOS
+          - {os: "ubuntu-latest", graphlib: "nogt"}
+          - {os: "macOS-latest", graphlib: "nogt"}
         include:
-        - {os: "macOS-14", graphlib: "gt", chemlib: "obabel", python-version: "3.12"}
-        - {os: "macOS-14", graphlib: "nx", chemlib: "rdkit", python-version: "3.12"}
+          - {os: "macOS-14", graphlib: "all", chemlib: "obabel", python-version: "3.12"}
+          - {os: "macOS-14", graphlib: "all", chemlib: "rdkit", python-version: "3.12"}
 
     steps:
     - uses: actions/checkout@v3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 829dc7f..a6111ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,10 +8,24 @@
 Date:            XX/YY/ZZZZ
 Contributors:    @RMeli
 
+### Added
+
+* Warnings filter for tests of multiple backends [PR #118 | @RMeli]
+* Parametrized fixture to run tests with all available backends [PR #118 | @RMeli]
+
 ### Improved
 
 * Test IDs [PR #117 | @RMeli]
 
+### Changed
+
+* Location of backend tests to standalone file [PR #118 | @RMeli]
+
+### Removed
+
+* Many CI configurations in favour of running tests for all available backends [PR #118 | @RMeli]
+* `tests/molecule.py` in favour of fixtures [PR #118 | @RMeli]
+
 ## Version 0.7.0
 
 Date:            05/04/2024
diff --git a/devtools/conda-envs/spyrmsd-test-obabel-gt.yaml b/devtools/conda-envs/spyrmsd-test-obabel-all.yaml
similarity index 87%
rename from devtools/conda-envs/spyrmsd-test-obabel-gt.yaml
rename to devtools/conda-envs/spyrmsd-test-obabel-all.yaml
index 47476cb..d067db0 100644
--- a/devtools/conda-envs/spyrmsd-test-obabel-gt.yaml
+++ b/devtools/conda-envs/spyrmsd-test-obabel-all.yaml
@@ -4,12 +4,15 @@ channels:
 dependencies:
   # Base
   - python
+  - pip
   - setuptools
 
   # Maths
   - numpy
   - scipy
   - graph-tool
+  - networkx>=2
+  - rustworkx
 
   # Chemistry
   - openbabel
diff --git a/devtools/conda-envs/spyrmsd-test-obabel-nx.yaml b/devtools/conda-envs/spyrmsd-test-obabel-nogt.yaml
similarity index 92%
rename from devtools/conda-envs/spyrmsd-test-obabel-nx.yaml
rename to devtools/conda-envs/spyrmsd-test-obabel-nogt.yaml
index f16ceaa..bca1f0c 100644
--- a/devtools/conda-envs/spyrmsd-test-obabel-nx.yaml
+++ b/devtools/conda-envs/spyrmsd-test-obabel-nogt.yaml
@@ -4,12 +4,14 @@ channels:
 dependencies:
   # Base
   - python
+  - pip
   - setuptools
 
   # Maths
   - numpy
   - scipy
   - networkx>=2
+  - rustworkx
 
   # Chemistry
   - openbabel
diff --git a/devtools/conda-envs/spyrmsd-test-obabel-rx.yaml b/devtools/conda-envs/spyrmsd-test-obabel-rx.yaml
deleted file mode 100644
index af3cf83..0000000
--- a/devtools/conda-envs/spyrmsd-test-obabel-rx.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: spyrmsd
-channels:
-  - conda-forge
-dependencies:
-  # Base
-  - python
-  - setuptools
-
-  # Maths
-  - numpy
-  - scipy
-  - rustworkx
-
-  # Chemistry
-  - openbabel
-
-  # Testing
-  - pytest
-  - pytest-cov
-  - pytest-benchmark
-
-  # Dev
-  - mypy
-  - flake8
-  - black
-  - codecov
diff --git a/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml b/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
index d1d7ced..b2f2b9b 100644
--- a/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
+++ b/devtools/conda-envs/spyrmsd-test-rdkit-all.yaml
@@ -5,6 +5,7 @@ channels:
 dependencies:
   # Base
   - python
+  - pip
   - setuptools
 
   # Maths
diff --git a/devtools/conda-envs/spyrmsd-test-rdkit-gt.yaml b/devtools/conda-envs/spyrmsd-test-rdkit-gt.yaml
deleted file mode 100644
index 06e9dd9..0000000
--- a/devtools/conda-envs/spyrmsd-test-rdkit-gt.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: spyrmsd
-channels:
-  - conda-forge
-  - rdkit
-dependencies:
-  # Base
-  - python
-  - setuptools
-
-  # Maths
-  - numpy
-  - scipy
-  - graph-tool
-
-  # Chemistry
-  - rdkit
-
-  # Testing
-  - pytest
-  - pytest-cov
-  - pytest-benchmark
-
-  # Dev
-  - mypy
-  - flake8
-  - black
-  - codecov
diff --git a/devtools/conda-envs/spyrmsd-test-rdkit-nx.yaml b/devtools/conda-envs/spyrmsd-test-rdkit-nogt.yaml
similarity index 92%
rename from devtools/conda-envs/spyrmsd-test-rdkit-nx.yaml
rename to devtools/conda-envs/spyrmsd-test-rdkit-nogt.yaml
index e35ae1b..72b5429 100644
--- a/devtools/conda-envs/spyrmsd-test-rdkit-nx.yaml
+++ b/devtools/conda-envs/spyrmsd-test-rdkit-nogt.yaml
@@ -5,12 +5,14 @@ channels:
 dependencies:
   # Base
   - python
+  - pip
   - setuptools
 
   # Maths
   - numpy
   - scipy
   - networkx>=2
+  - rustworkx
 
   # Chemistry
   - rdkit
diff --git a/devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml b/devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml
deleted file mode 100644
index e752f97..0000000
--- a/devtools/conda-envs/spyrmsd-test-rdkit-rx.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: spyrmsd
-channels:
-  - conda-forge
-  - rdkit
-dependencies:
-  # Base
-  - python
-  - setuptools
-
-  # Maths
-  - numpy
-  - scipy
-  - rustworkx
-
-  # Chemistry
-  - rdkit
-
-  # Testing
-  - pytest
-  - pytest-cov
-  - pytest-benchmark
-
-  # Dev
-  - mypy
-  - flake8
-  - black
-  - codecov
diff --git a/tests/conftest.py b/tests/conftest.py
index 9c04244..ed7949b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,9 +5,18 @@
     https://docs.pytest.org/en/latest/example/simple.html
 """
 
+import os
+import warnings
+from collections import namedtuple
+
 import numpy as np
 import pytest
 
+import spyrmsd
+from spyrmsd import io
+
+Mol = namedtuple("Mol", ["mol", "name", "n_atoms", "n_bonds", "n_h"])
+
 
 def pytest_addoption(parser):
     parser.addoption(
@@ -69,3 +78,91 @@ def pytest_generate_tests(metafunc):
         n = metafunc.config.getoption("--n-tests")
 
         metafunc.parametrize("idx", np.random.randint(0, pytest.n_systems, size=n))
+
+
+@pytest.fixture(autouse=True, params=spyrmsd.available_backends)
+def set_backend(request):
+    # Capture warning when trying to switch to the same backend
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore")
+        spyrmsd.set_backend(request.param)
+
+
+@pytest.fixture(scope="session")
+def molpath():
+    fdir = os.path.dirname(os.path.abspath(__file__))
+    return os.path.join(fdir, f"data{os.sep}molecules")
+
+
+@pytest.fixture
+def benzene(molpath):
+    mol = io.loadmol(os.path.join(molpath, "benzene.sdf"))
+    return Mol(mol, "benzene", 12, 12, 6)
+
+
+@pytest.fixture
+def pyridine(molpath):
+    mol = io.loadmol(os.path.join(molpath, "pyridine.sdf"))
+    return Mol(mol, "pyridine", 11, 11, 5)
+
+
+@pytest.fixture(
+    params=[
+        # (name, n_atoms, n_bonds, n_h)
+        ("benzene", 12, 12, 6),
+        ("ethanol", 9, 8, 6),
+        ("pyridine", 11, 11, 5),
+        ("dialanine", 23, 22, 12),
+    ]
+)
+def mol(request, molpath):
+    """
+    Load molecule as sPyRMSD molecule.
+    """
+
+    name, n_atoms, n_bonds, n_h = request.param
+
+    mol = io.loadmol(os.path.join(molpath, f"{name}.sdf"))
+
+    return Mol(mol, name, n_atoms, n_bonds, n_h)
+
+
+@pytest.fixture
+def rawmol(mol, molpath):
+    """
+    Load molecule as a molecule of the current molecular I/O library.
+    """
+
+    RawMol = namedtuple(
+        "RawMol", ["mol", "rawmol", "name", "n_atoms", "n_bonds", "n_h"]
+    )
+
+    rawmol = io.load(os.path.join(molpath, f"{mol.name}.sdf"))
+
+    return RawMol(mol.mol, rawmol, mol.name, mol.n_atoms, mol.n_bonds, mol.n_h)
+
+
+@pytest.fixture
+def trps(molpath):
+    trp_list = []
+    for i in range(6):
+        trp_list.append(io.loadmol(os.path.join(molpath, f"trp{i}.pdb")))
+
+    return trp_list
+
+
+@pytest.fixture
+def docking_2viz(molpath):
+    mols = {}  # Dictionary (pose, molecule)
+    for i in [1, 2, 3]:
+        mols[i] = io.loadmol(os.path.join(molpath, f"2viz_{i}.sdf"))
+
+    return mols
+
+
+@pytest.fixture
+def docking_1cbr(molpath):
+    return [
+        io.loadmol(os.path.join(molpath, "1cbr_ligand.mol2")),
+        *io.loadallmols(os.path.join(molpath, "1cbr_docking.sdf")),
+    ]
diff --git a/tests/molecules.py b/tests/molecules.py
deleted file mode 100644
index 45cd20d..0000000
--- a/tests/molecules.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import os
-from typing import Any, List, Tuple
-
-from spyrmsd import io, molecule
-
-fdir = os.path.dirname(os.path.abspath(__file__))
-molpath = os.path.join(fdir, "data/molecules/")
-
-
-def load(fname: str) -> Tuple[Any, molecule.Molecule]:
-    """
-    Load molecule from file.
-
-    Parameters
-    ----------
-    fname: str
-        Input file name
-
-    Returns
-    -------
-    Tuple[Any, molecule.Molecule]
-        Loaded molecule as `pybel.Molecule` or `rdkit.Chem.rdkem.Mol` and
-        `pyrmsd.molecule.Molecule`
-    """
-
-    fname = os.path.join(molpath, fname)
-
-    m = io.load(fname)
-
-    mol = io.to_molecule(m, adjacency=True)
-
-    return m, mol
-
-
-def loadall(fname: str) -> Tuple[List[Any], List[molecule.Molecule]]:
-    """
-    Load all molecule from file.
-
-    Parameters
-    ----------
-    fname: str
-        Input file name
-
-    Returns
-    -------
-    Tuple[List[Any], List[molecule.Molecule]]
-        Loaded molecule as `pybel.Molecule` or `rdkit.Chem.rdchem.Mol` and
-        `pyrmsd.molecule.Molecule`
-    """
-
-    fname = os.path.join(molpath, fname)
-
-    ms = io.loadall(fname)
-
-    mols = [io.to_molecule(m, adjacency=True) for m in ms]
-
-    return ms, mols
-
-
-obbenzene, benzene = load("benzene.sdf")
-obpyridine, pyridine = load("pyridine.sdf")
-obethanol, ethanol = load("ethanol.sdf")
-obdialanine, dialanine = load("dialanine.sdf")
-obsdf = [obbenzene, obpyridine, obethanol, obdialanine]
-sdf = [benzene, pyridine, ethanol, dialanine]
-
-allmolecules = sdf
-allobmolecules = obsdf
-
-obdocking_2viz, docking_2viz = {}, {}
-for i in [1, 2, 3]:
-    obdocking_2viz[i], docking_2viz[i] = load(f"2viz_{i}.sdf")
-
-obdocking_1cbr = [load("1cbr_ligand.mol2")[0], *loadall("1cbr_docking.sdf")[0]]
-docking_1cbr = [load("1cbr_ligand.mol2")[1], *loadall("1cbr_docking.sdf")[1]]
-
-intrp, trp = [], []
-for i in range(6):
-    intrp.append(load(f"trp{i}.pdb")[0])
-    trp.append(load(f"trp{i}.pdb")[1])
diff --git a/tests/test_backends.py b/tests/test_backends.py
new file mode 100644
index 0000000..fa77e36
--- /dev/null
+++ b/tests/test_backends.py
@@ -0,0 +1,103 @@
+import numpy as np
+import pytest
+
+import spyrmsd
+from spyrmsd import graph
+
+# TODO: Run even with two backends installed
+
+
+@pytest.mark.filterwarnings(
+    "ignore::UserWarning"
+)  # Silence "The backend is already" warning
+@pytest.mark.skipif(
+    # Run test if all supported backends are installed
+    not set(spyrmsd.graph._supported_backends) <= set(spyrmsd.available_backends),
+    reason="Not all of the required backends are installed",
+)
+def test_set_backend() -> None:
+    import graph_tool as gt
+    import networkx as nx
+    import rustworkx as rx
+
+    A = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]])
+
+    spyrmsd.set_backend("networkx")
+    assert spyrmsd.get_backend() == "networkx"
+
+    Gnx = graph.graph_from_adjacency_matrix(A)
+    assert isinstance(Gnx, nx.Graph)
+
+    spyrmsd.set_backend("graph-tool")
+    assert spyrmsd.get_backend() == "graph_tool"
+
+    Ggt = graph.graph_from_adjacency_matrix(A)
+    assert isinstance(Ggt, gt.Graph)
+
+    spyrmsd.set_backend("rustworkx")
+    assert spyrmsd.get_backend() == "rustworkx"
+
+    Grx = graph.graph_from_adjacency_matrix(A)
+    assert isinstance(Grx, rx.PyGraph)
+
+
+def test_set_backend_unknown():
+    with pytest.raises(ValueError, match="backend is not recognized or supported"):
+        spyrmsd.set_backend("unknown")
+
+
+def test_set_backend_same():
+    current_backend = spyrmsd.get_backend()
+    with pytest.warns(UserWarning, match=f"The backend is already {current_backend}."):
+        spyrmsd.set_backend(current_backend)
+
+
+@pytest.mark.filterwarnings(
+    "ignore::UserWarning"
+)  # Silence "The backend is already" warning
+@pytest.mark.skipif(
+    # Run test if all supported backends are installed
+    not set(spyrmsd.graph._supported_backends) <= set(spyrmsd.available_backends),
+    reason="Not all of the required backends are installed",
+)
+def test_molecule_graph_cache(mol) -> None:
+    import graph_tool as gt
+    import networkx as nx
+    import rustworkx as rx
+
+    m = mol.mol
+
+    # Check molecules is in a clean state
+    assert len(m.G.items()) == 0
+    assert not m.stripped
+
+    spyrmsd.set_backend("networkx")
+    m.to_graph()
+
+    assert "networkx" in m.G.keys()
+    assert "graph_tool" not in m.G.keys()
+    assert "rustworkx" not in m.G.keys()
+
+    spyrmsd.set_backend("graph-tool")
+    m.to_graph()
+
+    assert "networkx" in m.G.keys()
+    assert "graph_tool" in m.G.keys()
+    assert "rustworkx" not in m.G.keys()
+
+    spyrmsd.set_backend("rustworkx")
+    m.to_graph()
+
+    assert "networkx" in m.G.keys()
+    assert "graph_tool" in m.G.keys()
+    assert "rustworkx" in m.G.keys()
+
+    # Make sure all backends (still) have a cache
+    assert isinstance(m.G["networkx"], nx.Graph)
+    assert isinstance(m.G["graph_tool"], gt.Graph)
+    assert isinstance(m.G["rustworkx"], rx.PyGraph)
+
+    # Strip molecule to ensure the cache is reset
+    m.strip()
+
+    assert len(m.G.items()) == 0
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 1d68cef..c633a70 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -2,10 +2,9 @@
 import pytest
 
 import spyrmsd
-from spyrmsd import constants, graph, io, molecule
+from spyrmsd import constants, graph, io
 from spyrmsd.exceptions import NonIsomorphicGraphs
 from spyrmsd.graphs import _common as gc
-from tests import molecules
 
 
 def test_adjacency_matrix_from_atomic_coordinates_distance() -> None:
@@ -28,43 +27,42 @@ def test_adjacency_matrix_from_atomic_coordinates_distance() -> None:
     assert graph.num_edges(G) == 1
 
 
-@pytest.mark.parametrize(
-    "mol, n_bonds",
-    [(molecules.benzene, 12), (molecules.ethanol, 8), (molecules.dialanine, 22)],
-    ids=["benzene", "ethanol", "dialanine"],
-)
-def test_adjacency_matrix_from_atomic_coordinates(
-    mol: molecule.Molecule, n_bonds: int
-) -> None:
-    A = graph.adjacency_matrix_from_atomic_coordinates(mol.atomicnums, mol.coordinates)
+def test_adjacency_matrix_from_atomic_coordinates(mol) -> None:
+    A = graph.adjacency_matrix_from_atomic_coordinates(
+        mol.mol.atomicnums, mol.mol.coordinates
+    )
 
     G = graph.graph_from_adjacency_matrix(A)
 
-    assert graph.num_vertices(G) == len(mol)
-    assert graph.num_edges(G) == n_bonds
+    assert graph.num_vertices(G) == mol.n_atoms
+    assert graph.num_edges(G) == mol.n_bonds
 
 
-@pytest.mark.parametrize("mol", molecules.allobmolecules)
-def test_adjacency_matrix_from_mol(mol) -> None:
-    natoms = io.numatoms(mol)
-    nbonds = io.numbonds(mol)
+def test_adjacency_matrix_from_mol(rawmol) -> None:
+    natoms = io.numatoms(rawmol.rawmol)
+    nbonds = io.numbonds(rawmol.rawmol)
 
-    A = io.adjacency_matrix(mol)
+    assert natoms == rawmol.n_atoms
+    assert nbonds == rawmol.n_bonds
+
+    A = io.adjacency_matrix(rawmol.rawmol)
 
     assert A.shape == (natoms, natoms)
     assert np.all(A == A.T)
     assert np.sum(A) == nbonds * 2
 
-    for i, j in io.bonds(mol):
+    for i, j in io.bonds(rawmol.rawmol):
         assert A[i, j] == 1
 
 
-@pytest.mark.parametrize("mol", molecules.allobmolecules)
-def test_graph_from_adjacency_matrix(mol) -> None:
-    natoms = io.numatoms(mol)
-    nbonds = io.numbonds(mol)
+def test_graph_from_adjacency_matrix(rawmol) -> None:
+    natoms = io.numatoms(rawmol.rawmol)
+    nbonds = io.numbonds(rawmol.rawmol)
+
+    assert natoms == rawmol.n_atoms
+    assert nbonds == rawmol.n_bonds
 
-    A = io.adjacency_matrix(mol)
+    A = io.adjacency_matrix(rawmol.rawmol)
 
     assert A.shape == (natoms, natoms)
     assert np.all(A == A.T)
@@ -76,14 +74,13 @@ def test_graph_from_adjacency_matrix(mol) -> None:
     assert graph.num_edges(G) == nbonds
 
 
-@pytest.mark.parametrize(
-    "rawmol, mol", zip(molecules.allobmolecules, molecules.allmolecules)
-)
-def test_graph_from_adjacency_matrix_atomicnums(rawmol, mol) -> None:
-    natoms = io.numatoms(rawmol)
-    nbonds = io.numbonds(rawmol)
+def test_graph_from_adjacency_matrix_atomicnums(rawmol) -> None:
+    mol = rawmol.mol
+
+    natoms = io.numatoms(rawmol.rawmol)
+    nbonds = io.numbonds(rawmol.rawmol)
 
-    A = io.adjacency_matrix(rawmol)
+    A = io.adjacency_matrix(rawmol.rawmol)
 
     assert len(mol) == natoms
     assert mol.adjacency_matrix.shape == (natoms, natoms)
@@ -99,28 +96,44 @@ def test_graph_from_adjacency_matrix_atomicnums(rawmol, mol) -> None:
         assert graph.vertex_property(G, "aprops", idx) == atomicnum
 
 
-@pytest.mark.parametrize(
-    "G1, G2",
-    [
-        *[(graph.lattice(n, n), graph.lattice(n, n)) for n in range(2, 5)],
-        *[(graph.cycle(n), graph.cycle(n)) for n in range(2, 5)],
-    ],
-)
-def test_match_graphs_isomorphic(G1, G2) -> None:
+@pytest.mark.parametrize("n", list(range(2, 5)))
+def test_match_graphs_isomorphic_lattice(n) -> None:
+    G1 = graph.lattice(n, n)
+    G2 = graph.lattice(n, n)
+
     with pytest.warns(UserWarning, match=gc.warn_no_atomic_properties):
         isomorphisms = graph.match_graphs(G1, G2)
 
     assert len(isomorphisms) != 0
 
 
-@pytest.mark.parametrize(
-    "G1, G2",
-    [
-        *[(graph.lattice(n, n), graph.lattice(n + 1, n)) for n in range(2, 5)],
-        *[(graph.cycle(n), graph.cycle(n + 1)) for n in range(1, 5)],
-    ],
-)
-def test_match_graphs_not_isomorphic(G1, G2) -> None:
+@pytest.mark.parametrize("n", list(range(2, 5)))
+def test_match_graphs_isomorphic_cycle(n) -> None:
+    G1 = graph.cycle(n)
+    G2 = graph.cycle(n)
+
+    with pytest.warns(UserWarning, match=gc.warn_no_atomic_properties):
+        isomorphisms = graph.match_graphs(G1, G2)
+
+    assert len(isomorphisms) != 0
+
+
+@pytest.mark.parametrize("n", list(range(2, 5)))
+def test_match_graphs_not_isomorphic_lattice(n) -> None:
+    G1 = graph.lattice(n, n)
+    G2 = graph.lattice(n + 1, n)
+
+    with pytest.raises(
+        NonIsomorphicGraphs, match=gc.error_non_isomorphic_graphs
+    ), pytest.warns(UserWarning, match=gc.warn_no_atomic_properties):
+        graph.match_graphs(G1, G2)
+
+
+@pytest.mark.parametrize("n", range(2, 5))
+def test_match_graphs_not_isomorphic_cycle(n) -> None:
+    G1 = graph.cycle(n)
+    G2 = graph.cycle(n + 1)
+
     with pytest.raises(
         NonIsomorphicGraphs, match=gc.error_non_isomorphic_graphs
     ), pytest.warns(UserWarning, match=gc.warn_no_atomic_properties):
@@ -144,48 +157,15 @@ def test_build_graph_node_features(property) -> None:
     assert graph.num_edges(G) == 3
 
 
-@pytest.mark.skipif(
-    spyrmsd.get_backend() != "graph_tool",
-    reason="NetworkX supports all Python objects as node properties.",
-)
 def test_build_graph_node_features_unsupported() -> None:
+    if spyrmsd.get_backend() != "graph-tool":
+        pytest.skip(
+            "NetworkX and RustworkX support all Python objects as node properties."
+        )
+
     A = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]])
 
     property = [True, False, True]
 
     with pytest.raises(ValueError, match="Unsupported property type:"):
         _ = graph.graph_from_adjacency_matrix(A, property)
-
-
-@pytest.mark.skipif(
-    # Run test if all supported backends are installed
-    not set(spyrmsd.graph._supported_backends) <= set(spyrmsd.available_backends),
-    reason="Not all of the required backends are installed",
-)
-def test_set_backend() -> None:
-    import graph_tool as gt
-    import networkx as nx
-    import rustworkx as rx
-
-    A = np.array([[0, 1, 1], [1, 0, 0], [1, 0, 1]])
-
-    spyrmsd.set_backend("networkx")
-    assert spyrmsd.get_backend() == "networkx"
-
-    Gnx = graph.graph_from_adjacency_matrix(A)
-    assert isinstance(Gnx, nx.Graph)
-
-    spyrmsd.set_backend("graph-tool")
-    assert spyrmsd.get_backend() == "graph_tool"
-
-    Ggt = graph.graph_from_adjacency_matrix(A)
-    assert isinstance(Ggt, gt.Graph)
-
-    spyrmsd.set_backend("rustworkx")
-    assert spyrmsd.get_backend() == "rustworkx"
-
-    Grx = graph.graph_from_adjacency_matrix(A)
-    assert isinstance(Grx, rx.PyGraph)
-
-    with pytest.raises(ValueError, match="backend is not recognized or supported"):
-        spyrmsd.set_backend("unknown")
diff --git a/tests/test_hungarian.py b/tests/test_hungarian.py
index 0532f1f..cc83c99 100644
--- a/tests/test_hungarian.py
+++ b/tests/test_hungarian.py
@@ -3,14 +3,12 @@
 import numpy as np
 import pytest
 
-from spyrmsd import hungarian, molecule
-from tests import molecules
+from spyrmsd import hungarian
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_cost_mtx(mol: molecule.Molecule):
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_cost_mtx(mol):
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     M = hungarian.cost_mtx(mol1.coordinates, mol2.coordinates)
 
@@ -27,10 +25,9 @@ def test_cost_mtx(mol: molecule.Molecule):
             assert M[i, j] == pytest.approx(np.dot(ab, ab))
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_optimal_assignement_same_molecule(mol: molecule.Molecule):
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_optimal_assignement_same_molecule(mol):
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     assert len(mol1) == len(mol2)
 
diff --git a/tests/test_molecule.py b/tests/test_molecule.py
index b0f6064..c216ee6 100644
--- a/tests/test_molecule.py
+++ b/tests/test_molecule.py
@@ -1,107 +1,90 @@
 import copy
 import os
 from collections import defaultdict
-from typing import DefaultDict, List, Tuple
+from typing import DefaultDict
 
 import numpy as np
 import pytest
 
-import spyrmsd
 from spyrmsd import constants, graph, io, molecule, utils
-from tests import molecules
 
 
-# atoms is a list of atomic numbers and atom counts
-@pytest.mark.parametrize(
-    "mol, atoms",
-    [
-        (molecules.benzene, [(1, 6), (6, 6)]),
-        (molecules.ethanol, [(1, 6), (6, 2), (8, 1)]),
-        (molecules.dialanine, [(1, 12), (6, 6), (7, 2), (8, 3)]),
-    ],
-    ids=["benzene", "ethanol", "dialanine"],
-)
-def test_load(mol: molecule.Molecule, atoms: List[Tuple[int, int]]) -> None:
-    n = sum([n_atoms for _, n_atoms in atoms])
+def test_load(mol) -> None:
+    # Atoms for each type
+    atoms = {
+        "benzene": [(1, 6), (6, 6)],
+        "ethanol": [(1, 6), (6, 2), (8, 1)],
+        "pyridine": [(1, 5), (6, 5), (7, 1)],
+        "dialanine": [(1, 12), (6, 6), (7, 2), (8, 3)],
+    }
 
-    assert len(mol) == n
-    assert mol.atomicnums.shape == (n,)
-    assert mol.coordinates.shape == (n, 3)
+    n = sum([n_atoms for _, n_atoms in atoms[mol.name]])
+
+    assert mol.n_atoms == n
+    assert mol.mol.atomicnums.shape == (n,)
+    assert mol.mol.coordinates.shape == (n, 3)
 
     # Count number of atoms of different elements
     atomcount: DefaultDict[int, int] = defaultdict(int)
-    for atomicnum in mol.atomicnums:
+    for atomicnum in mol.mol.atomicnums:
         atomcount[atomicnum] += 1
 
-    assert len(atomcount) == len(atoms)
+    assert len(atomcount) == len(atoms[mol.name])
 
-    for Z, n_atoms in atoms:
+    for Z, n_atoms in atoms[mol.name]:
         assert atomcount[Z] == n_atoms
 
 
-def test_loadall() -> None:
-    path = os.path.join(molecules.molpath, "1cbr_docking.sdf")
+def test_loadall(molpath) -> None:
+    path = os.path.join(molpath, "1cbr_docking.sdf")
 
     mols = io.loadall(path)
 
     assert len(mols) == 10
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_molecule_translate(mol: molecule.Molecule) -> None:
-    mt = copy.deepcopy(mol)
+def test_molecule_translate(mol) -> None:
+    mt = copy.deepcopy(mol.mol)
 
     t = np.array([0.5, 1.1, -0.1])
     mt.translate(t)
 
-    for tcoord, coord in zip(mt.coordinates, mol.coordinates):
+    for tcoord, coord in zip(mt.coordinates, mol.mol.coordinates):
         assert np.allclose(tcoord - t, coord)
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_molecule_rotate_z(mol: molecule.Molecule) -> None:
+def test_molecule_rotate_z(mol) -> None:
     z_axis = np.array([0, 0, 1])
 
     for angle in [0, 45, 90]:
-        rotated = np.zeros((len(mol), 3))
-        for i, coord in enumerate(mol.coordinates):
+        rotated = np.zeros((mol.n_atoms, 3))
+        for i, coord in enumerate(mol.mol.coordinates):
             rotated[i] = utils.rotate(coord, angle, z_axis, units="deg")
 
-        mol.rotate(angle, z_axis, units="deg")
-
-        assert np.allclose(mol.coordinates, rotated)
+        mol.mol.rotate(angle, z_axis, units="deg")
 
-        # Reset
-        mol.rotate(-angle, z_axis, units="deg")
+        assert np.allclose(mol.mol.coordinates, rotated)
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_molecule_rotate(mol: molecule.Molecule) -> None:
+def test_molecule_rotate(mol) -> None:
     axis = np.random.rand(3)
 
     for angle in np.random.rand(10) * 180:
-        rotated = np.zeros((len(mol), 3))
-        for i, coord in enumerate(mol.coordinates):
+        rotated = np.zeros((mol.n_atoms, 3))
+        for i, coord in enumerate(mol.mol.coordinates):
             rotated[i] = utils.rotate(coord, angle, axis, units="deg")
 
-        mol.rotate(angle, axis, units="deg")
+        mol.mol.rotate(angle, axis, units="deg")
 
-        assert np.allclose(mol.coordinates, rotated)
+        assert np.allclose(mol.mol.coordinates, rotated)
 
-        # Reset
-        mol.rotate(-angle, axis, units="deg")
 
+def test_molecule_center_of_geometry_benzene(benzene) -> None:
+    assert np.allclose(benzene.mol.center_of_geometry(), np.zeros(3))
 
-def test_molecule_center_of_geometry_benzene() -> None:
-    mol = molecules.benzene
 
-    assert np.allclose(mol.center_of_geometry(), np.zeros(3))
-
-
-def test_molecule_center_of_mass_benzene() -> None:
-    mol = molecules.benzene
-
-    assert np.allclose(mol.center_of_mass(), np.zeros(3))
+def test_molecule_center_of_mass_benzene(benzene) -> None:
+    assert np.allclose(benzene.mol.center_of_mass(), np.zeros(3))
 
 
 def test_molecule_center_of_mass_H2() -> None:
@@ -127,49 +110,28 @@ def test_molecule_center_of_mass_HF() -> None:
     assert np.allclose(mol.center_of_mass(), np.array([0, 0, z_com]))
 
 
-@pytest.mark.parametrize(
-    "mol, n_atoms, stripped",
-    [
-        (molecules.benzene, 12, 6),
-        (molecules.ethanol, 9, 6),
-        (molecules.dialanine, 23, 12),
-    ],
-    ids=["benzene", "ethanol", "dialanine"],
-)
-def test_molecule_strip(mol: molecule.Molecule, n_atoms: int, stripped: int) -> None:
-    m = copy.deepcopy(mol)
+def test_molecule_strip(mol) -> None:
+    m = copy.deepcopy(mol.mol)
 
-    assert len(m) == n_atoms
+    assert len(m) == mol.n_atoms
 
     m.strip()
 
-    assert len(m) == n_atoms - stripped
+    assert len(m) == mol.n_atoms - mol.n_h
 
 
-@pytest.mark.parametrize(
-    "mol, n_bonds",
-    [(molecules.benzene, 12), (molecules.ethanol, 8), (molecules.dialanine, 22)],
-    ids=["benzene", "ethanol", "dialanine"],
-)
-def test_graph_from_adjacency_matrix(mol: molecule.Molecule, n_bonds: int) -> None:
-    G = mol.to_graph()
+def test_graph_from_adjacency_matrix(mol) -> None:
+    G = mol.mol.to_graph()
 
-    assert graph.num_vertices(G) == len(mol)
-    assert graph.num_edges(G) == n_bonds
+    assert graph.num_vertices(G) == mol.n_atoms
+    assert graph.num_edges(G) == mol.n_bonds
 
-    for idx, atomicnum in enumerate(mol.atomicnums):
+    for idx, atomicnum in enumerate(mol.mol.atomicnums):
         assert graph.vertex_property(G, "aprops", idx) == atomicnum
 
 
-@pytest.mark.parametrize(
-    "mol, n_bonds",
-    [(molecules.benzene, 12), (molecules.ethanol, 8), (molecules.dialanine, 22)],
-    ids=["benzene", "ethanol", "dialanine"],
-)
-def test_graph_from_atomic_coordinates_perception(
-    mol: molecule.Molecule, n_bonds: int
-) -> None:
-    m = copy.deepcopy(mol)
+def test_graph_from_atomic_coordinates_perception(mol) -> None:
+    m = copy.deepcopy(mol.mol)
 
     delattr(m, "adjacency_matrix")
     m.G = {}
@@ -178,10 +140,10 @@ def test_graph_from_atomic_coordinates_perception(
         # Uses automatic bond perception
         G = m.to_graph()
 
-        assert graph.num_vertices(G) == len(m)
-        assert graph.num_edges(G) == n_bonds
+        assert graph.num_vertices(G) == mol.n_atoms
+        assert graph.num_edges(G) == mol.n_bonds
 
-        for idx, atomicnum in enumerate(mol.atomicnums):
+        for idx, atomicnum in enumerate(mol.mol.atomicnums):
             assert graph.vertex_property(G, "aprops", idx) == atomicnum
 
 
@@ -190,13 +152,13 @@ def test_graph_from_atomic_coordinates_perception(
     [True, False],
     ids=["adjacency", "no_adjacency"],
 )
-def test_from_obmol(adjacency):
+def test_from_obmol(molpath, adjacency):
     pytest.importorskip("openbabel")
 
     from spyrmsd.optional import obabel as ob
 
     # Load molecules with OpenBabel
-    path = os.path.join(molecules.molpath, "1cbr_docking.sdf")
+    path = os.path.join(molpath, "1cbr_docking.sdf")
     mols = ob.loadall(path)
 
     # Convert OpenBabel molecules to spyrmsd molecules
@@ -220,13 +182,13 @@ def test_from_obmol(adjacency):
     [True, False],
     ids=["adjacency", "no_adjacency"],
 )
-def test_from_rdmol(adjacency):
+def test_from_rdmol(molpath, adjacency):
     pytest.importorskip("rdkit")
 
     from spyrmsd.optional import rdkit as rd
 
     # Load molecules with RDKit
-    path = os.path.join(molecules.molpath, "1cbr_docking.sdf")
+    path = os.path.join(molpath, "1cbr_docking.sdf")
     mols = rd.loadall(path)
 
     # Convert OpenBabel molecules to spyrmsd molecules
@@ -243,43 +205,3 @@ def test_from_rdmol(adjacency):
             with pytest.raises(AttributeError):
                 # No adjacency_matrix attribute
                 mol.adjacency_matrix
-
-
-@pytest.mark.skipif(
-    # Run test if all supported backends are installed
-    not set(spyrmsd.graph._supported_backends) <= set(spyrmsd.available_backends),
-    reason="Not all of the required backends are installed",
-)
-@pytest.mark.parametrize(
-    "mol",
-    [(molecules.benzene), (molecules.ethanol), (molecules.dialanine)],
-    ids=["benzene", "ethanol", "dialanine"],
-)
-def test_molecule_graph_cache(mol) -> None:
-    import graph_tool as gt
-    import networkx as nx
-    import rustworkx as rx
-
-    ## Graph cache persists from previous tests, manually reset them
-    mol.G = {}
-    spyrmsd.set_backend("networkx")
-    mol.to_graph()
-
-    assert isinstance(mol.G["networkx"], nx.Graph)
-    assert "graph_tool" not in mol.G.keys()
-
-    spyrmsd.set_backend("graph-tool")
-    mol.to_graph()
-
-    spyrmsd.set_backend("rustworkx")
-    mol.to_graph()
-
-    ## Make sure all backends (still) have a cache
-    assert isinstance(mol.G["networkx"], nx.Graph)
-    assert isinstance(mol.G["graph_tool"], gt.Graph)
-    assert isinstance(mol.G["rustworkx"], rx.PyGraph)
-
-    ## Strip the molecule to ensure the cache is reset
-    mol.strip()
-
-    assert len(mol.G.items()) == 0
diff --git a/tests/test_qcp.py b/tests/test_qcp.py
index 3556eb0..2b72d93 100644
--- a/tests/test_qcp.py
+++ b/tests/test_qcp.py
@@ -5,14 +5,12 @@
 import numpy as np
 import pytest
 
-from spyrmsd import molecule, qcp
-from tests import molecules
+from spyrmsd import qcp
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_M_mtx(mol: molecule.Molecule) -> None:
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_M_mtx(mol) -> None:
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     # Build rotated coordinate set
     mol2.rotate(10, np.random.rand(3))
@@ -29,10 +27,9 @@ def S(i, j):
         assert M[i, j] == pytest.approx(S(i, j))
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_K_mtx(mol: molecule.Molecule) -> None:
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_K_mtx(mol) -> None:
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     # Build rotated coordinate set
     mol2.rotate(10, np.random.rand(3))
@@ -63,10 +60,9 @@ def test_lambda_max(
     assert qcp.lambda_max(*input) == pytest.approx(result)
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_lambda_max_eig(mol: molecule.Molecule) -> None:
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_lambda_max_eig(mol) -> None:
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     # Build rotated coordinate set
     mol2.rotate(10, np.random.rand(3))
diff --git a/tests/test_rmsd.py b/tests/test_rmsd.py
index f442d73..57e45c7 100644
--- a/tests/test_rmsd.py
+++ b/tests/test_rmsd.py
@@ -4,8 +4,7 @@
 import numpy as np
 import pytest
 
-from spyrmsd import molecule, qcp, rmsd
-from tests import molecules
+from spyrmsd import qcp, rmsd
 
 
 @pytest.fixture(
@@ -42,9 +41,9 @@ def lambda_max_failure(Ga, Gb, c2, c1, c0):
 @pytest.mark.parametrize(
     "t, RMSD", [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)], ids=["t0", "t1", "t2"]
 )
-def test_rmsd_benzene(t: float, RMSD: float) -> None:
-    mol1 = copy.deepcopy(molecules.benzene)
-    mol2 = copy.deepcopy(molecules.benzene)
+def test_rmsd_benzene(benzene, t: float, RMSD: float) -> None:
+    mol1 = copy.deepcopy(benzene.mol)
+    mol2 = copy.deepcopy(benzene.mol)
 
     mol2.translate(np.array([0, 0, t]))
 
@@ -60,9 +59,9 @@ def test_rmsd_benzene(t: float, RMSD: float) -> None:
     [(1, 2, 2.60065218), (1, 3, 9.94411523), (2, 3, 9.4091711)],
     ids=["1-2", "1-3", "2-3"],
 )
-def test_rmsd_2viz(i: int, j: int, result: float) -> None:
-    moli = copy.deepcopy(molecules.docking_2viz[i])
-    molj = copy.deepcopy(molecules.docking_2viz[j])
+def test_rmsd_2viz(docking_2viz, i: int, j: int, result: float) -> None:
+    moli = copy.deepcopy(docking_2viz[i])
+    molj = copy.deepcopy(docking_2viz[j])
 
     assert rmsd.rmsd(
         moli.coordinates, molj.coordinates, moli.atomicnums, molj.atomicnums
@@ -76,9 +75,9 @@ def test_rmsd_2viz(i: int, j: int, result: float) -> None:
     [(1, 2, 2.65327362), (1, 3, 10.11099065), (2, 3, 9.57099612)],
     ids=["1-2", "1-3", "2-3"],
 )
-def test_rmsd_2viz_stripped(i: int, j: int, result: float) -> None:
-    moli = copy.deepcopy(molecules.docking_2viz[i])
-    molj = copy.deepcopy(molecules.docking_2viz[j])
+def test_rmsd_2viz_stripped(docking_2viz, i: int, j: int, result: float) -> None:
+    moli = copy.deepcopy(docking_2viz[i])
+    molj = copy.deepcopy(docking_2viz[j])
 
     moli.strip()
     molj.strip()
@@ -88,9 +87,9 @@ def test_rmsd_2viz_stripped(i: int, j: int, result: float) -> None:
     ) == pytest.approx(result)
 
 
-def test_rmsd_centred_benzene() -> None:
-    mol1 = copy.deepcopy(molecules.benzene)
-    mol2 = copy.deepcopy(molecules.benzene)
+def test_rmsd_centred_benzene(benzene) -> None:
+    mol1 = copy.deepcopy(benzene.mol)
+    mol2 = copy.deepcopy(benzene.mol)
 
     mol2.translate(np.array([0, 0, 1]))
 
@@ -107,10 +106,9 @@ def test_rmsd_centred_benzene() -> None:
     ) == pytest.approx(0)
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_rmsd_minimize(mol: molecule.Molecule) -> None:
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_rmsd_minimize(mol) -> None:
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     assert rmsd.rmsd(
         mol1.coordinates, mol2.coordinates, mol1.atomicnums, mol2.atomicnums
@@ -151,9 +149,9 @@ def test_rmsd_minimize(mol: molecule.Molecule) -> None:
     [(1, 2, 1.95277757), (1, 3, 3.11801105), (2, 3, 2.98609758)],
     ids=["1-2", "1-3", "2-3"],
 )
-def test_rmsd_qcp_2viz(i: int, j: int, result: float) -> None:
-    moli = copy.deepcopy(molecules.docking_2viz[i])
-    molj = copy.deepcopy(molecules.docking_2viz[j])
+def test_rmsd_qcp_2viz(docking_2viz, i: int, j: int, result: float) -> None:
+    moli = copy.deepcopy(docking_2viz[i])
+    molj = copy.deepcopy(docking_2viz[j])
 
     assert rmsd.rmsd(
         moli.coordinates,
@@ -171,9 +169,9 @@ def test_rmsd_qcp_2viz(i: int, j: int, result: float) -> None:
     [(1, 2, 1.98171656), (1, 3, 3.01799306), (2, 3, 2.82917355)],
     ids=["1-2", "1-3", "2-3"],
 )
-def test_rmsd_qcp_2viz_stripped(i: int, j: int, result: float) -> None:
-    moli = copy.deepcopy(molecules.docking_2viz[i])
-    molj = copy.deepcopy(molecules.docking_2viz[j])
+def test_rmsd_qcp_2viz_stripped(docking_2viz, i: int, j: int, result: float) -> None:
+    moli = copy.deepcopy(docking_2viz[i])
+    molj = copy.deepcopy(docking_2viz[j])
 
     # Strip hydrogen atoms
     moli.strip()
@@ -208,9 +206,9 @@ def test_rmsd_qcp_2viz_stripped(i: int, j: int, result: float) -> None:
     ],
     ids=["1", "2", "3", "4", "5"],
 )
-def test_rmsd_qcp_protein(i: int, rmsd_dummy: float, rmsd_min: float):
-    mol0 = copy.deepcopy(molecules.trp[0])
-    mol = copy.deepcopy(molecules.trp[i])
+def test_rmsd_qcp_protein(trps, i: int, rmsd_dummy: float, rmsd_min: float):
+    mol0 = copy.deepcopy(trps[0])
+    mol = copy.deepcopy(trps[i])
 
     assert rmsd.rmsd(
         mol0.coordinates, mol.coordinates, mol0.atomicnums, mol.atomicnums
@@ -230,9 +228,9 @@ def test_rmsd_qcp_protein(i: int, rmsd_dummy: float, rmsd_min: float):
     [(60, 1e-4), (120, 1e-4), (180, 1e-4), (240, 1e-4), (300, 1e-4)],
     ids=["60", "120", "180", "240", "300"],
 )
-def test_rmsd_hungarian_benzene_rotated(angle: float, tol: float) -> None:
-    mol1 = copy.deepcopy(molecules.benzene)
-    mol2 = copy.deepcopy(molecules.benzene)
+def test_rmsd_hungarian_benzene_rotated(benzene, angle: float, tol: float) -> None:
+    mol1 = copy.deepcopy(benzene.mol)
+    mol2 = copy.deepcopy(benzene.mol)
 
     assert rmsd.rmsd(
         mol1.coordinates, mol2.coordinates, mol1.atomicnums, mol2.atomicnums
@@ -263,10 +261,10 @@ def test_rmsd_hungarian_benzene_rotated(angle: float, tol: float) -> None:
     ids=["60", "120", "180", "240", "300"],
 )
 def test_rmsd_hungarian_benzene_shifted_rotated(
-    d: float, angle: float, tol: float
+    benzene, d: float, angle: float, tol: float
 ) -> None:
-    mol1 = copy.deepcopy(molecules.benzene)
-    mol2 = copy.deepcopy(molecules.benzene)
+    mol1 = copy.deepcopy(benzene.mol)
+    mol2 = copy.deepcopy(benzene.mol)
 
     mol2.translate([0, 0, d])
 
@@ -289,10 +287,9 @@ def test_rmsd_hungarian_benzene_shifted_rotated(
     ) == pytest.approx(abs(d), abs=tol)
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_rmsd_hungarian_centred(mol: molecule.Molecule) -> None:
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_rmsd_hungarian_centred(mol) -> None:
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     mol2.translate(np.random.rand(3))
 
@@ -310,10 +307,9 @@ def test_rmsd_hungarian_centred(mol: molecule.Molecule) -> None:
     ) == pytest.approx(0)
 
 
-@pytest.mark.parametrize("mol", molecules.allmolecules)
-def test_symmrmsd_centred(mol: molecule.Molecule) -> None:
-    mol1 = copy.deepcopy(mol)
-    mol2 = copy.deepcopy(mol)
+def test_symmrmsd_centred(mol) -> None:
+    mol1 = copy.deepcopy(mol.mol)
+    mol2 = copy.deepcopy(mol.mol)
 
     mol2.translate(np.random.rand(3))
 
@@ -341,9 +337,9 @@ def test_symmrmsd_centred(mol: molecule.Molecule) -> None:
 
 
 @pytest.mark.parametrize("angle", [60, 120, 180, 240, 300, 360])
-def test_symmrmsd_rotated_benzene(angle: float) -> None:
-    mol1 = copy.deepcopy(molecules.benzene)
-    mol2 = copy.deepcopy(molecules.benzene)
+def test_symmrmsd_rotated_benzene(benzene, angle: float) -> None:
+    mol1 = copy.deepcopy(benzene.mol)
+    mol2 = copy.deepcopy(benzene.mol)
 
     mol2.rotate(angle, np.array([0, 0, 1]), units="deg")
 
@@ -375,9 +371,9 @@ def test_symmrmsd_rotated_benzene(angle: float) -> None:
 
 
 @pytest.mark.parametrize("angle", [60, 120, 180, 240, 300, 360])
-def test_symmrmsd_rotated_benzene_stripped(angle: float) -> None:
-    mol1 = copy.deepcopy(molecules.benzene)
-    mol2 = copy.deepcopy(molecules.benzene)
+def test_symmrmsd_rotated_benzene_stripped(benzene, angle: float) -> None:
+    mol1 = copy.deepcopy(benzene.mol)
+    mol2 = copy.deepcopy(benzene.mol)
 
     mol2.rotate(angle, np.array([0, 0, 1]), units="deg")
 
@@ -411,9 +407,9 @@ def test_symmrmsd_rotated_benzene_stripped(angle: float) -> None:
     ) == pytest.approx(0, abs=1e-4)
 
 
-def test_symmrmsd_atomicnums_matching_pyridine_stripped() -> None:
-    mol1 = copy.deepcopy(molecules.pyridine)
-    mol2 = copy.deepcopy(molecules.pyridine)
+def test_symmrmsd_atomicnums_matching_pyridine_stripped(pyridine) -> None:
+    mol1 = copy.deepcopy(pyridine.mol)
+    mol2 = copy.deepcopy(pyridine.mol)
 
     mol2.rotate(60, np.array([0, 0, 1]), units="deg")
 
@@ -485,9 +481,9 @@ def test_symmrmsd_atomicnums_matching_pyridine_stripped() -> None:
         "10-minimize",
     ],
 )
-def test_rmsd_symmrmsd(index: int, RMSD: float, minimize: bool) -> None:
-    molc = copy.deepcopy(molecules.docking_1cbr[0])
-    mol = copy.deepcopy(molecules.docking_1cbr[index])
+def test_rmsd_symmrmsd(docking_1cbr, index: int, RMSD: float, minimize: bool) -> None:
+    molc = copy.deepcopy(docking_1cbr[0])
+    mol = copy.deepcopy(docking_1cbr[index])
 
     molc.strip()
     mol.strip()
@@ -551,9 +547,11 @@ def test_rmsd_symmrmsd_disconnected_node() -> None:
     ],
     ids=["no_minimize", "minimize"],
 )
-def test_multi_spyrmsd(minimize: bool, referenceRMSDs: List[float]) -> None:
-    molc = copy.deepcopy(molecules.docking_1cbr[0])
-    mols = [copy.deepcopy(mol) for mol in molecules.docking_1cbr[1:]]
+def test_multi_spyrmsd(
+    docking_1cbr, minimize: bool, referenceRMSDs: List[float]
+) -> None:
+    molc = copy.deepcopy(docking_1cbr[0])
+    mols = [copy.deepcopy(mol) for mol in docking_1cbr[1:]]
 
     molc.strip()
 
@@ -611,9 +609,11 @@ def test_multi_spyrmsd(minimize: bool, referenceRMSDs: List[float]) -> None:
     ],
     ids=["no_minimize", "minimize"],
 )
-def test_symmrmsd_cache(minimize: bool, referenceRMSDs: List[float]) -> None:
-    molc = copy.deepcopy(molecules.docking_1cbr[0])
-    mols = [copy.deepcopy(mol) for mol in molecules.docking_1cbr[1:]]
+def test_symmrmsd_cache(
+    docking_1cbr, minimize: bool, referenceRMSDs: List[float]
+) -> None:
+    molc = copy.deepcopy(docking_1cbr[0])
+    mols = [copy.deepcopy(mol) for mol in docking_1cbr[1:]]
 
     molc.strip()
 
@@ -756,7 +756,7 @@ def test_issue_35_2():
     [(1, 2, 1.95277757), (1, 3, 3.11801105), (2, 3, 2.98609758)],
     ids=["1-2", "1-3", "2-3"],
 )
-def test_rmsd_atol(i: int, j: int, result: float):
+def test_rmsd_atol(docking_2viz, i: int, j: int, result: float):
     """
     Test usage of the :code:`atol` parameter for the QCP method.
 
@@ -764,8 +764,8 @@ def test_rmsd_atol(i: int, j: int, result: float):
     (https://github.com/RMeli/spyrmsd/issues/35)
     """
 
-    moli = copy.deepcopy(molecules.docking_2viz[i])
-    molj = copy.deepcopy(molecules.docking_2viz[j])
+    moli = copy.deepcopy(docking_2viz[i])
+    molj = copy.deepcopy(docking_2viz[j])
 
     # Check results are different from 0.0
     assert not result == pytest.approx(0.0)
@@ -792,9 +792,9 @@ def test_rmsd_atol(i: int, j: int, result: float):
 @pytest.mark.parametrize(
     "i, reference", [(1, 0.476858), (2, 1.68089), (3, 1.50267)], ids=["1", "2", "3"]
 )
-def test_symmrmsd_atol(i: bool, reference: float) -> None:
-    moli = copy.deepcopy(molecules.docking_1cbr[0])
-    molj = copy.deepcopy(molecules.docking_1cbr[i])
+def test_symmrmsd_atol(docking_1cbr, i: bool, reference: float) -> None:
+    moli = copy.deepcopy(docking_1cbr[0])
+    molj = copy.deepcopy(docking_1cbr[i])
 
     moli.strip()
     molj.strip()
@@ -824,11 +824,11 @@ def test_symmrmsd_atol(i: bool, reference: float) -> None:
     ) == pytest.approx(0.0)
 
 
-def test_symmrmsd_atol_multi() -> None:
+def test_symmrmsd_atol_multi(docking_1cbr) -> None:
     references = [0.476858, 1.68089, 1.50267]
 
-    molc = copy.deepcopy(molecules.docking_1cbr[0])
-    mols = [copy.deepcopy(mol) for mol in molecules.docking_1cbr[1:4]]
+    molc = copy.deepcopy(docking_1cbr[0])
+    mols = [copy.deepcopy(mol) for mol in docking_1cbr[1:4]]
 
     molc.strip()
 
@@ -901,9 +901,9 @@ def test_symmrmsd_atol_multi() -> None:
     ],
     ids=["no_minimize", "minimize"],
 )
-def test_rmsdwrapper_nosymm_protein(minimize: bool, referenceRMSDs: List[float]):
-    mol0 = copy.deepcopy(molecules.trp[0])
-    mols = [copy.deepcopy(mol) for mol in molecules.trp[1:]]
+def test_rmsdwrapper_nosymm_protein(trps, minimize: bool, referenceRMSDs: List[float]):
+    mol0 = copy.deepcopy(trps[0])
+    mols = [copy.deepcopy(mol) for mol in trps[1:]]
 
     RMSDs = rmsd.rmsdwrapper(mol0, mols, symmetry=False, minimize=minimize, strip=False)
 
@@ -948,9 +948,11 @@ def test_rmsdwrapper_nosymm_protein(minimize: bool, referenceRMSDs: List[float])
     ],
     ids=["minimize", "no_minimize"],
 )
-def test_rmsdwrapper_isomorphic(minimize: bool, referenceRMSDs: List[float]) -> None:
-    molref = copy.deepcopy(molecules.docking_1cbr[0])
-    mols = [copy.deepcopy(mol) for mol in molecules.docking_1cbr[1:]]
+def test_rmsdwrapper_isomorphic(
+    docking_1cbr, minimize: bool, referenceRMSDs: List[float]
+) -> None:
+    molref = copy.deepcopy(docking_1cbr[0])
+    mols = [copy.deepcopy(mol) for mol in docking_1cbr[1:]]
 
     RMSDs = rmsd.rmsdwrapper(molref, mols, minimize=minimize, strip=True)
 
@@ -964,9 +966,11 @@ def test_rmsdwrapper_isomorphic(minimize: bool, referenceRMSDs: List[float]) ->
     [(True, 0.476858), (False, 0.592256)],
     ids=["minimize", "no_minimize"],
 )
-def test_rmsdwrapper_single_molecule(minimize: bool, referenceRMSD: float) -> None:
-    molref = copy.deepcopy(molecules.docking_1cbr[0])
-    mols = copy.deepcopy(molecules.docking_1cbr[1])
+def test_rmsdwrapper_single_molecule(
+    docking_1cbr, minimize: bool, referenceRMSD: float
+) -> None:
+    molref = copy.deepcopy(docking_1cbr[0])
+    mols = copy.deepcopy(docking_1cbr[1])
 
     RMSD = rmsd.rmsdwrapper(molref, mols, minimize=minimize, strip=True)