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 @@ [](https://spyrmsd.readthedocs.io/en/develop/?badge=develop) [](https://opensource.org/licenses/MIT) -[](https://pypi.org/project/spyrmsd/) +[](https://pypi.org/project/spyrmsd/) [](https://anaconda.org/conda-forge/spyrmsd) [](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)