diff --git a/package/CHANGELOG b/package/CHANGELOG index dede22e74a..fd0b869186 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -60,6 +60,8 @@ Fixes DSSP by porting upstream PyDSSP 0.9.1 fix (Issue #4913) Enhancements + * Enables parallelization for analysis.atomicdistances.AtomicDistances + (Issue #4662, PR #4822) * DSSP uses ``capped_distance`` and ``calc_bonds`` for faster distance calculations and supports ``backend`` selection (e.g. ``"distopia"``) (PR #5182) diff --git a/package/MDAnalysis/analysis/atomicdistances.py b/package/MDAnalysis/analysis/atomicdistances.py index e3709e25e1..ff1e39f526 100644 --- a/package/MDAnalysis/analysis/atomicdistances.py +++ b/package/MDAnalysis/analysis/atomicdistances.py @@ -114,7 +114,7 @@ from MDAnalysis.analysis.results import Results import logging -from .base import AnalysisBase +from .base import AnalysisBase, ResultsGroup logger = logging.getLogger("MDAnalysis.analysis.atomicdistances") @@ -147,15 +147,27 @@ class AtomicDistances(AnalysisBase): .. versionadded:: 2.5.0 .. versionchanged:: 2.11.0 - Distance data are now made available in :attr:`results.distances` instead - of :attr:`results` and :attr:`results` is now a - :class:`~MDAnalysis.analysis.results.Results` instance; this fixes an API issue - (see `Issue #4819`_) in a *backwards-incompatible* manner. + * Distance data are now made available in :attr:`results.distances` instead + of :attr:`results` and :attr:`results` is now a + :class:`~MDAnalysis.analysis.results.Results` instance; this fixes an API issue + (see `Issue #4819`_) in a *backwards-incompatible* manner. + * Enabled **parallel execution** with the ``multiprocessing`` and ``dask`` + backends; use the new method :meth:`get_supported_backends` to see all + supported backends. .. _`Issue #4819`: https://github.com/MDAnalysis/mdanalysis/issues/4819 - """ + _analysis_algorithm_is_parallelizable = True + + @classmethod + def get_supported_backends(cls): + return ( + "serial", + "multiprocessing", + "dask", + ) + def __init__(self, ag1, ag2, pbc=True, **kwargs): # check ag1 and ag2 have the same number of atoms if ag1.atoms.n_atoms != ag2.atoms.n_atoms: @@ -185,3 +197,10 @@ def _single_frame(self): self.results.distances[self._frame_index] = calc_bonds( self._ag1.positions, self._ag2.positions, box ) + + def _get_aggregator(self): + return ResultsGroup( + lookup={ + "distances": ResultsGroup.ndarray_vstack, # Get distances + } + ) diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index 7ffe99ef32..9ee3fec76c 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -21,6 +21,7 @@ from MDAnalysis.analysis.lineardensity import LinearDensity from MDAnalysis.analysis.polymer import PersistenceLength from MDAnalysis.analysis.rdf import InterRDF, InterRDF_s +from MDAnalysis.analysis.atomicdistances import AtomicDistances from MDAnalysis.lib.util import is_installed @@ -217,3 +218,11 @@ def client_InterRDF_s(request): @pytest.fixture(scope="module", params=params_for_cls(DistanceMatrix)) def client_DistanceMatrix(request): return request.param + + +# MDAnalysis.analysis.atomicdistances + + +@pytest.fixture(scope="module", params=params_for_cls(AtomicDistances)) +def client_AtomicDistances(request): + return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py b/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py index 5ed81303fe..90f4964f38 100644 --- a/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py +++ b/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py @@ -117,20 +117,28 @@ def test_ad_exceptions(self, ad_ag1, ad_ag3, ad_ag4): # only need to test that this class correctly applies distance calcs # calc_bonds() is tested elsewhere - def test_ad_pairwise_dist(self, ad_ag1, ad_ag2, expected_dist): + def test_ad_pairwise_dist( + self, ad_ag1, ad_ag2, expected_dist, client_AtomicDistances + ): """Ensure that pairwise distances between atoms are correctly calculated without PBCs.""" - pairwise_no_pbc = ad.AtomicDistances(ad_ag1, ad_ag2, pbc=False).run() + pairwise_no_pbc = ad.AtomicDistances(ad_ag1, ad_ag2, pbc=False).run( + **client_AtomicDistances + ) actual = pairwise_no_pbc.results assert isinstance(actual, Results) distances = actual.distances # compare with expected values from dist() assert_allclose(distances, expected_dist) - def test_ad_pairwise_dist_pbc(self, ad_ag1, ad_ag2, expected_pbc_dist): + def test_ad_pairwise_dist_pbc( + self, ad_ag1, ad_ag2, expected_pbc_dist, client_AtomicDistances + ): """Ensure that pairwise distances between atoms are correctly calculated with PBCs.""" - pairwise_pbc = ad.AtomicDistances(ad_ag1, ad_ag2).run() + pairwise_pbc = ad.AtomicDistances(ad_ag1, ad_ag2).run( + **client_AtomicDistances + ) actual = pairwise_pbc.results assert isinstance(actual, Results) distances = actual.distances