diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index f28604e..2fa5744 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -16,9 +16,9 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 23fda8a..ebaf04f 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -16,9 +16,9 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 218a798..829580e 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 6dc9e06..7f2979a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -26,18 +26,19 @@ 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 + - uses: actions/checkout@v4 - name: Info shell: bash @@ -69,7 +70,7 @@ jobs: pytest -v --benchmark --cov=spyrmsd --cov-report=xml --color=yes tests/ - name: CodeCov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: unittests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6da5c7c..f90cebf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ exclude: | )$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-json - id: check-merge-conflict @@ -14,20 +14,20 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 24.4.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort args: ["--profile", "black"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.9.0 hooks: - id: mypy additional_dependencies: [types-requests] diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f0996..20cd53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,32 @@ ------------------------------------------------------------------------------ +## Version 0.8.0 + +Date: 28/05/2024 +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 + +* the default backend for `pip` installations to `rustworkx` [PR#122 | @RMeli] +* `pre-commit` versions [PR #120 | @RMeli] +* Versions of several GitHub Actions [PR #120 | @RMeli] +* 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 @@ -10,7 +36,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] @@ -19,7 +45,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/CITATION.cff b/CITATION.cff index 07a57a1..7d90190 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.7.0 +version: 0.8.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 e2e5bb8..2b3f000 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 the other backends for higher performance. +> `spyrmsd` will install [rustworkx] (multi-platform) when using `pip` or `conda`. You can install other backends manually. > [!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`. @@ -76,11 +76,11 @@ The following packages are required to use `spyrmsd` as a module: One of the following graph libraries is required: * [graph-tool] -* [NetworkX] * [rustworkx] +* [NetworkX] > [!NOTE] -> `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. +> `spyrmsd` uses the following priority when multiple graph libraries are present: [graph-tool], [rustworkx], [NetworkX]. *This order might change. Use `set_backend` to ensure you are always using the same backend, if needed.* #### Standalone Tool @@ -178,10 +178,10 @@ To ensure code quality and consistency the following tools are used during devel * [black](https://black.readthedocs.io/en/stable/) * [Flake 8](http://flake8.pycqa.org/en/latest/) (CI) -* [isort]() +* [isort](https://pycqa.github.io/isort/) * [mypy](http://mypy-lang.org/) (CI) -Pre-commit `git` hooks can be installed with [pre-commit](https://pre-commit.com/). +Pre-commit `git` hooks can be installed with [pre-commit]. ## Copyright @@ -198,3 +198,4 @@ Project based on the [Computational Molecular Science Python Cookiecutter](https [rustworkx]: https://www.rustworkx.org [NetworkX]: https://networkx.github.io/ [graph-tool]: https://graph-tool.skewed.de/ +[pre-commit]: https://pre-commit.com/ diff --git a/devtools/conda-envs/spyrmsd-all.yaml b/devtools/conda-envs/spyrmsd-all.yaml index d47e18d..f78222e 100644 --- a/devtools/conda-envs/spyrmsd-all.yaml +++ b/devtools/conda-envs/spyrmsd-all.yaml @@ -16,8 +16,9 @@ dependencies: - numpy - pandas - scipy - - networkx>=2 - graph-tool + - rustworkx + - networkx>=2 - scikit-learn # Chemistry diff --git a/devtools/conda-envs/spyrmsd-docs.yaml b/devtools/conda-envs/spyrmsd-docs.yaml index 7730ceb..6986367 100644 --- a/devtools/conda-envs/spyrmsd-docs.yaml +++ b/devtools/conda-envs/spyrmsd-docs.yaml @@ -12,6 +12,7 @@ dependencies: - scipy - networkx>=2 - graph-tool + - rustworkx # Chemistry - openbabel 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-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.yaml b/devtools/conda-envs/spyrmsd.yaml index 9a76a89..be0c766 100644 --- a/devtools/conda-envs/spyrmsd.yaml +++ b/devtools/conda-envs/spyrmsd.yaml @@ -9,4 +9,5 @@ dependencies: # Maths - numpy - scipy + - rustworkx - networkx diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 31497c3..058432e 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -44,12 +44,12 @@ Module The following packages are required to use ``spyrmsd`` as a module: -* graph-tool_ or NetworkX_ +* graph-tool_, rustworkx_, or NetworkX_ * numpy_ * scipy_ .. note:: - ``spyrmsd`` uses graph-tool_ by default but will fall back to NetworkX_ if the former is not installed (e.g. on Windows). + ``spyrmsd`` uses graph-tool_ by default but will fall back to either rustworkx_ or NetworkX_ if the former is not installed (e.g. on Windows). Standalone Tool ~~~~~~~~~~~~~~~ @@ -67,3 +67,4 @@ Additionally, one of the following packages is required to use ``spyrmsd`` as a .. _NetworkX: https://networkx.github.io/ .. _numpy: https://numpy.org/ .. _scipy: https://www.scipy.org/ +.. _rustworkx: https://www.rustworkx.org diff --git a/pyproject.toml b/pyproject.toml index 39b18ef..9f9c372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,4 +13,4 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3",] requires-python = ">=3.9" -requires = ["numpy", "scipy", "networkx>=2"] +requires = ["numpy", "scipy", "rustworkx"] diff --git a/setup.py b/setup.py index d819573..c037c1c 100644 --- a/setup.py +++ b/setup.py @@ -23,16 +23,17 @@ description=short_description[0], long_description=long_description, long_description_content_type="text/markdown", - version="0.7.0-dev", + version="0.8.0", license="MIT", packages=find_packages(), include_package_data=True, url="https://spyrmsd.readthedocs.io", - install_requires=["numpy", "scipy", "networkx>=2"], + install_requires=["numpy", "scipy", "rustworkx"], extras_require={ "bib": ["duecredit"], "rdkit": ["rdkit"], "openbabel": ["openbabel"], + "networkx": ["networkx"], }, platforms=["Linux", "Mac OS-X", "Unix", "Windows"], python_requires=">=3.9", diff --git a/spyrmsd/__init__.py b/spyrmsd/__init__.py index b2f849b..c6bef8c 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" # This will print latest Zenodo version due.cite( diff --git a/spyrmsd/__main__.py b/spyrmsd/__main__.py index 4e8ba96..1de4a72 100644 --- a/spyrmsd/__main__.py +++ b/spyrmsd/__main__.py @@ -66,8 +66,8 @@ warnings.simplefilter("ignore") spyrmsd.set_backend(args.graph_backend) - if args.verbose: - print(f"Graph library: {spyrmsd.get_backend()}") + if args.verbose: + print(f"Graph library: {spyrmsd.get_backend()}") # Loop over molecules within fil RMSDlist = rmsdwrapper( diff --git a/spyrmsd/graph.py b/spyrmsd/graph.py index f777eed..65e44ee 100644 --- a/spyrmsd/graph.py +++ b/spyrmsd/graph.py @@ -5,7 +5,10 @@ from spyrmsd import constants -_supported_backends = ("graph_tool", "networkx", "rustworkx") +# The first backend found from this list is set as default +# TODO: Need to determine if graph_tool or rustworkx is better +# NetworkX is slow, therefore it is the last resort +_supported_backends = ("graph_tool", "rustworkx", "networkx") _available_backends = [] _current_backend = None 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_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..373071d 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,42 +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)], -) -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) @@ -75,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 - A = io.adjacency_matrix(rawmol) + natoms = io.numatoms(rawmol.rawmol) + nbonds = io.numbonds(rawmol.rawmol) + + A = io.adjacency_matrix(rawmol.rawmol) assert len(mol) == natoms assert mol.adjacency_matrix.shape == (natoms, natoms) @@ -98,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): @@ -148,6 +162,11 @@ def test_build_graph_node_features(property) -> None: 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] 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_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..c216ee6 100644 --- a/tests/test_molecule.py +++ b/tests/test_molecule.py @@ -1,106 +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)]), - ], -) -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: @@ -126,46 +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), - ], -) -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)], -) -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)], -) -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 = {} @@ -174,24 +140,25 @@ 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 @pytest.mark.parametrize( "adjacency", [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 @@ -213,14 +180,15 @@ def test_from_obmol(adjacency): @pytest.mark.parametrize( "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 @@ -237,41 +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)] -) -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 0470c07..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)) @@ -55,6 +52,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 @@ -62,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 dc54c24..57e45c7 100644 --- a/tests/test_rmsd.py +++ b/tests/test_rmsd.py @@ -4,11 +4,12 @@ import numpy as np import pytest -from spyrmsd import molecule, qcp, rmsd -from tests import molecules +from spyrmsd import qcp, rmsd -@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,10 +38,12 @@ 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)]) -def test_rmsd_benzene(t: float, RMSD: float) -> None: - mol1 = copy.deepcopy(molecules.benzene) - mol2 = copy.deepcopy(molecules.benzene) +@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(benzene, t: float, RMSD: float) -> None: + mol1 = copy.deepcopy(benzene.mol) + mol2 = copy.deepcopy(benzene.mol) mol2.translate(np.array([0, 0, t])) @@ -52,11 +55,13 @@ 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]) - 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 @@ -66,11 +71,13 @@ 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]) - 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() @@ -80,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])) @@ -99,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 @@ -139,11 +145,13 @@ 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]) - 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, @@ -157,11 +165,13 @@ 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]) - 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() @@ -194,10 +204,11 @@ 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]) - 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 @@ -213,11 +224,13 @@ 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) - 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 @@ -239,15 +252,19 @@ 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 + 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]) @@ -270,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)) @@ -291,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)) @@ -322,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") @@ -356,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") @@ -392,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") @@ -443,10 +458,32 @@ 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]) - 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() @@ -508,10 +545,13 @@ 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() @@ -567,10 +607,13 @@ 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() @@ -709,9 +752,11 @@ 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): +def test_rmsd_atol(docking_2viz, i: int, j: int, result: float): """ Test usage of the :code:`atol` parameter for the QCP method. @@ -719,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) @@ -744,10 +789,12 @@ 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)]) -def test_symmrmsd_atol(i: bool, reference: float) -> None: - moli = copy.deepcopy(molecules.docking_1cbr[0]) - molj = copy.deepcopy(molecules.docking_1cbr[i]) +@pytest.mark.parametrize( + "i, reference", [(1, 0.476858), (2, 1.68089), (3, 1.50267)], ids=["1", "2", "3"] +) +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() @@ -777,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() @@ -852,10 +899,11 @@ 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) @@ -898,10 +946,13 @@ 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) @@ -913,10 +964,13 @@ 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]) - 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) 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)