diff --git a/.github/actions/setup-deps/action.yaml b/.github/actions/setup-deps/action.yaml index e5e0b25744a..b7fc861ce07 100644 --- a/.github/actions/setup-deps/action.yaml +++ b/.github/actions/setup-deps/action.yaml @@ -53,6 +53,8 @@ inputs: default: 'chemfiles-python>=0.9' clustalw: default: 'clustalw=2.1' + distopia: + default: 'distopia>=0.2.0' h5py: default: 'h5py>=2.10' joblib: @@ -115,6 +117,7 @@ runs: CONDA_OPT_DEPS: | ${{ inputs.chemfiles-python }} ${{ inputs.clustalw }} + ${{ inputs.distopia }} ${{ inputs.h5py }} ${{ inputs.joblib }} ${{ inputs.netcdf4 }} diff --git a/package/CHANGELOG b/package/CHANGELOG index 46299a9ab19..0a34834cf4e 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -26,6 +26,8 @@ Fixes (Issue #3336) Enhancements + * Add distopia distance calculation library bindings as a selectable backend + for `calc_bonds` in `MDA.lib.distances`. (Issue #3783, PR #3914) * AuxReaders are now pickle-able and copy-able (Issue #1785, PR #3887) * Add pickling support for Atom, Residue, Segment, ResidueGroup and SegmentGroup. (PR #3953) diff --git a/package/MDAnalysis/lib/_distopia.py b/package/MDAnalysis/lib/_distopia.py new file mode 100644 index 00000000000..7170cf2a556 --- /dev/null +++ b/package/MDAnalysis/lib/_distopia.py @@ -0,0 +1,71 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + +"""Stub module for distopia --- :mod:`MDAnalysis.analysis.distopia` +=================================================================== + +This module is a stub to provide distopia distance functions to `distances.py` +as a selectable backend. +""" + +# check for distopia +try: + import distopia +except ImportError: + HAS_DISTOPIA = False +else: + HAS_DISTOPIA = True + +from .c_distances import ( + calc_bond_distance_triclinic as _calc_bond_distance_triclinic_serial, +) +import warnings +import numpy as np + + +def calc_bond_distance_ortho( + coords1, coords2: np.ndarray, box: np.ndarray, results: np.ndarray +) -> None: + distopia.calc_bonds_ortho_float( + coords1, coords2, box[:3], results=results + ) + # upcast is currently required, change for 3.0, see #3927 + + +def calc_bond_distance( + coords1: np.ndarray, coords2: np.ndarray, results: np.ndarray +) -> None: + distopia.calc_bonds_no_box_float( + coords1, coords2, results=results + ) + # upcast is currently required, change for 3.0, see #3927 + + +def calc_bond_distance_triclinic( + coords1: np.ndarray, coords2: np.ndarray, box: np.ndarray, results: np.ndarray +) -> None: + # redirect to serial backend + warnings.warn( + "distopia does not support triclinic boxes, using serial backend instead." + ) + _calc_bond_distance_triclinic_serial(coords1, coords2, box, results) diff --git a/package/MDAnalysis/lib/distances.py b/package/MDAnalysis/lib/distances.py index a36b1e81617..062b212ea30 100644 --- a/package/MDAnalysis/lib/distances.py +++ b/package/MDAnalysis/lib/distances.py @@ -50,10 +50,57 @@ with OpenMP ========== ========================= ====================================== +Use of the distopia library +--------------------------- + +MDAnalysis has developed a standalone library, `distopia`_ for accelerating +the distance functions in this module using explicit SIMD vectorisation. +This can provide many-fold speedups in calculating distances. Distopia is +under active development and as such only a selection of functions in this +module are covered. Consult the following table to see if the function +you wish to use is covered by distopia. For more information see the +`distopia documentation`_. + +.. Table:: Functions available using the `distopia`_ backend. + :align: center + + +-------------------------------------+-----------------------------------+ + | Functions | Notes | + +=====================================+===================================+ + | MDAnalysis.lib.distances.calc_bonds | Doesn't support triclinic boxes | + +-------------------------------------+-----------------------------------+ + +If `distopia`_ is installed, the functions in this table will accept the key +'distopia' for the `backend` keyword argument. If the distopia backend is +selected the `distopia` library will be used to calculate the distances. Note +that for functions listed in this table **distopia is not the default backend +if and must be selected.** + +.. Note:: + Distopia does not currently support triclinic simulation boxes. If you + specify `distopia` as the backend and your simulation box is triclinic, + the function will fall back to the default `serial` backend. + +.. Note:: + Due to the use of Instruction Set Architecture (`ISA`_) specific SIMD + intrinsics in distopia via `VCL2`_, the precision of your results may + depend on the ISA available on your machine. However, in all tested cases + distopia satisfied the accuracy thresholds used to the functions in this + module. Please document any issues you encounter with distopia's accuracy + in the `relevant distopia issue`_ on the MDAnalysis GitHub repository. + +.. _distopia: https://github.com/MDAnalysis/distopia +.. _distopia documentation: https://www.mdanalysis.org/distopia +.. _ISA: https://en.wikipedia.org/wiki/Instruction_set_architecture +.. _VCL2: https://github.com/vectorclass/version2 +.. _relevant distopia issue: https://github.com/MDAnalysis/mdanalysis/issues/3915 + .. versionadded:: 0.13.0 .. versionchanged:: 2.3.0 Distance functions can now accept an :class:`~MDAnalysis.core.groups.AtomGroup` or an :class:`np.ndarray` +.. versionchanged:: 2.5.0 + Interface to the `distopia`_ package added. Functions --------- @@ -83,6 +130,7 @@ from ._augment import augment_coordinates, undo_augment from .nsgrid import FastNS from .c_distances import _minimize_vectors_ortho, _minimize_vectors_triclinic +from ._distopia import HAS_DISTOPIA # hack to select backend with backend= kwarg. Note that @@ -97,8 +145,11 @@ package="MDAnalysis.lib") except ImportError: pass -del importlib +if HAS_DISTOPIA: + _distances["distopia"] = importlib.import_module("._distopia", + package="MDAnalysis.lib") +del importlib def _run(funcname: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None, backend: str = "serial") -> Callable: @@ -1408,15 +1459,15 @@ def calc_bonds(coords1: Union[npt.NDArray, 'AtomGroup'], Preallocated result array of dtype ``numpy.float64`` and shape ``(n,)`` (for ``n`` coordinate pairs). Avoids recreating the array in repeated function calls. - backend : {'serial', 'OpenMP'}, optional - Keyword selecting the type of acceleration. + backend : {'serial', 'OpenMP', 'distopia'}, optional + Keyword selecting the type of acceleration. Defaults to 'serial'. Returns ------- - bondlengths : numpy.ndarray (``dtype=numpy.float64``, ``shape=(n,)``) or numpy.float64 - Array containing the bond lengths between each pair of coordinates. If - two single coordinates were supplied, their distance is returned as a - single number instead of an array. + bondlengths : numpy.ndarray (``dtype=numpy.float64``, ``shape=(n,)``) or + numpy.float64 Array containing the bond lengths between each pair of + coordinates. If two single coordinates were supplied, their distance is + returned as a single number instead of an array. .. versionadded:: 0.8 @@ -1428,6 +1479,8 @@ def calc_bonds(coords1: Union[npt.NDArray, 'AtomGroup'], .. versionchanged:: 2.3.0 Can now accept an :class:`~MDAnalysis.core.groups.AtomGroup` as an argument in any position and checks inputs using type hinting. + .. versionchanged:: 2.5.0 + Can now optionally use the fast distance functions from distopia """ numatom = coords1.shape[0] bondlengths = _check_result_array(result, (numatom,)) @@ -1435,19 +1488,30 @@ def calc_bonds(coords1: Union[npt.NDArray, 'AtomGroup'], if numatom > 0: if box is not None: boxtype, box = check_box(box) - if boxtype == 'ortho': - _run("calc_bond_distance_ortho", - args=(coords1, coords2, box, bondlengths), - backend=backend) + if boxtype == "ortho": + if backend == 'distopia': + bondlengths = bondlengths.astype(np.float32) + _run( + "calc_bond_distance_ortho", + args=(coords1, coords2, box, bondlengths), + backend=backend, + ) else: - _run("calc_bond_distance_triclinic", - args=(coords1, coords2, box, bondlengths), - backend=backend) + _run( + "calc_bond_distance_triclinic", + args=(coords1, coords2, box, bondlengths), + backend=backend, + ) else: - _run("calc_bond_distance", - args=(coords1, coords2, bondlengths), - backend=backend) - + if backend == 'distopia': + bondlengths = bondlengths.astype(np.float32) + _run( + "calc_bond_distance", + args=(coords1, coords2, bondlengths), + backend=backend, + ) + if backend == 'distopia': + bondlengths = bondlengths.astype(np.float64) return bondlengths diff --git a/testsuite/MDAnalysisTests/analysis/test_distances.py b/testsuite/MDAnalysisTests/analysis/test_distances.py index ae0d0471222..2a71ac31654 100644 --- a/testsuite/MDAnalysisTests/analysis/test_distances.py +++ b/testsuite/MDAnalysisTests/analysis/test_distances.py @@ -148,7 +148,7 @@ def test_pairwise_dist(self, ag, ag2, expected): '''Ensure that pairwise distances between atoms are correctly calculated.''' actual = MDAnalysis.analysis.distances.dist(ag, ag2)[2] - assert_equal(actual, expected) + assert_allclose(actual, expected) def test_pairwise_dist_box(self, ag, ag2, expected_box, box): '''Ensure that pairwise distances between atoms are @@ -161,7 +161,7 @@ def test_pairwise_dist_offset_effect(self, ag, ag2, expected): pairwise distance matrix.''' actual = MDAnalysis.analysis.distances.dist( ag, ag2, offset=229)[2] - assert_equal(actual, expected) + assert_allclose(actual, expected) def test_offset_calculation(self, ag, ag2): '''Test that offsets fed to dist() are correctly calculated.''' diff --git a/testsuite/MDAnalysisTests/lib/test_distances.py b/testsuite/MDAnalysisTests/lib/test_distances.py index e73ffc4c7ab..3fe4b2852b8 100644 --- a/testsuite/MDAnalysisTests/lib/test_distances.py +++ b/testsuite/MDAnalysisTests/lib/test_distances.py @@ -28,6 +28,7 @@ import MDAnalysis from MDAnalysis.lib import distances +from MDAnalysis.lib.distances import HAS_DISTOPIA from MDAnalysis.lib import mdamath from MDAnalysis.tests.datafiles import PSF, DCD, TRIC @@ -825,7 +826,15 @@ def convert_position_dtype_if_ndarray(a, b, c, d, dtype): conv_dtype_if_ndarr(d, dtype)) -@pytest.mark.parametrize('backend', ['serial', 'openmp']) +def distopia_conditional_backend(): + # functions that allow distopia acceleration need to be tested with + # distopia backend argument but distopia is an optional dep. + if HAS_DISTOPIA: + return ["serial", "openmp", "distopia"] + else: + return ["serial", "openmp"] + + class TestCythonFunctions(object): # Unit tests for calc_bonds calc_angles and calc_dihedrals in lib.distances # Tests both numerical results as well as input types as Cython will silently @@ -878,8 +887,9 @@ def wronglength(): ((-1, 0, 1), (0, -1, 0), (1, 0, 1), (-1, -1, -1)), # negative single ((4, 3, -2), (-2, 2, 2), (-5, 2, 2), (0, 2, 2))] # multiple boxlengths - @pytest.mark.parametrize('dtype', (np.float32, np.float64)) - @pytest.mark.parametrize('pos', ['positions', 'positions_atomgroups']) + @pytest.mark.parametrize("dtype", (np.float32, np.float64)) + @pytest.mark.parametrize("pos", ["positions", "positions_atomgroups"]) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) def test_bonds(self, box, backend, dtype, pos, request): a, b, c, d = request.getfixturevalue(pos) a, b, c, d = convert_position_dtype_if_ndarray(a, b, c, d, dtype) @@ -900,6 +910,7 @@ def test_bonds(self, box, backend, dtype, pos, request): assert_almost_equal(dists_pbc[3], 3.46410072, self.prec, err_msg="PBC check #w with box") + @pytest.mark.parametrize("backend", distopia_conditional_backend()) def test_bonds_badbox(self, positions, backend): a, b, c, d = positions badbox1 = np.array([10., 10., 10.], dtype=np.float64) @@ -911,24 +922,26 @@ def test_bonds_badbox(self, positions, backend): with pytest.raises(ValueError): distances.calc_bonds(a, b, box=badbox2, backend=backend) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) def test_bonds_badresult(self, positions, backend): a, b, c, d = positions badresult = np.zeros(len(a) - 1) # Bad result array with pytest.raises(ValueError): distances.calc_bonds(a, b, result=badresult, backend=backend) - @pytest.mark.parametrize('dtype', (np.float32, np.float64)) - @pytest.mark.parametrize('pos', ['positions', 'positions_atomgroups']) - def test_bonds_triclinic(self, triclinic_box, backend, dtype, pos, - request): + @pytest.mark.parametrize("dtype", (np.float32, np.float64)) + @pytest.mark.parametrize("pos", ["positions", "positions_atomgroups"]) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) + def test_bonds_triclinic(self, triclinic_box, backend, dtype, pos, request): a, b, c, d = request.getfixturevalue(pos) a, b, c, d = convert_position_dtype_if_ndarray(a, b, c, d, dtype) dists = distances.calc_bonds(a, b, box=triclinic_box, backend=backend) reference = np.array([0.0, 1.7320508, 1.4142136, 2.82842712]) assert_almost_equal(dists, reference, self.prec, err_msg="calc_bonds with triclinic box failed") - @pytest.mark.parametrize('shift', shifts) - @pytest.mark.parametrize('periodic', [True, False]) + @pytest.mark.parametrize("shift", shifts) + @pytest.mark.parametrize("periodic", [True, False]) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) def test_bonds_single_coords(self, shift, periodic, backend): box = np.array([10, 20, 30, 90., 90., 90.], dtype=np.float32) @@ -946,8 +959,9 @@ def test_bonds_single_coords(self, shift, periodic, backend): assert_almost_equal(result, reference, decimal=self.prec) - @pytest.mark.parametrize('dtype', (np.float32, np.float64)) - @pytest.mark.parametrize('pos', ['positions', 'positions_atomgroups']) + @pytest.mark.parametrize("dtype", (np.float32, np.float64)) + @pytest.mark.parametrize("pos", ["positions", "positions_atomgroups"]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_angles(self, backend, dtype, pos, request): a, b, c, d = request.getfixturevalue(pos) a, b, c, d = convert_position_dtype_if_ndarray(a, b, c, d, dtype) @@ -963,19 +977,33 @@ def test_angles(self, backend, dtype, pos, request): assert_almost_equal(angles[3], 0.098174833, self.prec, err_msg="Small angle failed in calc_angles") + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_angles_bad_result(self, positions, backend): a, b, c, d = positions badresult = np.zeros(len(a) - 1) # Bad result array with pytest.raises(ValueError): distances.calc_angles(a, b, c, result=badresult, backend=backend) - @pytest.mark.parametrize('case', [ - (np.array([[1, 1, 1], [1, 2, 1], [2, 2, 1]], dtype=np.float32), 0.5 * np.pi), # 90 degree angle - (np.array([[1, 1, 1], [1, 2, 1], [1, 3, 1]], dtype=np.float32), np.pi), # straight line / 180. - (np.array([[1, 1, 1], [1, 2, 1], [2, 1, 1]], dtype=np.float32), 0.25 * np.pi), # 45 - ]) - @pytest.mark.parametrize('shift', shifts) - @pytest.mark.parametrize('periodic', [True, False]) + @pytest.mark.parametrize( + "case", + [ + ( + np.array([[1, 1, 1], [1, 2, 1], [2, 2, 1]], dtype=np.float32), + 0.5 * np.pi, + ), # 90 degree angle + ( + np.array([[1, 1, 1], [1, 2, 1], [1, 3, 1]], dtype=np.float32), + np.pi, + ), # straight line / 180. + ( + np.array([[1, 1, 1], [1, 2, 1], [2, 1, 1]], dtype=np.float32), + 0.25 * np.pi, + ), # 45 + ], + ) + @pytest.mark.parametrize("shift", shifts) + @pytest.mark.parametrize("periodic", [True, False]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_angles_single_coords(self, case, shift, periodic, backend): def manual_angle(x, y, z): return mdamath.angle(y - x, y - z) @@ -994,8 +1022,9 @@ def manual_angle(x, y, z): reference = ref if periodic else manual_angle(a, b, c) assert_almost_equal(result, reference, decimal=4) - @pytest.mark.parametrize('dtype', (np.float32, np.float64)) - @pytest.mark.parametrize('pos', ['positions', 'positions_atomgroups']) + @pytest.mark.parametrize("dtype", (np.float32, np.float64)) + @pytest.mark.parametrize("pos", ["positions", "positions_atomgroups"]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_dihedrals(self, backend, dtype, pos, request): a, b, c, d = request.getfixturevalue(pos) a, b, c, d = convert_position_dtype_if_ndarray(a, b, c, d, dtype) @@ -1008,6 +1037,7 @@ def test_dihedrals(self, backend, dtype, pos, request): assert_almost_equal(dihedrals[3], -0.50714064, self.prec, err_msg="arbitrary dihedral angle failed") + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_dihedrals_wronglength(self, positions, wronglength, backend): a, b, c, d = positions with pytest.raises(ValueError): @@ -1022,6 +1052,7 @@ def test_dihedrals_wronglength(self, positions, wronglength, backend): with pytest.raises(ValueError): distances.calc_dihedrals(a, b, c, wronglength, backend=backend) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_dihedrals_bad_result(self, positions, backend): a, b, c, d = positions badresult = np.zeros(len(a) - 1) # Bad result array @@ -1029,16 +1060,50 @@ def test_dihedrals_bad_result(self, positions, backend): with pytest.raises(ValueError): distances.calc_dihedrals(a, b, c, d, result=badresult, backend=backend) - @pytest.mark.parametrize('case', [ - (np.array([[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 2, 1]], dtype=np.float32), 0.), # 0 degree angle (cis) - (np.array([[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 0, 1]], dtype=np.float32), np.pi), # 180 degree (trans) - (np.array([[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 1, 2]], dtype=np.float32), 0.5 * np.pi), # 90 degree - (np.array([[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 1, 0]], dtype=np.float32), 0.5 * np.pi), # other 90 degree - (np.array([[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 2, 2]], dtype=np.float32), 0.25 * np.pi), # 45 degree - (np.array([[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 0, 2]], dtype=np.float32), 0.75 * np.pi), # 135 - ]) - @pytest.mark.parametrize('shift', shifts) - @pytest.mark.parametrize('periodic', [True, False]) + @pytest.mark.parametrize( + "case", + [ + ( + np.array( + [[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 2, 1]], dtype=np.float32 + ), + 0.0, + ), # 0 degree angle (cis) + ( + np.array( + [[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 0, 1]], dtype=np.float32 + ), + np.pi, + ), # 180 degree (trans) + ( + np.array( + [[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 1, 2]], dtype=np.float32 + ), + 0.5 * np.pi, + ), # 90 degree + ( + np.array( + [[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 1, 0]], dtype=np.float32 + ), + 0.5 * np.pi, + ), # other 90 degree + ( + np.array( + [[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 2, 2]], dtype=np.float32 + ), + 0.25 * np.pi, + ), # 45 degree + ( + np.array( + [[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 0, 2]], dtype=np.float32 + ), + 0.75 * np.pi, + ), # 135 + ], + ) + @pytest.mark.parametrize("shift", shifts) + @pytest.mark.parametrize("periodic", [True, False]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_dihedrals_single_coords(self, case, shift, periodic, backend): def manual_dihedral(a, b, c, d): return mdamath.dihedral(b - a, c - b, d - c) @@ -1059,27 +1124,45 @@ def manual_dihedral(a, b, c, d): reference = ref if periodic else manual_dihedral(a, b, c, d) assert_almost_equal(abs(result), abs(reference), decimal=4) - def test_numpy_compliance(self, positions, backend): + @pytest.mark.parametrize("backend", distopia_conditional_backend()) + def test_numpy_compliance_bonds(self, positions, backend): a, b, c, d = positions # Checks that the cython functions give identical results to the numpy versions bonds = distances.calc_bonds(a, b, backend=backend) - angles = distances.calc_angles(a, b, c, backend=backend) - dihedrals = distances.calc_dihedrals(a, b, c, d, backend=backend) - bonds_numpy = np.array([mdamath.norm(y - x) for x, y in zip(a, b)]) + + assert_almost_equal( + bonds, + bonds_numpy, + self.prec, + err_msg="Cython bonds didn't match numpy calculations", + ) + + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_numpy_compliance_angles(self, positions, backend): + a, b, c, d = positions + # Checks that the cython functions give identical results to the numpy versions + angles = distances.calc_angles(a, b, c, backend=backend) vec1 = a - b vec2 = c - b angles_numpy = np.array([mdamath.angle(x, y) for x, y in zip(vec1, vec2)]) + # numpy 0 angle returns NaN rather than 0 + assert_almost_equal( + angles[1:], + angles_numpy[1:], + self.prec, + err_msg="Cython angles didn't match numpy calcuations", + ) + + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_numpy_compliance_dihedrals(self, positions, backend): + a, b, c, d = positions + # Checks that the cython functions give identical results to the numpy versions + dihedrals = distances.calc_dihedrals(a, b, c, d, backend=backend) ab = a - b bc = b - c cd = c - d dihedrals_numpy = np.array([mdamath.dihedral(x, y, z) for x, y, z in zip(ab, bc, cd)]) - - assert_almost_equal(bonds, bonds_numpy, self.prec, - err_msg="Cython bonds didn't match numpy calculations") - # numpy 0 angle returns NaN rather than 0 - assert_almost_equal(angles[1:], angles_numpy[1:], self.prec, - err_msg="Cython angles didn't match numpy calcuations") assert_almost_equal(dihedrals, dihedrals_numpy, self.prec, err_msg="Cython dihedrals didn't match numpy calculations") @@ -1368,18 +1451,19 @@ def test_input_unchanged_transform_RtoS_and_StoR(self, coords, box, backend): res = distances.transform_StoR(crd, box, backend=backend) assert_equal(crd, ref) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) def test_input_unchanged_calc_bonds(self, coords, box, backend): crds = coords[:2] refs = [crd.copy() for crd in crds] res = distances.calc_bonds(crds[0], crds[1], box=box, backend=backend) assert_equal(crds, refs) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) - def test_input_unchanged_calc_bonds_atomgroup(self, coords_atomgroups, - box, backend): + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) + def test_input_unchanged_calc_bonds_atomgroup( + self, coords_atomgroups, box, backend + ): crds = coords_atomgroups[:2] refs = [crd.positions.copy() for crd in crds] res = distances.calc_bonds(crds[0], crds[1], box=box, backend=backend) @@ -1526,8 +1610,8 @@ def test_empty_input_transform_StoR(self, empty_coord, box, backend): res = distances.transform_StoR(empty_coord, box, backend=backend) assert_equal(res, empty_coord) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) def test_empty_input_calc_bonds(self, empty_coord, box, backend): res = distances.calc_bonds(empty_coord, empty_coord, box=box, backend=backend) @@ -1680,10 +1764,9 @@ def test_output_dtype_transform_RtoS(self, incoords, box, backend): assert res.dtype.type == np.float32 assert res.shape == incoords.shape - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('incoords', - [2 * [coords[0]]] + list(comb(coords[1:], 2))) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("incoords", [2 * [coords[0]]] + list(comb(coords[1:], 2))) + @pytest.mark.parametrize("backend", distopia_conditional_backend()) def test_output_type_calc_bonds(self, incoords, box, backend): res = distances.calc_bonds(*incoords, box=box, backend=backend) maxdim = max([crd.ndim for crd in incoords])