diff --git a/.github/actions/build-src/action.yaml b/.github/actions/build-src/action.yaml index 8abab0f13e9..95f4eb95401 100644 --- a/.github/actions/build-src/action.yaml +++ b/.github/actions/build-src/action.yaml @@ -66,6 +66,15 @@ runs: micromamba info micromamba list + - name: mda_deps + shell: bash -l {0} + run: | + # Install mdakit deps that depend on MDA + python -m pip install --no-deps \ + waterdynamics \ + pathsimanalysis \ + mdahole2 + - name: build_mda_main shell: bash -l {0} run: | @@ -84,6 +93,12 @@ runs: fi python -m pip install ${BUILD_FLAGS} -v -e ./testsuite + - name: post_build_env_check + shell: bash -l {0} + run: | + pip list + micromamba list + - name: build_docs if: ${{ inputs.build-docs == 'true' }} shell: bash -l {0} diff --git a/.github/actions/setup-deps/action.yaml b/.github/actions/setup-deps/action.yaml index cceae40f99c..97112b09159 100644 --- a/.github/actions/setup-deps/action.yaml +++ b/.github/actions/setup-deps/action.yaml @@ -31,8 +31,6 @@ inputs: default: 'hypothesis' matplotlib: default: 'matplotlib-base' - mdahole2: - default: 'mdahole2-base' mda_xdrlib: default: 'mda-xdrlib' mmtf-python: @@ -41,8 +39,6 @@ inputs: default: 'numpy' packaging: default: 'packaging' - pathsimanalysis: - default: 'pathsimanalysis' pip: default: 'pip' pytest: @@ -53,8 +49,6 @@ inputs: default: 'threadpoolctl' tqdm: default: 'tqdm>=4.43.0' - waterdynamics: - default: 'waterdynamics' # conda-installed optional dependencies biopython: default: 'biopython>=1.80' @@ -120,18 +114,15 @@ runs: ${{ inputs.griddataformats }} ${{ inputs.hypothesis }} ${{ inputs.matplotlib }} - ${{ inputs.mdahole2 }} ${{ inputs.mda_xdrlib }} ${{ inputs.mmtf-python }} ${{ inputs.numpy }} ${{ inputs.packaging }} - ${{ inputs.pathsimanalysis }} ${{ inputs.pip }} ${{ inputs.pytest }} ${{ inputs.scipy }} ${{ inputs.threadpoolctl }} ${{ inputs.tqdm }} - ${{ inputs.waterdynamics }} CONDA_OPT_DEPS: | ${{ inputs.biopython }} ${{ inputs.chemfiles-python }} diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9ce7d4af839..377575ef8c1 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -38,10 +38,10 @@ jobs: matrix: buildplat: - [ubuntu-22.04, manylinux_x86_64, x86_64] - - [macos-11, macosx_*, x86_64] + - [macos-12, macosx_*, x86_64] - [windows-2019, win_amd64, AMD64] - [macos-14, macosx_*, arm64] - python: ["cp39", "cp310", "cp311", "cp312"] + python: ["cp310", "cp311", "cp312"] defaults: run: working-directory: ./package @@ -51,7 +51,7 @@ jobs: fetch-depth: 0 - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.20.0 with: package-dir: package env: @@ -142,7 +142,7 @@ jobs: mv dist/MDAnalysisTests-* testsuite/dist - name: upload_source_and_wheels - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 with: skip_existing: true repository_url: https://test.pypi.org/legacy/ @@ -171,7 +171,7 @@ jobs: mv dist/MDAnalysisTests-* testsuite/dist - name: upload_tests - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 with: packages_dir: testsuite/dist skip_existing: true @@ -201,7 +201,7 @@ jobs: mv dist/MDAnalysisTests-* testsuite/dist - name: upload_source_and_wheels - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.0 upload_pypi_mdanalysistests: if: | @@ -227,7 +227,7 @@ jobs: mv dist/MDAnalysisTests-* testsuite/dist - name: upload_tests - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.0 with: packages_dir: testsuite/dist diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 72c7e365385..4585fc4de71 100644 --- a/.github/workflows/gh-ci-cron.yaml +++ b/.github/workflows/gh-ci-cron.yaml @@ -4,6 +4,11 @@ on: # 3 am Tuesdays and Fridays - cron: "0 3 * * 2,5" workflow_dispatch: + # Uncomment when you need to test on a PR + # pull_request: + # branches: + # - develop + concurrency: # Probably overly cautious group naming. @@ -21,6 +26,7 @@ env: MPLBACKEND: agg jobs: + # a pip only, minimal deps install w/ scipy & numpy nightly upstream wheels numpy_and_scipy_dev: if: "github.repository == 'MDAnalysis/mdanalysis'" runs-on: ubuntu-latest @@ -34,45 +40,51 @@ jobs: with: os-type: "ubuntu" - - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 - with: - environment-name: mda - create-args: >- - python=3.11 - pip - # using jaime's shim to avoid pulling down the cudatoolkit - condarc: | - channels: - - jaimergp/label/unsupported-cudatoolkit-shim - - conda-forge - - bioconda - - - name: install_deps - uses: ./.github/actions/setup-deps + - uses: actions/setup-python@v4 with: - micromamba: true - full-deps: true + python-version: ${{ matrix.python-version }} - # overwrite installs by picking up nightly wheels + # minimally install nightly wheels & core deps - name: nightly_wheels run: | - pip install --pre -U -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple scipy numpy networkx matplotlib pandas + # Nightlies: add in networkx and matplotlib because we can + python -m pip install --pre -U --extra-index https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ + scipy \ + numpy \ + networkx \ + matplotlib \ + # Base deps + python -m pip install \ + "cython>=0.28" \ + packaging \ + "setuptools>69.4" \ + wheel \ + "griddataformats>=0.4.0" \ + "mmtf-python>=1.0" \ + "joblib>=0.12" \ + "tqdm>=4.43.0" \ + threadpoolctl \ + fasteners \ + mda-xdrlib \ + pytest \ + pytest-xdist \ + pytest-timeout + # deps that depend on MDA + python -m pip install --no-deps \ + waterdynamics \ + pathsimanalysis \ + mdahole2 + + - name: pre_install_list_deps + run: python -m pip list - - name: list_deps + - name: build_srcs run: | - micromamba list - pip list + python -m pip install --no-build-isolation -v -e ./package + python -m pip install --no-build-isolation -v -e ./testsuite - # Intentionally going with setup.py builds so we can build with latest - - name: build_srcs - uses: ./.github/actions/build-src - with: - build-tests: true - build-docs: false - # We don't use build isolation because we want to ensure that we - # test building with brand new versions of NumPy here. - isolation: false + - name: post_install_list_deps + run: python -m pip list - name: run_tests run: | @@ -136,7 +148,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, macos-11] + os: [ubuntu-20.04, macos-12] steps: - uses: actions/checkout@v4 @@ -151,7 +163,7 @@ jobs: with: environment-name: mda create-args: >- - python=3.9 + python=3.10 pip condarc: | channels: @@ -210,6 +222,9 @@ jobs: run: | pip install pytest-xdist pytest-timeout + - name: check env + run: pip list + - name: run_tests run: | pytest --timeout=200 -n auto testsuite/MDAnalysisTests --disable-pytest-warnings --durations=50 @@ -218,12 +233,14 @@ jobs: conda-latest-release: # A set of runner to check that the latest conda release works as expected if: "github.repository == 'MDAnalysis/mdanalysis'" - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os }} timeout-minutes: 60 strategy: fail-fast: false matrix: - os: [ubuntu, macos] + # Stick to macos-13 because some of our + # optional depss don't support arm64 (i.e. macos-14) + os: [ubuntu-latest, macos-13] python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 @@ -247,16 +264,16 @@ jobs: - conda-forge - bioconda + - name: install_mdanalysis + run: | + micromamba install mdanalysis mdanalysistests + - name: install_deps uses: ./.github/actions/setup-deps with: micromamba: true full-deps: true - - name: install_mdanalysis - run: | - micromamba install mdanalysis mdanalysistests - - name: run_tests run: | pytest --timeout=200 -n auto --pyargs MDAnalysisTests diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index 072c4197991..9b9cb8b7579 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -283,6 +283,9 @@ jobs: python -m pip install mdanalysis-*.tar.gz python -m pip install mdanalysistests-*.tar.gz + - name: check install + run: pip list + - name: run tests working-directory: ./dist run: python -m pytest --timeout=200 -n auto --pyargs MDAnalysisTests diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 16d303bb196..ca3e744d45b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -101,7 +101,7 @@ jobs: displayName: 'pin to older NumPy (wheel test)' condition: and(succeeded(), ne(variables['NUMPY_MIN'], '')) - script: >- - python -m pip install + python -m pip install -vvv biopython "chemfiles>=0.10,<0.10.4" duecredit @@ -112,8 +112,8 @@ jobs: networkx parmed pytng>=0.2.3 - tidynamics>=1.0.0 rdkit>=2020.03.1 + tidynamics>=1.0.0 displayName: 'Install additional dependencies for 64-bit tests' condition: and(succeeded(), eq(variables['PYTHON_ARCH'], 'x64')) - script: >- diff --git a/package/AUTHORS b/package/AUTHORS index 52e17f7c20d..9728e7ac531 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -243,7 +243,7 @@ Chronological list of authors - Kurt McKee - Fabian Zills - Laksh Krishna Sharma - + - Matthew Davies External code diff --git a/package/CHANGELOG b/package/CHANGELOG index 056282c6214..aecde6e6468 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -17,11 +17,13 @@ The rules for this file: ??/??/?? IAlibay, HeetVekariya, marinegor, lilyminium, RMeli, ljwoods2, aditya292002, pstaerk, PicoCentauri, BFedder, tyler.je.reddy, SampurnaM, leonwehrhan, kainszs, orionarcher, - yuxuanzhuang, PythonFZ, laksh-krishna-sharma + yuxuanzhuang, PythonFZ, laksh-krishna-sharma, orbeckst, MattTDavies, + talagayev * 2.8.0 Fixes + * Catch higher dimensional indexing in GroupBase & ComponentBase (Issue #4647) * Do not raise an Error reading H5MD files with datasets like `observables//` (part of Issue #4598, PR #4615) * Fix failure in double-serialization of TextIOPicklable file reader. @@ -54,6 +56,8 @@ Fixes Enhancements * Introduce parallelization API to `AnalysisBase` and to `analysis.rms.RMSD` class (Issue #4158, PR #4304) + * Enables parallelization for analysis.gnm.GNMAnalysis (Issue #4672) + * explicitly mark `analysis.pca.PCA` as not parallelizable (Issue #4680) * Improve error message for `AtomGroup.unwrap()` when bonds are not present.(Issue #4436, PR #4642) * Add `analysis.DSSP` module for protein secondary structure assignment, based on [pydssp](https://github.com/ShintaroMinami/PyDSSP) * Added a tqdm progress bar for `MDAnalysis.analysis.pca.PCA.transform()` diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index 86a62fa7b9f..510fb887d01 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -92,7 +92,7 @@ import numpy as np -from .base import AnalysisBase +from .base import AnalysisBase, ResultsGroup from MDAnalysis.analysis.base import Results @@ -245,8 +245,19 @@ class GNMAnalysis(AnalysisBase): Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and store results as attributes ``times``, ``eigenvalues`` and ``eigenvectors`` of the ``results`` attribute. + + .. versionchanged:: 2.8.0 + Enabled **parallel execution** with the ``multiprocessing`` and ``dask`` + backends; use the new method :meth:`get_supported_backends` to see all + supported backends. """ + _analysis_algorithm_is_parallelizable = True + + @classmethod + def get_supported_backends(cls): + return ("serial", "multiprocessing", "dask") + def __init__(self, universe, select='protein and name CA', @@ -348,6 +359,15 @@ def _conclude(self): self.results.eigenvalues = np.asarray(self.results.eigenvalues) self.results.eigenvectors = np.asarray(self.results.eigenvectors) + def _get_aggregator(self): + return ResultsGroup( + lookup={ + "eigenvectors": ResultsGroup.ndarray_hstack, + "eigenvalues": ResultsGroup.ndarray_hstack, + "times": ResultsGroup.ndarray_hstack, + } + ) + class closeContactGNMAnalysis(GNMAnalysis): r"""GNMAnalysis only using close contacts. diff --git a/package/MDAnalysis/analysis/pca.py b/package/MDAnalysis/analysis/pca.py index e4818aabf21..d9b88cc8e5d 100644 --- a/package/MDAnalysis/analysis/pca.py +++ b/package/MDAnalysis/analysis/pca.py @@ -143,7 +143,7 @@ class PCA(AnalysisBase): generates the principal components of the backbone of the atomgroup and then transforms those atomgroup coordinates by the direction of those variances. Please refer to the :ref:`PCA-tutorial` for more detailed - instructions. When using mean selections, the first frame of the selected + instructions. When using mean selections, the first frame of the selected trajectory slice is used as a reference. Parameters @@ -239,6 +239,7 @@ class PCA(AnalysisBase): incorrectly handle cases where the ``frame`` argument was passed. """ + _analysis_algorithm_is_parallelizable = False def __init__(self, universe, select='all', align=False, mean=None, n_components=None, **kwargs): diff --git a/package/MDAnalysis/converters/RDKit.py b/package/MDAnalysis/converters/RDKit.py index da52e23b915..139528440ab 100644 --- a/package/MDAnalysis/converters/RDKit.py +++ b/package/MDAnalysis/converters/RDKit.py @@ -87,7 +87,6 @@ from io import StringIO import numpy as np -from numpy.lib import NumpyVersion from . import base from ..coordinates import memory @@ -96,13 +95,8 @@ from ..exceptions import NoDataError try: - # TODO: remove this guard when RDKit has a release - # that supports NumPy 2 - if NumpyVersion(np.__version__) < "2.0.0": - from rdkit import Chem - from rdkit.Chem import AllChem - else: - raise ImportError + from rdkit import Chem + from rdkit.Chem import AllChem except ImportError: pass else: diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index c272f96f1e4..91b2c779304 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -570,7 +570,10 @@ def __init__(self, *args): raise TypeError(errmsg) from None # indices for the objects I hold - self._ix = np.asarray(ix, dtype=np.intp) + ix = np.asarray(ix, dtype=np.intp) + if ix.ndim > 1: + raise IndexError('Group index must be 1d') + self._ix = ix self._u = u self._cache = dict() @@ -597,6 +600,7 @@ def __getitem__(self, item): # hack to make lists into numpy arrays # important for boolean slicing item = np.array(item) + # We specify _derived_class instead of self.__class__ to allow # subclasses, such as UpdatingAtomGroup, to control the class # resulting from slicing. @@ -4246,6 +4250,9 @@ class ComponentBase(_MutableBase): def __init__(self, ix, u): # index of component + if not isinstance(ix, numbers.Integral): + raise IndexError('Component can only be indexed by a single integer') + self._ix = ix self._u = u diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index 55bae7e6bd8..75d62284b7b 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -8,6 +8,7 @@ ) from MDAnalysis.analysis.rms import RMSD, RMSF from MDAnalysis.lib.util import is_installed +from MDAnalysis.analysis.gnm import GNMAnalysis def params_for_cls(cls, exclude: list[str] = None): @@ -87,3 +88,8 @@ def client_RMSD(request): @pytest.fixture(scope='module', params=params_for_cls(RMSF)) def client_RMSF(request): return request.param + + +@pytest.fixture(scope='module', params=params_for_cls(GNMAnalysis)) +def client_GNMAnalysis(request): + return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_gnm.py b/testsuite/MDAnalysisTests/analysis/test_gnm.py index 6521c08eb86..d8a547a5428 100644 --- a/testsuite/MDAnalysisTests/analysis/test_gnm.py +++ b/testsuite/MDAnalysisTests/analysis/test_gnm.py @@ -38,10 +38,10 @@ def universe(): return mda.Universe(GRO, XTC) -def test_gnm(universe, tmpdir): +def test_gnm(universe, tmpdir, client_GNMAnalysis): output = os.path.join(str(tmpdir), 'output.txt') gnm = mda.analysis.gnm.GNMAnalysis(universe, ReportVector=output) - gnm.run() + gnm.run(**client_GNMAnalysis) result = gnm.results assert len(result.times) == 10 assert_almost_equal(gnm.results.times, np.arange(0, 1000, 100), decimal=4) @@ -51,9 +51,9 @@ def test_gnm(universe, tmpdir): 4.2058769e-15, 3.9839431e-15]) -def test_gnm_run_step(universe): +def test_gnm_run_step(universe, client_GNMAnalysis): gnm = mda.analysis.gnm.GNMAnalysis(universe) - gnm.run(step=3) + gnm.run(step=3, **client_GNMAnalysis) result = gnm.results assert len(result.times) == 4 assert_almost_equal(gnm.results.times, np.arange(0, 1200, 300), decimal=4) @@ -88,9 +88,9 @@ def test_gnm_SVD_fail(universe): mda.analysis.gnm.GNMAnalysis(universe).run(stop=1) -def test_closeContactGNMAnalysis(universe): +def test_closeContactGNMAnalysis(universe, client_GNMAnalysis): gnm = mda.analysis.gnm.closeContactGNMAnalysis(universe, weights="size") - gnm.run(stop=2) + gnm.run(stop=2, **client_GNMAnalysis) result = gnm.results assert len(result.times) == 2 assert_almost_equal(gnm.results.times, (0, 100), decimal=4) @@ -114,9 +114,9 @@ def test_closeContactGNMAnalysis(universe): 0.0, 0.0, -2.263157894736841, -0.24333213169614382]) -def test_closeContactGNMAnalysis_weights_None(universe): +def test_closeContactGNMAnalysis_weights_None(universe, client_GNMAnalysis): gnm = mda.analysis.gnm.closeContactGNMAnalysis(universe, weights=None) - gnm.run(stop=2) + gnm.run(stop=2, **client_GNMAnalysis) result = gnm.results assert len(result.times) == 2 assert_almost_equal(gnm.results.times, (0, 100), decimal=4) diff --git a/testsuite/MDAnalysisTests/analysis/test_pca.py b/testsuite/MDAnalysisTests/analysis/test_pca.py index ec874b900fe..b0358ba4243 100644 --- a/testsuite/MDAnalysisTests/analysis/test_pca.py +++ b/testsuite/MDAnalysisTests/analysis/test_pca.py @@ -23,6 +23,7 @@ import numpy as np import MDAnalysis as mda from MDAnalysis.analysis import align +import MDAnalysis.analysis.pca from MDAnalysis.analysis.pca import (PCA, cosine_content, rmsip, cumulative_overlap) @@ -384,3 +385,23 @@ def test_pca_attr_warning(u, attr): wmsg = f"The `{attr}` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): getattr(pca, attr) is pca.results[attr] + +@pytest.mark.parametrize( + "classname,is_parallelizable", + [ + (MDAnalysis.analysis.pca.PCA, False), + ] +) +def test_class_is_parallelizable(classname, is_parallelizable): + assert classname._analysis_algorithm_is_parallelizable == is_parallelizable + + +@pytest.mark.parametrize( + "classname,backends", + [ + (MDAnalysis.analysis.pca.PCA, ('serial',)), + ] +) +def test_supported_backends(classname, backends): + assert classname.get_supported_backends() == backends + diff --git a/testsuite/MDAnalysisTests/core/test_atom.py b/testsuite/MDAnalysisTests/core/test_atom.py index 35d5d67477a..d63d0574f06 100644 --- a/testsuite/MDAnalysisTests/core/test_atom.py +++ b/testsuite/MDAnalysisTests/core/test_atom.py @@ -121,6 +121,11 @@ def test_atom_pickle(self, universe, ix): atm_in = pickle.loads(pickle.dumps(atm_out)) assert atm_in == atm_out + def test_improper_initialisation(self, universe): + with pytest.raises(IndexError): + indices = [0, 1] + mda.core.groups.Atom(indices, universe) + class TestAtomNoForceNoVel(object): @staticmethod diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index 6173ea76f5c..4456362c498 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -1237,6 +1237,12 @@ def test_bad_make(self): with pytest.raises(TypeError): mda.core.groups.AtomGroup(['these', 'are', 'not', 'atoms']) + def test_invalid_index_initialisation(self, universe): + indices = [[1, 2, 3], + [4, 5, 6]] + with pytest.raises(IndexError): + mda.core.groups.AtomGroup(indices, universe) + def test_n_atoms(self, ag): assert ag.n_atoms == 3341 @@ -1592,6 +1598,13 @@ def test_index_advancedslice(self, universe): "an AtomGroup") assert_equal(ag[1], ag[-1], "advanced slicing does not preserve order") + def test_2d_indexing_caught(self, universe): + u = universe + index_2d = [[1, 2, 3], + [4, 5, 6]] + with pytest.raises(IndexError): + u.atoms[index_2d] + @pytest.mark.parametrize('sel', (np.array([True, False, True]), [True, False, True])) def test_boolean_indexing_2(self, universe, sel): diff --git a/testsuite/MDAnalysisTests/util.py b/testsuite/MDAnalysisTests/util.py index 8438a95bdbf..57b65df42c8 100644 --- a/testsuite/MDAnalysisTests/util.py +++ b/testsuite/MDAnalysisTests/util.py @@ -117,7 +117,7 @@ def import_not_available(module_name): # TODO: remove once these packages have a release # with NumPy 2 support if NumpyVersion(np.__version__) >= "2.0.0": - if module_name in {"rdkit", "parmed"}: + if module_name == "parmed": return True try: test = importlib.import_module(module_name)