From 94b1b799c609af6a2e5c6554cec822010b9e5332 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sun, 25 Aug 2024 21:08:13 +0100 Subject: [PATCH 01/57] Fix #4687 -- rdkit values in azure CI (#4688) * Investigate rdkit issue * Update azure-pipelines.yml * fix numpy 2.0 import block * fix imports --- azure-pipelines.yml | 4 ++-- package/MDAnalysis/converters/RDKit.py | 10 ++-------- testsuite/MDAnalysisTests/util.py | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 41424cf50de..cace2be35ba 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/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/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) From d73995a468523c35ee82562d942e8cc1f7e9d29d Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Mon, 26 Aug 2024 11:07:49 +0100 Subject: [PATCH 02/57] mark analysis.pca.PCA as not parallelizable (#4684) - fix #4680 - PCA explicitly marked as not parallelizable (at least not with simple split-apply-combine) - add tests - update CHANGELOG --- package/CHANGELOG | 3 ++- package/MDAnalysis/analysis/pca.py | 3 ++- .../MDAnalysisTests/analysis/test_pca.py | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 8ffcae6d24f..ede4ca29d8d 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -17,7 +17,7 @@ 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 * 2.8.0 @@ -55,6 +55,7 @@ Fixes Enhancements * Introduce parallelization API to `AnalysisBase` and to `analysis.rms.RMSD` class (Issue #4158, PR #4304) + * 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/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/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 + From 489905f613b39f6a546217867be3f3d7841294a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:21:35 +0100 Subject: [PATCH 03/57] Bump pypa/gh-action-pypi-publish in the github-actions group (#4699) Bumps the github-actions group with 1 update: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish). Updates `pypa/gh-action-pypi-publish` from 1.9.0 to 1.10.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9ce7d4af839..caf07bdfae2 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -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.0 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.0 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 From 277b99f712d5bd1b75ae25fb0fff9752a58f8ce4 Mon Sep 17 00:00:00 2001 From: MattTDavies <128810112+MattTDavies@users.noreply.github.com> Date: Sat, 7 Sep 2024 02:57:38 +0100 Subject: [PATCH 04/57] Further fixes to GroupBase high dimensional indexing: Frankenatom fix (#4692) * Moved index dimension check to initialisation, rather than indexing to more robustly catch Frankenatoms * Added improper atomgroup initialisation test * pep8 fixes * Added CHANGELOG info --- package/CHANGELOG | 4 ++-- package/MDAnalysis/core/groups.py | 13 +++++++------ testsuite/MDAnalysisTests/core/test_atom.py | 5 +++++ testsuite/MDAnalysisTests/core/test_atomgroup.py | 6 ++++++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index ede4ca29d8d..f2264922088 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -17,12 +17,12 @@ 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, orbeckst + yuxuanzhuang, PythonFZ, laksh-krishna-sharma, orbeckst, MattTDavies * 2.8.0 Fixes - * Catch higher dimensional indexing in GroupBase (Issue #4647) + * 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. diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index e8304d3b042..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() @@ -598,11 +601,6 @@ def __getitem__(self, item): # important for boolean slicing item = np.array(item) - if isinstance(item, np.ndarray) and item.ndim > 1: - # disallow high dimensional indexing. - # this doesnt stop the underlying issue - raise IndexError('Group index must be 1d') - # We specify _derived_class instead of self.__class__ to allow # subclasses, such as UpdatingAtomGroup, to control the class # resulting from slicing. @@ -4252,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/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 1497456e2da..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 From d11a1ef77628fd3bfe608cf7b9d8371e76ea2bec Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sat, 7 Sep 2024 17:20:52 +0100 Subject: [PATCH 05/57] [CI] MDAnalysis self-dependency build fix (#4502) * Fix MDAnalysis cron CI * Fix deployment workflow python and utility versions * Make sure we install mdakit dependencies using no-deps * Fix nightly wheel tests. --- .github/actions/build-src/action.yaml | 15 ++++ .github/actions/setup-deps/action.yaml | 9 --- .github/workflows/deploy.yaml | 10 +-- .github/workflows/gh-ci-cron.yaml | 97 +++++++++++++++----------- .github/workflows/gh-ci.yaml | 3 + 5 files changed, 80 insertions(+), 54 deletions(-) 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 caf07bdfae2..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.10.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.10.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 with: packages_dir: testsuite/dist skip_existing: true diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 72c7e365385..7d2ecc6f6ae 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 05bda86b3cd..471e3bd20de 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -276,6 +276,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 From 7618e05176b5f2c3008f375f840396ea8592488a Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sun, 8 Sep 2024 12:56:14 +0100 Subject: [PATCH 06/57] Update gh-ci-cron.yaml (#4705) --- .github/workflows/gh-ci-cron.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 7d2ecc6f6ae..4585fc4de71 100644 --- a/.github/workflows/gh-ci-cron.yaml +++ b/.github/workflows/gh-ci-cron.yaml @@ -5,9 +5,9 @@ on: - cron: "0 3 * * 2,5" workflow_dispatch: # Uncomment when you need to test on a PR - pull_request: - branches: - - develop + # pull_request: + # branches: + # - develop concurrency: From b3208b39aab61be53f8b610f1fef628f83262205 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:13:42 +0100 Subject: [PATCH 07/57] Implementation of Parallelization to analysis.gnm (#4700) * Fixes #4672 * Changes made in this Pull Request: - Parallelization of the class GNMAnalysis in analysis.gnm.py - Addition of parallelization tests (including fixtures in analysis/conftest.py) - update of CHANGELOG --------- Co-authored-by: Oliver Beckstein --- package/CHANGELOG | 4 +++- package/MDAnalysis/analysis/gnm.py | 22 ++++++++++++++++++- .../MDAnalysisTests/analysis/conftest.py | 6 +++++ .../MDAnalysisTests/analysis/test_gnm.py | 16 +++++++------- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index f2264922088..aecde6e6468 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -17,7 +17,8 @@ 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, orbeckst, MattTDavies + yuxuanzhuang, PythonFZ, laksh-krishna-sharma, orbeckst, MattTDavies, + talagayev * 2.8.0 @@ -55,6 +56,7 @@ 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) 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/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) From 319b6676a533d6609661b0ce22e88df212fc9060 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhuang Date: Mon, 9 Sep 2024 21:04:49 -0700 Subject: [PATCH 08/57] Remove GSD from optional test dependencies (#4707) * Fixed high dimensional GroupBase indexing. * fixed pep8 issues * Removed sanitisation * Fix #4687 -- rdkit values in azure CI (#4688) * Investigate rdkit issue * Update azure-pipelines.yml * fix numpy 2.0 import block * fix imports * mark analysis.pca.PCA as not parallelizable (#4684) - fix #4680 - PCA explicitly marked as not parallelizable (at least not with simple split-apply-combine) - add tests - update CHANGELOG * disable gsd * disable gsd in azure * reduce timeout and set logical * fix azure * restore timeout to 200 --------- Co-authored-by: Matthew Davies <128810112+MattTDavies@users.noreply.github.com> Co-authored-by: Irfan Alibay Co-authored-by: Oliver Beckstein --- .github/workflows/gh-ci.yaml | 7 +++++-- azure-pipelines.yml | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index 471e3bd20de..8a45aec650f 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -85,6 +85,8 @@ jobs: with: micromamba: true full-deps: ${{ matrix.full-deps }} + # disable GSD because it occasionally introduce hanging in testing #4209 + gsd: '' # in most cases will just default to empty, i.e. pick up max version from other deps numpy: ${{ matrix.numpy }} extra-pip-deps: ${{ matrix.extra-pip-deps }} @@ -113,7 +115,7 @@ jobs: PYTEST_FLAGS="${PYTEST_FLAGS} --cov-config=.coveragerc --cov=MDAnalysis --cov-report=xml" fi echo $PYTEST_FLAGS - pytest -n auto --timeout=200 testsuite/MDAnalysisTests $PYTEST_FLAGS + pytest -n logical --timeout=200 testsuite/MDAnalysisTests $PYTEST_FLAGS - name: run_asv if: contains(matrix.name, 'asv_check') @@ -161,6 +163,7 @@ jobs: with: micromamba: true full-deps: true + gsd: '' extra-pip-deps: "docutils sphinx-sitemap sphinxcontrib-bibtex pybtex pybtex-docutils" extra-conda-deps: "mdanalysis-sphinx-theme>=1.3.0" @@ -281,4 +284,4 @@ jobs: - name: run tests working-directory: ./dist - run: python -m pytest --timeout=200 -n auto --pyargs MDAnalysisTests + run: python -m pytest --timeout=200 -n logical --pyargs MDAnalysisTests diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cace2be35ba..250f3ba63b5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -105,7 +105,6 @@ jobs: biopython "chemfiles>=0.10,<0.10.4" duecredit - "gsd>3.0.0" joblib GridDataFormats mmtf-python @@ -114,6 +113,8 @@ jobs: pytng>=0.2.3 rdkit>=2020.03.1 tidynamics>=1.0.0 + # remove from azure to avoid test hanging #4707 + # "gsd>3.0.0" displayName: 'Install additional dependencies for 64-bit tests' condition: and(succeeded(), eq(variables['PYTHON_ARCH'], 'x64')) - script: >- @@ -128,7 +129,7 @@ jobs: displayName: 'Check installed packages' - powershell: | cd testsuite - pytest MDAnalysisTests --disable-pytest-warnings -n auto --timeout=200 -rsx --cov=MDAnalysis + pytest MDAnalysisTests --disable-pytest-warnings -n logical --timeout=200 -rsx --cov=MDAnalysis displayName: 'Run MDAnalysis Test Suite' - script: | curl -s https://codecov.io/bash | bash From 4fafd51de84d5b89be0559a412acefde0040847c Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Sat, 14 Sep 2024 02:19:44 +0200 Subject: [PATCH 09/57] Implementation of Parallelization to analysis.bat (#4693) * Fixes #4663 * Changes made in this Pull Request: - parallelization of the class BAT in analysis.bat.py - addition of parallelization tests (including fixtures in analysis/conftest.py) - update docs for analysis class - update of CHANGELOG --- package/CHANGELOG | 1 + package/MDAnalysis/analysis/bat.py | 29 ++++++++++++++++--- .../MDAnalysisTests/analysis/conftest.py | 6 ++++ .../MDAnalysisTests/analysis/test_bat.py | 12 ++++---- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index aecde6e6468..d799b52e4e0 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -58,6 +58,7 @@ Enhancements (Issue #4158, PR #4304) * Enables parallelization for analysis.gnm.GNMAnalysis (Issue #4672) * explicitly mark `analysis.pca.PCA` as not parallelizable (Issue #4680) + * enables parallelization for analysis.bat.BAT (Issue #4663) * 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/bat.py b/package/MDAnalysis/analysis/bat.py index 0c3e15a32e9..5186cb6c882 100644 --- a/package/MDAnalysis/analysis/bat.py +++ b/package/MDAnalysis/analysis/bat.py @@ -164,7 +164,7 @@ class to calculate dihedral angles for a given set of atoms or residues import copy import MDAnalysis as mda -from .base import AnalysisBase +from .base import AnalysisBase, ResultsGroup from MDAnalysis.lib.distances import calc_bonds, calc_angles, calc_dihedrals from MDAnalysis.lib.mdamath import make_whole @@ -253,10 +253,28 @@ class BAT(AnalysisBase): Bond-Angle-Torsions (BAT) internal coordinates will be computed for the group of atoms and all frame in the trajectory belonging to `ag`. + .. 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. + """ - @due.dcite(Doi("10.1002/jcc.26036"), - description="Bond-Angle-Torsions Coordinate Transformation", - path="MDAnalysis.analysis.bat.BAT") + _analysis_algorithm_is_parallelizable = True + + @classmethod + def get_supported_backends(cls): + return ( + "serial", + "multiprocessing", + "dask", + ) + + @due.dcite( + Doi("10.1002/jcc.26036"), + description="Bond-Angle-Torsions Coordinate Transformation", + path="MDAnalysis.analysis.bat.BAT", + ) + def __init__(self, ag, initial_atom=None, filename=None, **kwargs): r"""Parameters ---------- @@ -558,3 +576,6 @@ def Cartesian(self, bat_frame): def atoms(self): """The atomgroup for which BAT are computed (read-only property)""" return self._ag + + def _get_aggregator(self): + return ResultsGroup(lookup={'bat': ResultsGroup.ndarray_vstack}) diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index 75d62284b7b..f53cdcfaac1 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -7,6 +7,7 @@ OldAPIAnalysis, ) from MDAnalysis.analysis.rms import RMSD, RMSF +from MDAnalysis.analysis.bat import BAT from MDAnalysis.lib.util import is_installed from MDAnalysis.analysis.gnm import GNMAnalysis @@ -93,3 +94,8 @@ def client_RMSF(request): @pytest.fixture(scope='module', params=params_for_cls(GNMAnalysis)) def client_GNMAnalysis(request): return request.param + + +@pytest.fixture(scope='module', params=params_for_cls(BAT)) +def client_BAT(request): + return request.param \ No newline at end of file diff --git a/testsuite/MDAnalysisTests/analysis/test_bat.py b/testsuite/MDAnalysisTests/analysis/test_bat.py index c80353baff6..5fb2603df62 100644 --- a/testsuite/MDAnalysisTests/analysis/test_bat.py +++ b/testsuite/MDAnalysisTests/analysis/test_bat.py @@ -41,16 +41,16 @@ def selected_residues(self): return ag @pytest.fixture() - def bat(self, selected_residues): + def bat(self, selected_residues, client_BAT): R = BAT(selected_residues) - R.run() + R.run(**client_BAT) return R.results.bat @pytest.fixture - def bat_npz(self, tmpdir, selected_residues): + def bat_npz(self, tmpdir, selected_residues, client_BAT): filename = str(tmpdir / 'test_bat_IO.npy') R = BAT(selected_residues) - R.run() + R.run(**client_BAT) R.save(filename) return filename @@ -73,8 +73,8 @@ def test_bat_coordinates(self, bat): atol=1.5e-5, err_msg="error: BAT coordinates should match test values") - def test_bat_coordinates_single_frame(self, selected_residues): - bat = BAT(selected_residues).run(start=1, stop=2).results.bat + def test_bat_coordinates_single_frame(self, selected_residues, client_BAT): + bat = BAT(selected_residues).run(start=1, stop=2, **client_BAT).results.bat test_bat = [np.load(BATArray)[1]] assert_allclose( bat, From 8939b3e74dc78fc307291a4471a43fb70c8c1628 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhuang Date: Thu, 26 Sep 2024 03:03:56 +0200 Subject: [PATCH 10/57] Adjust num of parts to accomodate number of frames (#4710) * Fixes #4685 * Changes made in this Pull Request: - set n_parts to the total number of frames being analyzed if n_parts is bigger and warn - add test - update CHANGELOG --- package/CHANGELOG | 2 ++ package/MDAnalysis/analysis/base.py | 7 +++++++ testsuite/MDAnalysisTests/analysis/test_base.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/package/CHANGELOG b/package/CHANGELOG index d799b52e4e0..35e9e0e3cbb 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,8 @@ The rules for this file: * 2.8.0 Fixes + * set `n_parts` to the total number of frames being analyzed if + `n_parts` is bigger. (Issue #4685) * 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) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 47a7eccd137..930f4fa90c2 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -577,6 +577,13 @@ def _setup_computation_groups( # similar to list(enumerate(frames)) enumerated_frames = np.vstack([np.arange(len(used_frames)), used_frames]).T + if len(enumerated_frames) == 0: + return [np.empty((0, 2), dtype=np.int64)] + elif len(enumerated_frames) < n_parts: + # Issue #4685 + n_parts = len(enumerated_frames) + warnings.warn(f"Set `n_parts` to {n_parts} to match the total " + "number of frames being analyzed") return np.array_split(enumerated_frames, n_parts) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 6e6b7921960..623b2ca5fe1 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -198,6 +198,20 @@ def test_start_stop_step_parallel(u, run_kwargs, frames, client_FrameAnalysis): assert_almost_equal(an.times, frames+1, decimal=4, err_msg=TIMES_ERR) +def test_reset_n_parts_to_n_frames(u): + """ + Issue #4685 + https://github.com/MDAnalysis/mdanalysis/issues/4685 + """ + a = FrameAnalysis(u.trajectory) + with pytest.warns(UserWarning, match='Set `n_parts` to'): + a.run(backend='multiprocessing', + start=0, + stop=1, + n_workers=2, + n_parts=2) + + @pytest.mark.parametrize('run_kwargs,frames', [ ({}, np.arange(98)), ({'start': 20}, np.arange(20, 98)), From b7f91a5e4d70786e1539576bd012484b5685b365 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Fri, 27 Sep 2024 01:23:52 -0700 Subject: [PATCH 11/57] Fix docs in timestep.pyx (#4719) - changed outdated reference of AtomGroup.coordinates() to AtomGroup.positions - changed outdated reference of AtomGroup.velocities() to AtomGroup.velocities - minor doc fixes --- package/MDAnalysis/coordinates/timestep.pyx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package/MDAnalysis/coordinates/timestep.pyx b/package/MDAnalysis/coordinates/timestep.pyx index 09304ec85ee..f2649d44c63 100644 --- a/package/MDAnalysis/coordinates/timestep.pyx +++ b/package/MDAnalysis/coordinates/timestep.pyx @@ -70,11 +70,11 @@ MDAnalysis. .. Note:: Normally one does not directly access :attr:`_pos` but uses - the :meth:`~MDAnalysis.core.groups.AtomGroup.coordinates` - method of an :class:`~MDAnalysis.core.groups.AtomGroup` but + the :attr:`~MDAnalysis.core.groups.AtomGroup.positions` + attribute of a :class:`~MDAnalysis.core.groups.AtomGroup` but sometimes it can be faster to directly use the raw coordinates. Any changes to this array are immediately - reflected in atom positions. If the frame is written to a new + reflected in the atom positions. If the frame is written to a new trajectory then the coordinates are changed. If a new trajectory frame is loaded, then *all* contents of :attr:`_pos` are overwritten. @@ -83,17 +83,17 @@ MDAnalysis. :class:`numpy.ndarray` of dtype :class:`~numpy.float32`. of shape (*n_atoms*, 3), holding the raw velocities (in MDAnalysis - units, i.e. typically Å/ps). + units, i.e., Å/ps). .. Note:: Normally velocities are accessed through the - :attr:`velocities` or the - :meth:`~MDAnalysis.core.groups.AtomGroup.velocities` - method of an :class:`~MDAnalysis.core.groups.AtomGroup` + :attr:`Timestep.velocities` attribute or the + :attr:`~MDAnalysis.core.groups.AtomGroup.velocities` + attribute of an :class:`~MDAnalysis.core.groups.AtomGroup` :attr:`~Timestep._velocities` only exists if the :attr:`has_velocities` - flag is True + flag is ``True``. .. versionadded:: 0.7.5 @@ -103,7 +103,7 @@ MDAnalysis. (*n_atoms*, 3), holding the forces :attr:`~Timestep._forces` only exists if :attr:`has_forces` - is True + is ``True``. .. versionadded:: 0.11.0 Added as optional to :class:`Timestep` From 84ee67b99fc3bf165d2f58057fac3315d8bb33af Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Fri, 27 Sep 2024 02:59:17 -0700 Subject: [PATCH 12/57] added parallelization in analysis.dihedrals (#4682) * fix #4673 * Changes made in this Pull Request: - parallelized Dihedral, Ramachandran, Janin - added tests (including fixtures in analysis/conftest.py) - update CHANGELOG Co-authored-by: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> --- package/CHANGELOG | 10 +- package/MDAnalysis/analysis/dihedrals.py | 27 +++++- package/MDAnalysis/analysis/rms.py | 14 +-- .../MDAnalysisTests/analysis/conftest.py | 31 +++++- .../analysis/test_dihedrals.py | 94 +++++++++++++------ .../MDAnalysisTests/analysis/test_rms.py | 6 +- 6 files changed, 133 insertions(+), 49 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 35e9e0e3cbb..62ab8a04630 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -38,14 +38,14 @@ Fixes * Fix `MDAnalysis.analysis.align.AlignTraj` not accepting writer kwargs (Issue #4564, PR #4565) * Fix #4259 via removing argument `parallelizable` of `NoJump` transformation. - * Fix doctest errors of analysis/pca.py related to rounding issues + * Fix doctest errors of analysis/pca.py related to rounding issues (Issue #3925, PR #4377) * Convert openmm Quantity to raw value for KE and PE in OpenMMSimulationReader. * Atomname methods can handle empty groups (Issue #2879, PR #4529) * Add support for TPR files produced by Gromacs 2024.1 (PR #4523) * Remove mutable data from ``progressbar_kwargs`` argument in ``AnalysisBase.run()`` (PR #4459) - * Fix ChainReader `__repr__()` method when sub-reader is MemoryReader + * Fix ChainReader `__repr__()` method when sub-reader is MemoryReader (Issue #3349, PR #4407) * Fix bug in PCA preventing use of `frames=...` syntax (PR #4423) * Fix `analysis/diffusionmap.py` iteration through trajectory to iteration @@ -61,15 +61,17 @@ Enhancements * Enables parallelization for analysis.gnm.GNMAnalysis (Issue #4672) * explicitly mark `analysis.pca.PCA` as not parallelizable (Issue #4680) * enables parallelization for analysis.bat.BAT (Issue #4663) + * enable parallelization for analysis.dihedrals.{Dihedral,Ramachandran,Janin} + (Issue #4673) * 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()` (PR #4531) * Improved performance of PDBWriter (Issue #2785, PR #4472) * Added parsing of arbitrary columns of the LAMMPS dump parser. (Issue #3504) - * Documented the r0 attribute in the `Contacts` class and added the + * Documented the r0 attribute in the `Contacts` class and added the `n_initial_contacts` attribute, with documentation. (Issue #2604, PR #4415) - * Implement average structures with iterative algorithm from + * Implement average structures with iterative algorithm from DOI 10.1021/acs.jpcb.7b11988. (Issue #2039, PR #4524) Changes diff --git a/package/MDAnalysis/analysis/dihedrals.py b/package/MDAnalysis/analysis/dihedrals.py index 29c8eee0bd8..56b95fc42c3 100644 --- a/package/MDAnalysis/analysis/dihedrals.py +++ b/package/MDAnalysis/analysis/dihedrals.py @@ -245,7 +245,7 @@ import warnings import MDAnalysis as mda -from MDAnalysis.analysis.base import AnalysisBase +from MDAnalysis.analysis.base import AnalysisBase, ResultsGroup from MDAnalysis.lib.distances import calc_dihedrals from MDAnalysis.analysis.data.filenames import Rama_ref, Janin_ref @@ -267,8 +267,16 @@ class Dihedral(AnalysisBase): .. versionchanged:: 2.0.0 :attr:`angles` results are now stored in a :class:`MDAnalysis.analysis.base.Results` instance. - + .. versionchanged:: 2.8.0 + introduced :meth:`get_supported_backends` allowing for parallel + execution on ``multiprocessing`` and ``dask`` backends. """ + _analysis_algorithm_is_parallelizable = True + + @classmethod + def get_supported_backends(cls): + return ('serial', 'multiprocessing', 'dask',) + def __init__(self, atomgroups, **kwargs): """Parameters @@ -298,6 +306,9 @@ def __init__(self, atomgroups, **kwargs): def _prepare(self): self.results.angles = [] + def _get_aggregator(self): + return ResultsGroup(lookup={'angles': ResultsGroup.ndarray_vstack}) + def _single_frame(self): angle = calc_dihedrals(self.ag1.positions, self.ag2.positions, self.ag3.positions, self.ag4.positions, @@ -379,8 +390,15 @@ class Ramachandran(AnalysisBase): .. versionchanged:: 2.0.0 :attr:`angles` results are now stored in a :class:`MDAnalysis.analysis.base.Results` instance. - + .. versionchanged:: 2.8.0 + introduced :meth:`get_supported_backends` allowing for parallel + execution on ``multiprocessing`` and ``dask`` backends. """ + _analysis_algorithm_is_parallelizable = True + + @classmethod + def get_supported_backends(cls): + return ('serial', 'multiprocessing', 'dask',) def __init__(self, atomgroup, c_name='C', n_name='N', ca_name='CA', check_protein=True, **kwargs): @@ -437,6 +455,9 @@ def __init__(self, atomgroup, c_name='C', n_name='N', ca_name='CA', def _prepare(self): self.results.angles = [] + def _get_aggregator(self): + return ResultsGroup(lookup={'angles': ResultsGroup.ndarray_vstack}) + def _single_frame(self): phi_angles = calc_dihedrals(self.ag1.positions, self.ag2.positions, self.ag3.positions, self.ag4.positions, diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index afb11ed7d2e..b8dcb97065f 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -103,16 +103,16 @@ ax.set_xlabel("time (ps)") ax.set_ylabel(r"RMSD ($\\AA$)") fig.savefig("rmsd_all_CORE_LID_NMP_ref1AKE.pdf") - + .. _figure-RMSD: .. figure:: /images/RSMD_plot.png :scale: 50 % :alt: RMSD plot - + RMSD plot for backbone and CORE, LID, NMP domain of the protein. - + Functions --------- @@ -359,8 +359,8 @@ class RMSD(AnalysisBase): :attr:`rmsd` results are now stored in a :class:`MDAnalysis.analysis.base.Results` instance. .. versionchanged:: 2.8.0 - introduced a :meth:`get_supported_backends` allowing for execution on with - ``multiprocessing`` and ``dask`` backends. + introduced :meth:`get_supported_backends` allowing for parallel + execution on ``multiprocessing`` and ``dask`` backends. """ _analysis_algorithm_is_parallelizable = True @@ -427,8 +427,8 @@ def __init__(self, atomgroup, reference=None, select='all', weights_groupselections : False or list of {"mass", ``None`` or array_like} (optional) 1. ``False`` will apply imposed weights to `groupselections` from - ``weights`` option if ``weights`` is either ``"mass"`` or ``None``. - Otherwise will assume a list of length equal to length of + ``weights`` option if ``weights`` is either ``"mass"`` or ``None``. + Otherwise will assume a list of length equal to length of `groupselections` filled with ``None`` values. 2. A list of {"mass", ``None`` or array_like} with the length of `groupselections` diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index f53cdcfaac1..5c6157d3bb6 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -7,10 +7,10 @@ OldAPIAnalysis, ) from MDAnalysis.analysis.rms import RMSD, RMSF +from MDAnalysis.analysis.dihedrals import Dihedral, Ramachandran, Janin from MDAnalysis.analysis.bat import BAT -from MDAnalysis.lib.util import is_installed from MDAnalysis.analysis.gnm import GNMAnalysis - +from MDAnalysis.lib.util import is_installed def params_for_cls(cls, exclude: list[str] = None): """ @@ -81,6 +81,8 @@ def client_OldAPIAnalysis(request): return request.param +# MDAnalysis.analysis.rms + @pytest.fixture(scope='module', params=params_for_cls(RMSD)) def client_RMSD(request): return request.param @@ -91,11 +93,32 @@ def client_RMSF(request): return request.param +# MDAnalysis.analysis.dihedrals + +@pytest.fixture(scope='module', params=params_for_cls(Dihedral)) +def client_Dihedral(request): + return request.param + + +@pytest.fixture(scope='module', params=params_for_cls(Ramachandran)) +def client_Ramachandran(request): + return request.param + + +@pytest.fixture(scope='module', params=params_for_cls(Janin)) +def client_Janin(request): + return request.param + + +# MDAnalysis.analysis.gnm + @pytest.fixture(scope='module', params=params_for_cls(GNMAnalysis)) def client_GNMAnalysis(request): return request.param - + +# MDAnalysis.analysis.bat + @pytest.fixture(scope='module', params=params_for_cls(BAT)) def client_BAT(request): - return request.param \ No newline at end of file + return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_dihedrals.py b/testsuite/MDAnalysisTests/analysis/test_dihedrals.py index 731d5cb0397..c5d291bf96d 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dihedrals.py +++ b/testsuite/MDAnalysisTests/analysis/test_dihedrals.py @@ -30,6 +30,7 @@ DihedralsArray, RamaArray, GLYRamaArray, JaninArray, LYSJaninArray, PDB_rama, PDB_janin) +import MDAnalysis.analysis.dihedrals from MDAnalysis.analysis.dihedrals import Dihedral, Ramachandran, Janin @@ -42,36 +43,42 @@ def atomgroup(self): return ag - def test_dihedral(self, atomgroup): - dihedral = Dihedral([atomgroup]).run() + def test_dihedral(self, atomgroup, client_Dihedral): + # client_Dihedral is defined in testsuite/analysis/conftest.py + # among with other testing fixtures. During testing, it will + # collect all possible backends and reasonable number of workers + # for a given AnalysisBase subclass, and extend the tests + # to run with all of them. + + dihedral = Dihedral([atomgroup]).run(**client_Dihedral) test_dihedral = np.load(DihedralArray) assert_allclose(dihedral.results.angles, test_dihedral, rtol=0, atol=1.5e-5, err_msg="error: dihedral angles should " "match test values") - def test_dihedral_single_frame(self, atomgroup): - dihedral = Dihedral([atomgroup]).run(start=5, stop=6) + def test_dihedral_single_frame(self, atomgroup, client_Dihedral): + dihedral = Dihedral([atomgroup]).run(start=5, stop=6, **client_Dihedral) test_dihedral = [np.load(DihedralArray)[5]] assert_allclose(dihedral.results.angles, test_dihedral, rtol=0, atol=1.5e-5, err_msg="error: dihedral angles should " "match test vales") - def test_atomgroup_list(self, atomgroup): - dihedral = Dihedral([atomgroup, atomgroup]).run() + def test_atomgroup_list(self, atomgroup, client_Dihedral): + dihedral = Dihedral([atomgroup, atomgroup]).run(**client_Dihedral) test_dihedral = np.load(DihedralsArray) assert_allclose(dihedral.results.angles, test_dihedral, rtol=0, atol=1.5e-5, err_msg="error: dihedral angles should " "match test values") - def test_enough_atoms(self, atomgroup): + def test_enough_atoms(self, atomgroup, client_Dihedral): with pytest.raises(ValueError): - dihedral = Dihedral([atomgroup[:2]]).run() + dihedral = Dihedral([atomgroup[:2]]).run(**client_Dihedral) - def test_dihedral_attr_warning(self, atomgroup): - dihedral = Dihedral([atomgroup]).run(stop=2) + def test_dihedral_attr_warning(self, atomgroup, client_Dihedral): + dihedral = Dihedral([atomgroup]).run(stop=2, **client_Dihedral) wmsg = "The `angle` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): @@ -88,37 +95,39 @@ def universe(self): def rama_ref_array(self): return np.load(RamaArray) - def test_ramachandran(self, universe, rama_ref_array): - rama = Ramachandran(universe.select_atoms("protein")).run() + def test_ramachandran(self, universe, rama_ref_array, client_Ramachandran): + rama = Ramachandran(universe.select_atoms("protein")).run( + **client_Ramachandran) assert_allclose(rama.results.angles, rama_ref_array, rtol=0, atol=1.5e-5, err_msg="error: dihedral angles should " "match test values") - def test_ramachandran_single_frame(self, universe, rama_ref_array): + def test_ramachandran_single_frame(self, universe, rama_ref_array, client_Ramachandran): rama = Ramachandran(universe.select_atoms("protein")).run( - start=5, stop=6) + start=5, stop=6, **client_Ramachandran) assert_allclose(rama.results.angles[0], rama_ref_array[5], rtol=0, atol=1.5e-5, err_msg="error: dihedral angles should " "match test values") - def test_ramachandran_residue_selections(self, universe): - rama = Ramachandran(universe.select_atoms("resname GLY")).run() + def test_ramachandran_residue_selections(self, universe, client_Ramachandran): + rama = Ramachandran(universe.select_atoms("resname GLY")).run( + **client_Ramachandran) test_rama = np.load(GLYRamaArray) assert_allclose(rama.results.angles, test_rama, rtol=0, atol=1.5e-5, err_msg="error: dihedral angles should " "match test values") - def test_outside_protein_length(self, universe): + def test_outside_protein_length(self, universe, client_Ramachandran): with pytest.raises(ValueError): rama = Ramachandran(universe.select_atoms("resid 220"), - check_protein=True).run() + check_protein=True).run(**client_Ramachandran) - def test_outside_protein_unchecked(self, universe): + def test_outside_protein_unchecked(self, universe, client_Ramachandran): rama = Ramachandran(universe.select_atoms("resid 220"), - check_protein=False).run() + check_protein=False).run(**client_Ramachandran) def test_protein_ends(self, universe): with pytest.warns(UserWarning) as record: @@ -158,15 +167,15 @@ def universe_tpr(self): def janin_ref_array(self): return np.load(JaninArray) - def test_janin(self, universe, janin_ref_array): - self._test_janin(universe, janin_ref_array) + def test_janin(self, universe, janin_ref_array, client_Janin): + self._test_janin(universe, janin_ref_array, client_Janin) - def test_janin_tpr(self, universe_tpr, janin_ref_array): + def test_janin_tpr(self, universe_tpr, janin_ref_array, client_Janin): """Test that CYSH are filtered (#2898)""" - self._test_janin(universe_tpr, janin_ref_array) + self._test_janin(universe_tpr, janin_ref_array, client_Janin) - def _test_janin(self, u, ref_array): - janin = Janin(u.select_atoms("protein")).run() + def _test_janin(self, u, ref_array, client_Janin): + janin = Janin(u.select_atoms("protein")).run(**client_Janin) # Test precision lowered to account for platform differences with osx assert_allclose(janin.results.angles, ref_array, rtol=0, atol=1.5e-3, @@ -180,8 +189,8 @@ def test_janin_single_frame(self, universe, janin_ref_array): err_msg="error: dihedral angles should " "match test values") - def test_janin_residue_selections(self, universe): - janin = Janin(universe.select_atoms("resname LYS")).run() + def test_janin_residue_selections(self, universe, client_Janin): + janin = Janin(universe.select_atoms("resname LYS")).run(**client_Janin) test_janin = np.load(LYSJaninArray) assert_allclose(janin.results.angles, test_janin, rtol=0, atol=1.5e-3, @@ -213,3 +222,32 @@ def test_janin_attr_warning(self, universe): wmsg = "The `angle` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): assert_equal(janin.angles, janin.results.angles) + + +# tests for parallelization + +@pytest.mark.parametrize( + "classname,is_parallelizable", + [ + (MDAnalysis.analysis.dihedrals.Dihedral, True), + (MDAnalysis.analysis.dihedrals.Ramachandran, True), + (MDAnalysis.analysis.dihedrals.Janin, True), + ] +) +def test_class_is_parallelizable(classname, is_parallelizable): + assert classname._analysis_algorithm_is_parallelizable == is_parallelizable + + +@pytest.mark.parametrize( + "classname,backends", + [ + (MDAnalysis.analysis.dihedrals.Dihedral, + ('serial', 'multiprocessing', 'dask',)), + (MDAnalysis.analysis.dihedrals.Ramachandran, + ('serial', 'multiprocessing', 'dask',)), + (MDAnalysis.analysis.dihedrals.Janin, + ('serial', 'multiprocessing', 'dask',)), + ] +) +def test_supported_backends(classname, backends): + assert classname.get_supported_backends() == backends diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index d42993feb46..87822e0b79c 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -267,7 +267,7 @@ def test_custom_groupselection_weights_applied_1D_array(self, universe, client_R select='backbone', groupselections=['name CA and resid 1-5', 'name CA and resid 1'], weights=None, - weights_groupselections=[[1, 0, 0, 0, 0], None]).run(step=49, + weights_groupselections=[[1, 0, 0, 0, 0], None]).run(step=49, **client_RMSD ) @@ -281,7 +281,7 @@ def test_custom_groupselection_weights_applied_mass(self, universe, correct_valu groupselections=['all', 'all'], weights=None, weights_groupselections=['mass', - universe.atoms.masses]).run(step=49, + universe.atoms.masses]).run(step=49, **client_RMSD ) @@ -439,7 +439,7 @@ def test_rmsf_attr_warning(self, universe, client_RMSF): (MDAnalysis.analysis.rms.RMSF, False), ] ) -def test_not_parallelizable(classname, is_parallelizable): +def test_class_is_parallelizable(classname, is_parallelizable): assert classname._analysis_algorithm_is_parallelizable == is_parallelizable From 78edac04b9048ed0986e54fc5429a60f32215da4 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Sat, 28 Sep 2024 13:39:56 -0700 Subject: [PATCH 13/57] [doc] fix versionchanged display in LAMMPS DumpsReader (#4720) fix: versionchanged was not displayed correctly in coordinates.LAMMPS.DumpReader --- package/MDAnalysis/coordinates/LAMMPS.py | 1 + 1 file changed, 1 insertion(+) diff --git a/package/MDAnalysis/coordinates/LAMMPS.py b/package/MDAnalysis/coordinates/LAMMPS.py index e772a3e3924..5099c742fcb 100644 --- a/package/MDAnalysis/coordinates/LAMMPS.py +++ b/package/MDAnalysis/coordinates/LAMMPS.py @@ -547,6 +547,7 @@ class DumpReader(base.ReaderBase): **kwargs Other keyword arguments used in :class:`~MDAnalysis.coordinates.base.ReaderBase` + .. versionchanged:: 2.7.0 Reading of arbitrary, additional columns is now supported. (Issue #3608) From 56766e9502fc6908b7d2a1b906068b6b74d4c2b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:28:40 +0100 Subject: [PATCH 14/57] Bump the github-actions group with 2 updates (#4723) Bumps the github-actions group with 2 updates: [actions/setup-python](https://github.com/actions/setup-python) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish). Updates `actions/setup-python` from 4 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) Updates `pypa/gh-action-pypi-publish` from 1.10.0 to 1.10.2 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.0...v1.10.2) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yaml | 8 ++++---- .github/workflows/gh-ci-cron.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 377575ef8c1..09a79a64c20 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -142,7 +142,7 @@ jobs: mv dist/MDAnalysisTests-* testsuite/dist - name: upload_source_and_wheels - uses: pypa/gh-action-pypi-publish@v1.10.1 + uses: pypa/gh-action-pypi-publish@v1.10.2 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.10.1 + uses: pypa/gh-action-pypi-publish@v1.10.2 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.10.0 + uses: pypa/gh-action-pypi-publish@v1.10.2 upload_pypi_mdanalysistests: if: | @@ -227,7 +227,7 @@ jobs: mv dist/MDAnalysisTests-* testsuite/dist - name: upload_tests - uses: pypa/gh-action-pypi-publish@v1.10.0 + uses: pypa/gh-action-pypi-publish@v1.10.2 with: packages_dir: testsuite/dist diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 4585fc4de71..0df5ad460f3 100644 --- a/.github/workflows/gh-ci-cron.yaml +++ b/.github/workflows/gh-ci-cron.yaml @@ -40,7 +40,7 @@ jobs: with: os-type: "ubuntu" - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From 650747d2612feace9b9704e28e08cd06bce46857 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Wed, 2 Oct 2024 10:43:06 +0200 Subject: [PATCH 15/57] Add black configuration ignoring all files (#4717) * add black configuration, ignoring all files * black * black version * adios 39 --- package/pyproject.toml | 6 ++++++ testsuite/pyproject.toml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/package/pyproject.toml b/package/pyproject.toml index 05b0eb3ba16..3d1ea04bddb 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -121,3 +121,9 @@ find = {} MDAnalysis = [ 'analysis/data/*.npy', ] + +[tool.black] +line-length = 79 +target-version = ['py310', 'py311', 'py312'] +extend-exclude = '.' +required-version = '24' diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 4af3b1d85c2..a757271db9d 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -151,3 +151,9 @@ filterwarnings = [ # NamedStream warnings "ignore:Constructed NamedStream:RuntimeWarning", ] + +[tool.black] +line-length = 79 +target-version = ['py310', 'py311', 'py312'] +extend-exclude = '.' +required-version = '24' From 474be5bbe32270bb9ddf02dc3cab74d3c1312c5e Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Mon, 7 Oct 2024 06:17:42 +0200 Subject: [PATCH 16/57] Implementation of Parallelization to analysis.hydrogenbonds.hbond_analysis (#4718) - Fixes #4664 - Parallelization of the backend support to the class HydrogenBondAnalysis in hbond_analysis.py - Moved setting up of donors and acceptors from _prepare() to __init__() (needed to make parallel processing work) - Addition of parallelization tests in test_hydrogenbonds_analysis.py and fixtures in conftest.py - Updated Changelog --------- Co-authored-by: Yuxuan Zhuang Co-authored-by: Oliver Beckstein --- package/CHANGELOG | 3 +- .../analysis/hydrogenbonds/hbond_analysis.py | 36 +++++++++----- .../MDAnalysisTests/analysis/conftest.py | 10 ++++ .../analysis/test_hydrogenbonds_analysis.py | 48 ++++++++++--------- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 62ab8a04630..ec5fe28ecad 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -62,7 +62,8 @@ Enhancements * explicitly mark `analysis.pca.PCA` as not parallelizable (Issue #4680) * enables parallelization for analysis.bat.BAT (Issue #4663) * enable parallelization for analysis.dihedrals.{Dihedral,Ramachandran,Janin} - (Issue #4673) + (Issue #4673) + * Enables parallelization for analysis.hydrogenbonds.hbond_analysis.HydrogenBondAnalysis (Issue #4664) * 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/hydrogenbonds/hbond_analysis.py b/package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py index e0597e2c3d2..3bf9d5c27a9 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py @@ -241,7 +241,7 @@ import numpy as np -from ..base import AnalysisBase, Results +from ..base import AnalysisBase, Results, ResultsGroup from MDAnalysis.lib.distances import capped_distance, calc_angles from MDAnalysis.lib.correlations import autocorrelation, correct_intermittency from MDAnalysis.exceptions import NoDataError @@ -267,6 +267,12 @@ class HydrogenBondAnalysis(AnalysisBase): Perform an analysis of hydrogen bonds in a Universe. """ + _analysis_algorithm_is_parallelizable = True + + @classmethod + def get_supported_backends(cls): + return ('serial', 'multiprocessing', 'dask',) + def __init__(self, universe, donors_sel=None, hydrogens_sel=None, acceptors_sel=None, between=None, d_h_cutoff=1.2, @@ -335,7 +341,9 @@ def __init__(self, universe, .. versionchanged:: 2.4.0 Added use of atom types in selection strings for hydrogen atoms, bond donors, or bond acceptors - + .. versionchanged:: 2.8.0 + Introduced :meth:`get_supported_backends` allowing for parallel execution on + :mod:`multiprocessing` and :mod:`dask` backends. """ self.u = universe @@ -383,6 +391,17 @@ def __init__(self, universe, self.results = Results() self.results.hbonds = None + # Set atom selections if they have not been provided + if self.acceptors_sel is None: + self.acceptors_sel = self.guess_acceptors() + if self.hydrogens_sel is None: + self.hydrogens_sel = self.guess_hydrogens() + + # Select atom groups + self._acceptors = self.u.select_atoms(self.acceptors_sel, + updating=self.update_selections) + self._donors, self._hydrogens = self._get_dh_pairs() + def guess_hydrogens(self, select='all', max_mass=1.1, @@ -699,16 +718,6 @@ def _filter_atoms(self, donors, acceptors): def _prepare(self): self.results.hbonds = [[], [], [], [], [], []] - # Set atom selections if they have not been provided - if self.acceptors_sel is None: - self.acceptors_sel = self.guess_acceptors() - if self.hydrogens_sel is None: - self.hydrogens_sel = self.guess_hydrogens() - - # Select atom groups - self._acceptors = self.u.select_atoms(self.acceptors_sel, - updating=self.update_selections) - self._donors, self._hydrogens = self._get_dh_pairs() def _single_frame(self): @@ -788,6 +797,9 @@ def _conclude(self): self.results.hbonds = np.asarray(self.results.hbonds).T + def _get_aggregator(self): + return ResultsGroup(lookup={'hbonds': ResultsGroup.ndarray_hstack}) + @property def hbonds(self): wmsg = ("The `hbonds` attribute was deprecated in MDAnalysis 2.0.0 " diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index 5c6157d3bb6..b5fe975dcd8 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -10,8 +10,12 @@ from MDAnalysis.analysis.dihedrals import Dihedral, Ramachandran, Janin from MDAnalysis.analysis.bat import BAT from MDAnalysis.analysis.gnm import GNMAnalysis +from MDAnalysis.analysis.hydrogenbonds.hbond_analysis import ( + HydrogenBondAnalysis, +) from MDAnalysis.lib.util import is_installed + def params_for_cls(cls, exclude: list[str] = None): """ This part contains fixtures for simultaneous testing @@ -122,3 +126,9 @@ def client_GNMAnalysis(request): @pytest.fixture(scope='module', params=params_for_cls(BAT)) def client_BAT(request): return request.param + +# MDAnalysis.analysis.hydrogenbonds + +@pytest.fixture(scope='module', params=params_for_cls(HydrogenBondAnalysis)) +def client_HydrogenBondAnalysis(request): + return request.param \ No newline at end of file diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py index b8ea644fc4a..503560a648f 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py @@ -53,9 +53,9 @@ def universe(): } @pytest.fixture(scope='class') - def h(self, universe): + def h(self, universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis(universe, **self.kwargs) - h.run() + h.run(**client_HydrogenBondAnalysis) return h def test_hbond_analysis(self, h): @@ -181,12 +181,12 @@ def universe(): @staticmethod @pytest.fixture(scope='class') - def hydrogen_bonds(universe): + def hydrogen_bonds(universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis( universe, **TestHydrogenBondAnalysisIdeal.kwargs ) - h.run() + h.run(**client_HydrogenBondAnalysis) return h def test_count_by_type(self, hydrogen_bonds): @@ -208,9 +208,12 @@ def test_no_bond_info_exception(self, universe): 'd_h_a_angle_cutoff': 120.0 } + u = universe.copy() + n_residues = 2 + u.add_TopologyAttr('mass', [15.999, 1.008, 1.008] * n_residues) + u.add_TopologyAttr('charge', [-1.04, 0.52, 0.52] * n_residues) with pytest.raises(NoDataError, match="no bond information"): - h = HydrogenBondAnalysis(universe, **kwargs) - h._get_dh_pairs() + h = HydrogenBondAnalysis(u, **kwargs) def test_no_bond_donor_sel(self, universe): @@ -263,10 +266,11 @@ def test_no_attr_hbonds(self, universe): with pytest.raises(NoDataError, match=".hbonds attribute is None"): hbonds.lifetime(tau_max=2, intermittency=1) - def test_logging_step_not_1(self, universe, caplog): + def test_logging_step_not_1(self, universe, caplog, + client_HydrogenBondAnalysis): hbonds = HydrogenBondAnalysis(universe, **self.kwargs) # using step 2 - hbonds.run(step=2) + hbonds.run(**client_HydrogenBondAnalysis, step=2) caplog.set_level(logging.WARNING) hbonds.lifetime(tau_max=2, intermittency=1) @@ -342,12 +346,12 @@ def universe(): @staticmethod @pytest.fixture(scope='class') - def hydrogen_bonds(universe): + def hydrogen_bonds(universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis( universe, **TestHydrogenBondAnalysisNoRes.kwargs ) - h.run() + h.run(**client_HydrogenBondAnalysis) return h def test_no_hydrogen_bonds(self, universe): @@ -441,10 +445,10 @@ def universe(): return u - def test_between_all(self, universe): + def test_between_all(self, universe, client_HydrogenBondAnalysis): # don't specify groups between which to find hydrogen bonds hbonds = HydrogenBondAnalysis(universe, between=None, **self.kwargs) - hbonds.run() + hbonds.run(**client_HydrogenBondAnalysis) # indices of [donor, hydrogen, acceptor] for each hydrogen bond expected_hbond_indices = [ @@ -457,14 +461,14 @@ def test_between_all(self, universe): expected_hbond_indices) assert_allclose(hbonds.results.hbonds[:, 4], expected_hbond_distances) - def test_between_PW(self, universe): + def test_between_PW(self, universe, client_HydrogenBondAnalysis): # Find only protein-water hydrogen bonds hbonds = HydrogenBondAnalysis( universe, between=["resname PROT", "resname SOL"], **self.kwargs ) - hbonds.run() + hbonds.run(**client_HydrogenBondAnalysis) # indices of [donor, hydrogen, acceptor] for each hydrogen bond expected_hbond_indices = [ @@ -475,7 +479,7 @@ def test_between_PW(self, universe): expected_hbond_indices) assert_allclose(hbonds.results.hbonds[:, 4], expected_hbond_distances) - def test_between_PW_PP(self, universe): + def test_between_PW_PP(self, universe, client_HydrogenBondAnalysis): # Find protein-water and protein-protein hydrogen bonds (not # water-water) hbonds = HydrogenBondAnalysis( @@ -486,7 +490,7 @@ def test_between_PW_PP(self, universe): ], **self.kwargs ) - hbonds.run() + hbonds.run(**client_HydrogenBondAnalysis) # indices of [donor, hydrogen, acceptor] for each hydrogen bond expected_hbond_indices = [ @@ -512,7 +516,7 @@ class TestHydrogenBondAnalysisTIP3P_GuessAcceptors_GuessHydrogens_UseTopology_(T 'd_h_a_angle_cutoff': 120.0 } - def test_no_hydrogens(self, universe): + def test_no_hydrogens(self, universe, client_HydrogenBondAnalysis): # If no hydrogens are identified at a given frame, check an # empty donor atom group is created test_kwargs = TestHydrogenBondAnalysisTIP3P.kwargs.copy() @@ -520,7 +524,7 @@ def test_no_hydrogens(self, universe): test_kwargs['hydrogens_sel'] = "name H" # no atoms have name H h = HydrogenBondAnalysis(universe, **test_kwargs) - h.run() + h.run(**client_HydrogenBondAnalysis) assert h._hydrogens.n_atoms == 0 assert h._donors.n_atoms == 0 @@ -629,9 +633,9 @@ def universe(): } @pytest.fixture(scope='class') - def h(self, universe): + def h(self, universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis(universe, **self.kwargs) - h.run(start=1, step=2) + h.run(**client_HydrogenBondAnalysis, start=1, step=2) return h def test_hbond_analysis(self, h): @@ -690,11 +694,11 @@ def test_empty_sel(self, universe, seltype): with pytest.warns(UserWarning, match=self.msg.format(seltype)): HydrogenBondAnalysis(universe, **sel_kwarg) - def test_hbond_analysis(self, universe): + def test_hbond_analysis(self, universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis(universe, donors_sel=' ', hydrogens_sel=' ', acceptors_sel=' ') - h.run() + h.run(**client_HydrogenBondAnalysis) assert h.donors_sel == '' assert h.hydrogens_sel == '' From 740e74e8c61ea01a4b2120bd369b11a58cb9c304 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:07:07 +0200 Subject: [PATCH 17/57] Implementation of parallelization to `MDAnalysis.analysis.dssp` (#4721) - Fixes #4674 - Parallelization of the backend support to the class DSSP in dssp.py - Addition of parallelization tests in test_dssp.py and fixtures in conftest.py - Updated Changelog --------- Co-authored-by: Yuxuan Zhuang --- package/CHANGELOG | 3 +- package/MDAnalysis/analysis/dssp/dssp.py | 29 ++++++++++++++--- .../MDAnalysisTests/analysis/conftest.py | 11 ++++++- .../MDAnalysisTests/analysis/test_dssp.py | 31 ++++++++++--------- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index ec5fe28ecad..b284ffddeec 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -62,7 +62,8 @@ Enhancements * explicitly mark `analysis.pca.PCA` as not parallelizable (Issue #4680) * enables parallelization for analysis.bat.BAT (Issue #4663) * enable parallelization for analysis.dihedrals.{Dihedral,Ramachandran,Janin} - (Issue #4673) + (Issue #4673) + * enables parallelization for analysis.dssp.dssp.DSSP (Issue #4674) * Enables parallelization for analysis.hydrogenbonds.hbond_analysis.HydrogenBondAnalysis (Issue #4664) * 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) diff --git a/package/MDAnalysis/analysis/dssp/dssp.py b/package/MDAnalysis/analysis/dssp/dssp.py index d2eccf09473..21dd9423e49 100644 --- a/package/MDAnalysis/analysis/dssp/dssp.py +++ b/package/MDAnalysis/analysis/dssp/dssp.py @@ -148,7 +148,7 @@ import numpy as np from MDAnalysis import Universe, AtomGroup -from ..base import AnalysisBase +from ..base import AnalysisBase, ResultsGroup from ...due import due, Doi due.cite( @@ -196,7 +196,7 @@ class DSSP(AnalysisBase): .. Warning:: For DSSP to work properly, your atoms must represent a protein. The hydrogen atom bound to the backbone nitrogen atom is matched by name - as given by the keyword argument `hydrogen_atom`. There may only be + as given by the keyword argument `hydrogen_atom`. There may only be a single backbone nitrogen hydrogen atom per residue; the one exception is proline, for which there should not exist any such hydrogens. The default value of `hydrogen_atom` should handle the common naming @@ -229,8 +229,8 @@ class DSSP(AnalysisBase): (except proline), namely the one bound to the backbone nitrogen. .. Note:: - To work with different hydrogen-naming conventions by default, the - default selection is broad but if hydrogens are incorrectly selected + To work with different hydrogen-naming conventions by default, the + default selection is broad but if hydrogens are incorrectly selected (e.g., a :exc:`ValueError` is raised) you must customize `hydrogen_name` for your specific case. @@ -263,7 +263,7 @@ class DSSP(AnalysisBase): The :attr:`results.dssp_ndarray` attribute holds a ``(n_frames, n_residues, 3)`` shape ndarray with a *one-hot encoding* of *loop* '-' (index 0), *helix* 'H' (index 1), and *sheet* 'E' - (index 2), respectively for each frame of the trajectory. It can be + (index 2), respectively for each frame of the trajectory. It can be used to compute, for instance, the **average secondary structure**: >>> from MDAnalysis.analysis.dssp import translate, DSSP @@ -276,8 +276,23 @@ class DSSP(AnalysisBase): .. versionadded:: 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, atoms: Union[Universe, AtomGroup], @@ -382,6 +397,10 @@ def _conclude(self): self.results.dssp_ndarray = np.array(self.results.dssp_ndarray) self.results.resids = self._heavy_atoms["CA"].resids + def _get_aggregator(self): + return ResultsGroup( + lookup={"dssp_ndarray": ResultsGroup.flatten_sequence}, + ) def translate(onehot: np.ndarray) -> np.ndarray: """Translate a one-hot encoding summary into char-based secondary structure diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index b5fe975dcd8..fc3c8a480c7 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -10,6 +10,7 @@ from MDAnalysis.analysis.dihedrals import Dihedral, Ramachandran, Janin from MDAnalysis.analysis.bat import BAT from MDAnalysis.analysis.gnm import GNMAnalysis +from MDAnalysis.analysis.dssp.dssp import DSSP from MDAnalysis.analysis.hydrogenbonds.hbond_analysis import ( HydrogenBondAnalysis, ) @@ -127,8 +128,16 @@ def client_GNMAnalysis(request): def client_BAT(request): return request.param + +# MDAnalysis.analysis.dssp.dssp + +@pytest.fixture(scope="module", params=params_for_cls(DSSP)) +def client_DSSP(request): + return request.param + + # MDAnalysis.analysis.hydrogenbonds @pytest.fixture(scope='module', params=params_for_cls(HydrogenBondAnalysis)) def client_HydrogenBondAnalysis(request): - return request.param \ No newline at end of file + return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_dssp.py b/testsuite/MDAnalysisTests/analysis/test_dssp.py index 403743d9728..f7a1e118931 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dssp.py +++ b/testsuite/MDAnalysisTests/analysis/test_dssp.py @@ -10,31 +10,31 @@ # Files that match glob pattern '????.pdb.gz' and matching '????.pdb.dssp' files, # containing the secondary structure assignment string, will be tested automatically. @pytest.mark.parametrize("pdb_filename", glob.glob(f"{DSSP_FOLDER}/?????.pdb.gz")) -def test_file_guess_hydrogens(pdb_filename): +def test_file_guess_hydrogens(pdb_filename, client_DSSP): u = mda.Universe(pdb_filename) with open(f"{pdb_filename.rstrip('.gz')}.dssp", "r") as fin: correct_answ = fin.read().strip().split()[0] - run = DSSP(u, guess_hydrogens=True).run() + run = DSSP(u, guess_hydrogens=True).run(**client_DSSP) answ = "".join(run.results.dssp[0]) assert answ == correct_answ -def test_trajectory(): +def test_trajectory(client_DSSP): u = mda.Universe(TPR, XTC).select_atoms("protein").universe - run = DSSP(u).run(stop=10) + run = DSSP(u).run(**client_DSSP, stop=10) first_frame = "".join(run.results.dssp[0]) last_frame = "".join(run.results.dssp[-1]) avg_frame = "".join(translate(run.results.dssp_ndarray.mean(axis=0))) assert first_frame[:10] != last_frame[:10] == avg_frame[:10] == "-EEEEEE---" protein = mda.Universe(TPR, XTC).select_atoms("protein") - run = DSSP(protein).run(stop=10) + run = DSSP(protein).run(**client_DSSP, stop=10) -def test_atomgroup(): +def test_atomgroup(client_DSSP): protein = mda.Universe(TPR, XTC).select_atoms("protein") - run = DSSP(protein).run(stop=10) + run = DSSP(protein).run(**client_DSSP, stop=10) first_frame = "".join(run.results.dssp[0]) last_frame = "".join(run.results.dssp[-1]) avg_frame = "".join(translate(run.results.dssp_ndarray.mean(axis=0))) @@ -42,9 +42,9 @@ def test_atomgroup(): assert first_frame[:10] != last_frame[:10] == avg_frame[:10] == "-EEEEEE---" -def test_trajectory_with_hydrogens(): +def test_trajectory_with_hydrogens(client_DSSP): u = mda.Universe(TPR, XTC).select_atoms("protein").universe - run = DSSP(u, guess_hydrogens=False).run(stop=10) + run = DSSP(u, guess_hydrogens=False).run(**client_DSSP, stop=10) first_frame = "".join(run.results.dssp[0]) last_frame = "".join(run.results.dssp[-1]) avg_frame = "".join(translate(run.results.dssp_ndarray.mean(axis=0))) @@ -53,28 +53,29 @@ def test_trajectory_with_hydrogens(): @pytest.mark.parametrize("pdb_filename", glob.glob(f"{DSSP_FOLDER}/2xdgA.pdb.gz")) -def test_trajectory_without_hydrogen_fails(pdb_filename): +def test_trajectory_without_hydrogen_fails(pdb_filename, client_DSSP): u = mda.Universe(pdb_filename) with pytest.raises(ValueError): - DSSP(u, guess_hydrogens=False).run() + DSSP(u, guess_hydrogens=False).run(**client_DSSP) @pytest.mark.parametrize( "pdb_filename", glob.glob(f"{DSSP_FOLDER}/1mr1D_failing.pdb.gz") ) -def test_trajectory_with_uneven_number_of_atoms_fails(pdb_filename): +def test_trajectory_with_uneven_number_of_atoms_fails(pdb_filename, + client_DSSP): u = mda.Universe(pdb_filename) with pytest.raises(ValueError): - DSSP(u, guess_hydrogens=True).run() + DSSP(u, guess_hydrogens=True).run(**client_DSSP) @pytest.mark.parametrize( "pdb_filename", glob.glob(f"{DSSP_FOLDER}/wrong_hydrogens.pdb.gz") ) -def test_exception_raises_with_atom_index(pdb_filename): +def test_exception_raises_with_atom_index(pdb_filename, client_DSSP): u = mda.Universe(pdb_filename) with pytest.raises( ValueError, match="Residue contains*", ): - DSSP(u, guess_hydrogens=False).run() + DSSP(u, guess_hydrogens=False).run(**client_DSSP) From 599c8db9efcd4cd372792a2bcae3cc555f9e2072 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sun, 13 Oct 2024 23:26:00 +0100 Subject: [PATCH 18/57] Add py3.13 support w/ minimal deps (#4732) * Add Python 3.13 to minimal CI testing, and include some minor test shims to accommodate. --- .github/workflows/deploy.yaml | 4 ++-- .github/workflows/gh-ci-cron.yaml | 4 +--- .github/workflows/gh-ci.yaml | 5 +++++ azure-pipelines.yml | 2 +- package/CHANGELOG | 1 + package/pyproject.toml | 3 ++- testsuite/MDAnalysisTests/analysis/test_base.py | 8 ++++---- testsuite/MDAnalysisTests/lib/test_log.py | 4 ++-- 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 09a79a64c20..5db1a4f5c16 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -41,7 +41,7 @@ jobs: - [macos-12, macosx_*, x86_64] - [windows-2019, win_amd64, AMD64] - [macos-14, macosx_*, arm64] - python: ["cp310", "cp311", "cp312"] + python: ["cp310", "cp311", "cp312", "cp313"] defaults: run: working-directory: ./package @@ -243,7 +243,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] type: ["FULL", "MIN"] exclude: # Multiple deps don't like windows diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 0df5ad460f3..072d1cecb51 100644 --- a/.github/workflows/gh-ci-cron.yaml +++ b/.github/workflows/gh-ci-cron.yaml @@ -41,8 +41,6 @@ jobs: os-type: "ubuntu" - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} # minimally install nightly wheels & core deps - name: nightly_wheels @@ -197,7 +195,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index 8a45aec650f..c07535e2f78 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -32,6 +32,11 @@ jobs: full-deps: [true, ] codecov: [true, ] include: + - name: python_313 + os: ubuntu-latest + python-version: "3.13" + full-deps: false + codecov: true - name: macOS_monterey_py311 os: macOS-12 python-version: "3.12" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 250f3ba63b5..27b1c0db3f9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -47,7 +47,7 @@ jobs: PYTHON_VERSION: '3.12' PYTHON_ARCH: 'x64' BUILD_TYPE: 'wheel' - NUMPY_MIN: '1.26.0' + NUMPY_MIN: '2.1.0' imageName: 'ubuntu-latest' Linux-Python310-64bit-full-wheel: PYTHON_VERSION: '3.10' diff --git a/package/CHANGELOG b/package/CHANGELOG index b284ffddeec..de7fddd2660 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -56,6 +56,7 @@ Fixes * Fix groups.py doctests using sphinx directives (Issue #3925, PR #4374) Enhancements + * MDAnalysis now supports Python 3.13 (PR #4732) * Introduce parallelization API to `AnalysisBase` and to `analysis.rms.RMSD` class (Issue #4158, PR #4304) * Enables parallelization for analysis.gnm.GNMAnalysis (Issue #4672) diff --git a/package/pyproject.toml b/package/pyproject.toml index 3d1ea04bddb..24d726c3ab4 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -61,6 +61,7 @@ classifiers = [ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Programming Language :: C', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Bio-Informatics', @@ -124,6 +125,6 @@ MDAnalysis = [ [tool.black] line-length = 79 -target-version = ['py310', 'py311', 'py312'] +target-version = ['py310', 'py311', 'py312', 'py313'] extend-exclude = '.' required-version = '24' diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 623b2ca5fe1..345e0dc671e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -345,17 +345,17 @@ def test_verbose_progressbar(u, capsys): def test_verbose_progressbar_run(u, capsys): FrameAnalysis(u.trajectory).run(verbose=True) _, err = capsys.readouterr() - expected = u'100%|██████████| 98/98 [00:00<00:00, 8799.49it/s]' + expected = u'100%|██████████' actual = err.strip().split('\r')[-1] - assert actual[:24] == expected[:24] + assert actual[:15] == expected def test_verbose_progressbar_run_with_kwargs(u, capsys): FrameAnalysis(u.trajectory).run( verbose=True, progressbar_kwargs={'desc': 'custom'}) _, err = capsys.readouterr() - expected = u'custom: 100%|██████████| 98/98 [00:00<00:00, 8799.49it/s]' + expected = u'custom: 100%|██████████' actual = err.strip().split('\r')[-1] - assert actual[:30] == expected[:30] + assert actual[:23] == expected def test_progressbar_multiprocessing(u): diff --git a/testsuite/MDAnalysisTests/lib/test_log.py b/testsuite/MDAnalysisTests/lib/test_log.py index c0f413ba2d2..acaa876df92 100644 --- a/testsuite/MDAnalysisTests/lib/test_log.py +++ b/testsuite/MDAnalysisTests/lib/test_log.py @@ -32,9 +32,9 @@ def test_output(self, capsys): for i in ProgressBar(list(range(10))): pass out, err = capsys.readouterr() - expected = u'100%|██████████| 10/10 [00:00<00:00, 583.67it/s]' + expected = u'100%|██████████' actual = err.strip().split('\r')[-1] - assert actual[:24] == expected[:24] + assert actual[:15] == expected def test_disable(self, capsys): for i in ProgressBar(list(range(10)), disable=True): From 571431a169e5881f7d09930c4d7e633df0f2cd40 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Mon, 14 Oct 2024 21:12:14 +0200 Subject: [PATCH 19/57] liners black config ignore formatting git blame --- .git-blame-ignore-revs | 2 + .github/workflows/darkerbot.yaml | 62 ------- .github/workflows/linters.yaml | 71 ++------ maintainer/ci/darker-outcomes.py | 238 -------------------------- package/MDAnalysis/due.py | 21 ++- package/MDAnalysis/topology/tables.py | 44 +++-- package/pyproject.toml | 7 +- 7 files changed, 58 insertions(+), 387 deletions(-) create mode 100644 .git-blame-ignore-revs delete mode 100644 .github/workflows/darkerbot.yaml delete mode 100644 maintainer/ci/darker-outcomes.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..1a65ab79f56 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# black +feca12100a2d2251fa2b4e4ad9598d9d6e00ba38 diff --git a/.github/workflows/darkerbot.yaml b/.github/workflows/darkerbot.yaml deleted file mode 100644 index 88d0a085159..00000000000 --- a/.github/workflows/darkerbot.yaml +++ /dev/null @@ -1,62 +0,0 @@ -name: Darker PR Bot - -on: - workflow_run: - workflows: [linters] - types: - - completed - - -concurrency: - # Probably overly cautious group naming. - # Commits to develop will cancel each other, but PRs will only cancel - # commits within the same PR - group: "${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }}" - cancel-in-progress: true - - -jobs: - darker_bot: - if: "github.repository == 'MDAnalysis/mdanalysis'" - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: setup_dependencies - run: | - pip install PyGithub - - - name: 'Download artifact' - uses: actions/github-script@v7 - with: - script: | - let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }); - let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "darkerlint" - })[0]; - let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - let fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/darker_results.zip`, Buffer.from(download.data)); - - - name: 'Unzip artifact' - run: unzip darker_results.zip - - - name: writer-errors - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - python maintainer/ci/darker-outcomes.py --json status.json diff --git a/.github/workflows/linters.yaml b/.github/workflows/linters.yaml index de5eb68fa86..ebc6225036c 100644 --- a/.github/workflows/linters.yaml +++ b/.github/workflows/linters.yaml @@ -16,12 +16,10 @@ defaults: shell: bash -l {0} jobs: - darker_lint: + black: if: "github.repository == 'MDAnalysis/mdanalysis'" runs-on: ubuntu-latest timeout-minutes: 10 - permissions: - pull-requests: write defaults: run: shell: bash @@ -35,66 +33,17 @@ jobs: with: python-version: "3.10" - - name: darker-main-code - id: darker-main-code - uses: akaihola/darker@v2.1.1 - continue-on-error: true + - uses: psf/black@stable with: - version: "~=1.6.1" - options: "--check --diff --color" - src: "./package/MDAnalysis" - revision: "HEAD^" - lint: "flake8" - - - name: darker-test-code - id: darker-test-code - uses: akaihola/darker@v2.1.1 - continue-on-error: true + options: "--check --verbose" + src: "./package" + version: "~= 24.0" + + - uses: psf/black@stable with: - version: "~=1.6.1" - options: "--check --diff --color" - src: "./testsuite/MDAnalysisTests" - revision: "HEAD^" - lint: "flake8" - - - name: get-pr-info - uses: actions/github-script@v7 - with: - script: - const prNumber = context.payload.number; - core.exportVariable('PULL_NUMBER', prNumber); - - - name: save-status - env: - MAIN: ${{ steps.darker-main-code.outcome }} - TEST: ${{ steps.darker-test-code.outcome }} - shell: python - run: | - import os - import json - from pathlib import Path - - Path('./darker_results/').mkdir(exist_ok=True) - - d = { - 'main_stat': os.environ['MAIN'], - 'test_stat': os.environ['TEST'], - 'PR_NUM': os.environ['PULL_NUMBER'], - 'RUN_ID': os.environ['GITHUB_RUN_ID'], - } - - with open('darker_results/status.json', 'w') as f: - json.dump(d, f) - - - name: check-json - run: cat darker_results/status.json - - - name: upload-status - uses: actions/upload-artifact@v4 - with: - name: darkerlint - path: darker_results/ - retention-days: 1 + options: "--check --verbose" + src: "./testsuite" + version: "~= 24.0" pylint_check: diff --git a/maintainer/ci/darker-outcomes.py b/maintainer/ci/darker-outcomes.py deleted file mode 100644 index 4d6bb7fc5ba..00000000000 --- a/maintainer/ci/darker-outcomes.py +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env python3 - -# MIT License - -# Copyright (c) 2023 Irfan Alibay - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import argparse -import os -from urllib import request -import json -from github import Github - - -parser = argparse.ArgumentParser( - description="Write PR comment for failed darker linting", -) - - -parser.add_argument( - "--json", - type=str, - help="Input JSON file with status results", -) - - -def get_pull_request(repo, pr_num): - """ - Simple method to get a PyGithub PR object from a PR number - - Parameters - ---------- - repo : PyGithub.Repository - Github Repository API object. - pr_num : int - Pull request number. - - Returns - ------- - PyGithub.PullRequest - Pull Request object corresponding to the input PR number. - """ - # Can get PR directly from PR number - return repo.get_pull(pr_num) - - -def get_action_url(repo, pr, run_id, workflow_name, job_name): - """ - Mix PyGithub & Github V3 REST API method to extract the url path to a - github actions workflow job corresponding to a given GITHUB_RUN_ID. - - Parameters - ---------- - repo : PyGithub.Repository - Github Repository API object. - run_id : str - Github actions RUN ID as defined by the github actions environment - variable `GITHUB_RUN_ID`. - workflow_name : str - Name of the workflow to extract a job url for. - job_name : str - Name of the job within the workflow to extract a url for. - - - Returns - ------- - str - URL to github actions workflow job or 'N/A' if a corresponding job name - could not be found. - """ - # Accessing get_workflow directly currently fails when passing a name - # Instead do the roundabout way by getting list of all workflows - linters = [wf for wf in repo.get_workflows() - if wf.name == workflow_name][0] - - # Extract the gh action run - run = [r for r in linters.get_runs(branch=pr.head.ref) - if r.id == int(run_id)][0] - - # The exact job url can't be recovered via the Python API - # Switch over to using the REST API instead - with request.urlopen(run.jobs_url) as url: - data = json.load(url) - - for job in data['jobs']: - if job['name'] == job_name: - return job['html_url'] - - return 'N/A' - - -def bool_outcome(outcome): - """ - Converts github action job status outcome to bool. - - Parameters - ---------- - outcome : str - Github action job step outcome message. - - Returns - ------- - bool - Whether or not the job step was successful. - """ - return True if (outcome == 'success') else False - - -def gen_message(pr, main_stat, test_stat, action_url): - """ - Generate a user facing message on the status of the darker linting - action. - - Parameters - ---------- - pr : PyGithub.PullRequest - Pull Request object representing the target for this message. - main_stat : str - Outcome of darker linting of main package code. - test_stat : str - Outcome of darker linting of testsuite code. - action_url : str - URL pointing to darker linting job log. - - - Returns - ------- - str - Message to be posted to PR author. - """ - - def _format_outcome(stat): - if bool_outcome(stat): - return "✅ Passed" - else: - return "⚠️ Possible failure" - - msg = ('### Linter Bot Results:\n\n' - f'Hi @{pr.user.login}! Thanks for making this PR. ' - 'We linted your code and found the following: \n\n') - - # If everything is ok - if bool_outcome(main_stat) and bool_outcome(test_stat): - msg += ('There are currently no issues detected! 🎉') - else: - msg += ('Some issues were found with the formatting of your code.\n' - '| Code Location | Outcome |\n' - '| --- | --- |\n' - f'| main package | {_format_outcome(main_stat)}|\n' - f'| testsuite | {_format_outcome(test_stat)}|\n' - '\nPlease have a look at the `darker-main-code` and ' - '`darker-test-code` steps here for more details: ' - f'{action_url}\n\n' - '---\n' - '_**Please note:** The `black` linter is purely ' - 'informational, you can safely ignore these outcomes if ' - 'there are no flake8 failures!_') - return msg - - -def post_comment(pr, message, match_string): - """ - Post a comment in a Pull Request. - - If a comment with text matching `match_string` is found in the - Pull Request, the comment will be edited. - - Parameters - ---------- - pr : PyGithub.PullRequest - Pull Request object representing the target for this message. - message : str - The message to post as a comment. - match_string : str - A matching string to recognise if the comment already exists. - """ - # Extract a list of matching comments from PR - comments = [comm for comm in pr.get_issue_comments() if match_string in comm.body] - - if len(comments) > 0: - # Edit the comment in-place - # By default we assume that the bot can write faster than anyone else - comments[0].edit(body=message) - else: - # If the comment doesn't exist, create one - pr.create_issue_comment(message) - - -if __name__ == "__main__": - args = parser.parse_args() - - git = Github(os.environ['GITHUB_TOKEN']) - repo = git.get_repo("MDAnalysis/mdanalysis") - - with open(args.json, 'r') as f: - status = json.load(f) - - run_id = status['RUN_ID'] - print(f"debug run_id: {run_id}") - - # Get Pull Request - pr_num = int(status['PR_NUM']) - print(f"debug pr_num: {pr_num}") - pr = get_pull_request(repo, pr_num) - - # Get the url to the github action job being pointed to - action_url = get_action_url(repo, pr, run_id, - workflow_name='linters', - job_name='darker_lint') - - # Get the message you want to post to users - with open(args.json, 'r') as f: - results_dict = json.load(f) - - message = gen_message(pr, - status['main_stat'], - status['test_stat'], - action_url) - - # Post your comment - post_comment(pr, message, match_string='Linter Bot Results:') diff --git a/package/MDAnalysis/due.py b/package/MDAnalysis/due.py index 0528bb83e15..7ab1e8b55a9 100644 --- a/package/MDAnalysis/due.py +++ b/package/MDAnalysis/due.py @@ -26,30 +26,33 @@ """ -__version__ = '0.0.5' +__version__ = "0.0.5" class InactiveDueCreditCollector(object): """Just a stub at the Collector which would not do anything""" + def _donothing(self, *args, **kwargs): """Perform no good and no bad""" - pass # pylint: disable=unnecessary-pass + pass # pylint: disable=unnecessary-pass def dcite(self, *args, **kwargs): """If I could cite I would""" + def nondecorating_decorator(func): return func + return nondecorating_decorator cite = load = add = _donothing def __repr__(self): - return self.__class__.__name__ + '()' + return self.__class__.__name__ + "()" def _donothing_func(*args, **kwargs): """Perform no good and no bad""" - pass # pylint: disable=unnecessary-pass + pass # pylint: disable=unnecessary-pass try: @@ -60,17 +63,21 @@ def _donothing_func(*args, **kwargs): import duecredit from duecredit import due, BibTeX, Doi, Url - if 'due' in locals() and not hasattr(due, 'cite'): + + if "due" in locals() and not hasattr(due, "cite"): raise RuntimeError( - "Imported due lacks .cite. DueCredit is now disabled") + "Imported due lacks .cite. DueCredit is now disabled" + ) except Exception as err: if not isinstance(err, ImportError): import logging import warnings + errmsg = "Failed to import duecredit due to {}".format(str(err)) warnings.warn(errmsg) logging.getLogger("duecredit").error( - "Failed to import duecredit due to {}".format(str(err))) + "Failed to import duecredit due to {}".format(str(err)) + ) # else: # Do not issue any warnings if duecredit is not installed; # this is the user's choice (Issue #1872) diff --git a/package/MDAnalysis/topology/tables.py b/package/MDAnalysis/topology/tables.py index 1c0b51b5817..ea46421cc42 100644 --- a/package/MDAnalysis/topology/tables.py +++ b/package/MDAnalysis/topology/tables.py @@ -46,8 +46,10 @@ .. autodata:: TABLE_VDWRADII """ +from typing import Any -def kv2dict(s, convertor=str): + +def kv2dict(s, convertor: Any = str): """Primitive ad-hoc parser of a key-value record list. * The string *s* should contain each key-value pair on a separate @@ -172,10 +174,12 @@ def kv2dict(s, convertor=str): #: with :func:`MDAnalysis.topology.core.guess_atom_type`. atomelements = kv2dict(TABLE_ATOMELEMENTS) +# fmt: off elements = ['H', 'LI', 'BE', 'B', 'C', 'N', 'O', 'F', 'NA', 'MG', 'AL', 'P', 'SI', 'S', 'CL', 'K'] +# fmt: on #: Plain-text table with atomic masses in u. TABLE_MASSES = """ @@ -376,28 +380,31 @@ def kv2dict(s, convertor=str): #: .. SeeAlso:: :func:`MDAnalysis.topology.core.guess_bonds` vdwradii = kv2dict(TABLE_VDWRADII, convertor=float) -Z2SYMB = {1: 'H', 2: 'He', - 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 7: 'N', 8: 'O', 9: 'F', 10: 'Ne', - 11: 'Na', 12: 'Mg', 13: 'Al', 14: 'Si', 15: 'P', 16: 'S', 17: 'Cl', 18: 'Ar', - 19: 'K', 20: 'Ca', 21: 'Sc', 22: 'Ti', 23: 'V', 24: 'Cr', 25: 'Mn', 26: 'Fe', - 27: 'Co', 28: 'Ni', 29: 'Cu', 30: 'Zn', 31: 'Ga', 32: 'Ge', 33: 'As', 34: 'Se', - 35: 'Br', 36: 'Kr', 37: 'Rb', 38: 'Sr', 39: 'Y', 40: 'Zr', 41: 'Nb', 42: 'Mo', - 43: 'Tc', 44: 'Ru', 45: 'Rh', 46: 'Pd', 47: 'Ag', 48: 'Cd', 49: 'In', 50: 'Sn', - 51: 'Sb', 52: 'Te', 53: 'I', 54: 'Xe', 55: 'Cs', 56: 'Ba', 57: 'La', 58: 'Ce', - 59: 'Pr', 60: 'Nd', 61: 'Pm', 62: 'Sm', 63: 'Eu', 64: 'Gd', 65: 'Tb', 66: 'Dy', - 67: 'Ho', 68: 'Er', 69: 'Tm', 70: 'Yb', 71: 'Lu', 72: 'Hf', 73: 'Ta', 74: 'W', - 75: 'Re', 76: 'Os', 77: 'Ir', 78: 'Pt', 79: 'Au', 80: 'Hg', 81: 'Tl', 82: 'Pb', - 83: 'Bi', 84: 'Po', 85: 'At', 86: 'Rn', 87: 'Fr', 88: 'Ra', 89: 'Ac', 90: 'Th', - 91: 'Pa', 92: 'U', 93: 'Np', 94: 'Pu', 95: 'Am', 96: 'Cm', 97: 'Bk', 98: 'Cf', - 99: 'Es', 100: 'Fm', 101: 'Md', 102: 'No', 103: 'Lr', 104: 'Rf', 105: 'Db', - 106: 'Sg', 107: 'Bh', 108: 'Hs', 109: 'Mt', 110: 'Ds', 111: 'Rg', 112: 'Cn', +# fmt: off +Z2SYMB = {1: 'H', 2: 'He', + 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 7: 'N', 8: 'O', 9: 'F', 10: 'Ne', + 11: 'Na', 12: 'Mg', 13: 'Al', 14: 'Si', 15: 'P', 16: 'S', 17: 'Cl', 18: 'Ar', + 19: 'K', 20: 'Ca', 21: 'Sc', 22: 'Ti', 23: 'V', 24: 'Cr', 25: 'Mn', 26: 'Fe', + 27: 'Co', 28: 'Ni', 29: 'Cu', 30: 'Zn', 31: 'Ga', 32: 'Ge', 33: 'As', 34: 'Se', + 35: 'Br', 36: 'Kr', 37: 'Rb', 38: 'Sr', 39: 'Y', 40: 'Zr', 41: 'Nb', 42: 'Mo', + 43: 'Tc', 44: 'Ru', 45: 'Rh', 46: 'Pd', 47: 'Ag', 48: 'Cd', 49: 'In', 50: 'Sn', + 51: 'Sb', 52: 'Te', 53: 'I', 54: 'Xe', 55: 'Cs', 56: 'Ba', 57: 'La', 58: 'Ce', + 59: 'Pr', 60: 'Nd', 61: 'Pm', 62: 'Sm', 63: 'Eu', 64: 'Gd', 65: 'Tb', 66: 'Dy', + 67: 'Ho', 68: 'Er', 69: 'Tm', 70: 'Yb', 71: 'Lu', 72: 'Hf', 73: 'Ta', 74: 'W', + 75: 'Re', 76: 'Os', 77: 'Ir', 78: 'Pt', 79: 'Au', 80: 'Hg', 81: 'Tl', 82: 'Pb', + 83: 'Bi', 84: 'Po', 85: 'At', 86: 'Rn', 87: 'Fr', 88: 'Ra', 89: 'Ac', 90: 'Th', + 91: 'Pa', 92: 'U', 93: 'Np', 94: 'Pu', 95: 'Am', 96: 'Cm', 97: 'Bk', 98: 'Cf', + 99: 'Es', 100: 'Fm', 101: 'Md', 102: 'No', 103: 'Lr', 104: 'Rf', 105: 'Db', + 106: 'Sg', 107: 'Bh', 108: 'Hs', 109: 'Mt', 110: 'Ds', 111: 'Rg', 112: 'Cn', 113: 'Nh', 114: 'Fl', 115: 'Mc', 116: 'Lv', 117: 'Ts', 118: 'Og'} +# fmt: on -SYMB2Z = {v:k for k, v in Z2SYMB.items()} +SYMB2Z = {v: k for k, v in Z2SYMB.items()} # Conversion between SYBYL atom types and corresponding elements # Tripos MOL2 file format: # https://web.archive.org/web/*/http://chemyang.ccnu.edu.cn/ccb/server/AIMMS/mol2.pdf +# fmt: off SYBYL2SYMB = { "H": "H", "H.spc": "H", "H.t3p": "H", "C.3": "C", "C.2": "C", "C.1": "C", "C.ar": "C", "C.cat": "C", @@ -428,4 +435,5 @@ def kv2dict(s, convertor=str): "Se": "Se", "Mo": "Mo", "Sn": "Sn", -} \ No newline at end of file +} +# fmt: on diff --git a/package/pyproject.toml b/package/pyproject.toml index 24d726c3ab4..cd7ec3a7806 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -126,5 +126,10 @@ MDAnalysis = [ [tool.black] line-length = 79 target-version = ['py310', 'py311', 'py312', 'py313'] -extend-exclude = '.' +include = ''' +( +tables\.py +| due\.py +) +''' required-version = '24' From 48e9e01f1e8a6f6555c3284789a10f2378cc18ba Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Mon, 14 Oct 2024 21:22:51 +0200 Subject: [PATCH 20/57] .git-blame-ignore-revs --- .git-blame-ignore-revs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 1a65ab79f56..d40a8a87bd9 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,2 @@ # black -feca12100a2d2251fa2b4e4ad9598d9d6e00ba38 +83e5f99051d86ca354b537be8854c40f9b6ce172 From caa04ec8583fb01d0aa967592240e01159c23617 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Fri, 18 Oct 2024 19:24:04 -0700 Subject: [PATCH 21/57] pin distopia<0.3.0 (#4740) - fix #4739 - pin distopia<0.3.0 - temporarily restrict distopia to >=0.2.0,<0.3.0 until MDAnalysis has caught up with distopia API changes - updated CHANGELOG - version check distopia - only enable HAS_DISTOPIA if the appropriate version of distopia is installed - issue RuntimeWarning if incorrect version present - added test --- .github/actions/setup-deps/action.yaml | 2 +- package/CHANGELOG | 1 + package/MDAnalysis/lib/_distopia.py | 15 ++++++- .../MDAnalysisTests/lib/test_distances.py | 42 +++++++++++++++++-- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-deps/action.yaml b/.github/actions/setup-deps/action.yaml index 97112b09159..cbfd91df7e1 100644 --- a/.github/actions/setup-deps/action.yaml +++ b/.github/actions/setup-deps/action.yaml @@ -59,7 +59,7 @@ inputs: dask: default: 'dask' distopia: - default: 'distopia>=0.2.0' + default: 'distopia>=0.2.0,<0.3.0' h5py: default: 'h5py>=2.10' hole2: diff --git a/package/CHANGELOG b/package/CHANGELOG index de7fddd2660..37c60672327 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -78,6 +78,7 @@ Enhancements DOI 10.1021/acs.jpcb.7b11988. (Issue #2039, PR #4524) Changes + * only use distopia < 0.3.0 due to API changes (Issue #4739) * The `fetch_mmtf` method has been removed as the REST API service for MMTF files has ceased to exist (Issue #4634) * MDAnalysis now builds against numpy 2.0 rather than the diff --git a/package/MDAnalysis/lib/_distopia.py b/package/MDAnalysis/lib/_distopia.py index 7170cf2a556..5344393fe14 100644 --- a/package/MDAnalysis/lib/_distopia.py +++ b/package/MDAnalysis/lib/_distopia.py @@ -27,6 +27,7 @@ This module is a stub to provide distopia distance functions to `distances.py` as a selectable backend. """ +import warnings # check for distopia try: @@ -36,10 +37,22 @@ else: HAS_DISTOPIA = True + # check for compatibility: currently needs to be >=0.2.0,<0.3.0 (issue + # #4740) No distopia.__version__ available so we have to do some probing. + needed_funcs = ['calc_bonds_no_box_float', 'calc_bonds_ortho_float'] + has_distopia_020 = all([hasattr(distopia, func) for func in needed_funcs]) + if not has_distopia_020: + warnings.warn("Install 'distopia>=0.2.0,<0.3.0' to be used with this " + "release of MDAnalysis. Your installed version of " + "distopia >=0.3.0 will NOT be used.", + category=RuntimeWarning) + del distopia + HAS_DISTOPIA = False + + from .c_distances import ( calc_bond_distance_triclinic as _calc_bond_distance_triclinic_serial, ) -import warnings import numpy as np diff --git a/testsuite/MDAnalysisTests/lib/test_distances.py b/testsuite/MDAnalysisTests/lib/test_distances.py index 3fe4b2852b8..4f7cd238bab 100644 --- a/testsuite/MDAnalysisTests/lib/test_distances.py +++ b/testsuite/MDAnalysisTests/lib/test_distances.py @@ -20,6 +20,8 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # +import sys +from unittest.mock import Mock, patch import pytest import numpy as np from numpy.testing import assert_equal, assert_almost_equal, assert_allclose @@ -788,9 +790,10 @@ def test_pbc_wrong_wassenaar_distance(self, backend): # expected. assert np.linalg.norm(point_a - point_b) != dist[0, 0] -@pytest.mark.parametrize("box", + +@pytest.mark.parametrize("box", [ - None, + None, np.array([10., 15., 20., 90., 90., 90.]), # otrho np.array([10., 15., 20., 70.53571, 109.48542, 70.518196]), # TRIC ] @@ -835,6 +838,39 @@ def distopia_conditional_backend(): return ["serial", "openmp"] +def test_HAS_DISTOPIA_incompatible_distopia(): + # warn if distopia is the wrong version and set HAS_DISTOPIA to False + sys.modules.pop("distopia", None) + sys.modules.pop("MDAnalysis.lib._distopia", None) + + # fail any Attribute access for calc_bonds_ortho_float, + # calc_bonds_no_box_float but pretend to have the distopia + # 0.3.0 functions (from + # https://github.com/MDAnalysis/distopia/blob/main/distopia/__init__.py + # __all__): + mock_distopia_030 = Mock(spec=[ + 'calc_bonds_ortho', + 'calc_bonds_no_box', + 'calc_bonds_triclinic', + 'calc_angles_no_box', + 'calc_angles_ortho', + 'calc_angles_triclinic', + 'calc_dihedrals_no_box', + 'calc_dihedrals_ortho', + 'calc_dihedrals_triclinic', + 'calc_distance_array_no_box', + 'calc_distance_array_ortho', + 'calc_distance_array_triclinic', + 'calc_self_distance_array_no_box', + 'calc_self_distance_array_ortho', + 'calc_self_distance_array_triclinic', + ]) + with patch.dict("sys.modules", {"distopia": mock_distopia_030}): + with pytest.warns(RuntimeWarning, + match="Install 'distopia>=0.2.0,<0.3.0' to"): + import MDAnalysis.lib._distopia + assert not MDAnalysis.lib._distopia.HAS_DISTOPIA + 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 @@ -1597,7 +1633,7 @@ def test_empty_input_self_capped_distance(self, empty_coord, min_cut, box, assert_equal(res[1], np.empty((0,), dtype=np.float64)) else: assert_equal(res, np.empty((0, 2), dtype=np.int64)) - + @pytest.mark.parametrize('box', boxes[:2]) @pytest.mark.parametrize('backend', ['serial', 'openmp']) def test_empty_input_transform_RtoS(self, empty_coord, box, backend): From 05876f2d52c5b4ed92592eff9a9fdcd140920e45 Mon Sep 17 00:00:00 2001 From: Fiona Naughton Date: Sat, 19 Oct 2024 22:53:43 +1100 Subject: [PATCH 22/57] Deprecate encore (#4737) * Deprecate encore for removal in 3.0 --- package/CHANGELOG | 2 ++ package/MDAnalysis/analysis/encore/__init__.py | 8 ++++++++ package/MDAnalysis/analysis/encore/bootstrap.py | 5 +++++ .../analysis/encore/clustering/ClusterCollection.py | 5 +++++ .../analysis/encore/clustering/ClusteringMethod.py | 5 +++++ package/MDAnalysis/analysis/encore/clustering/cluster.py | 5 +++++ package/MDAnalysis/analysis/encore/confdistmatrix.py | 5 +++++ package/MDAnalysis/analysis/encore/covariance.py | 6 ++++++ .../DimensionalityReductionMethod.py | 5 +++++ .../dimensionality_reduction/reduce_dimensionality.py | 5 +++++ package/MDAnalysis/analysis/encore/similarity.py | 5 +++++ .../sphinx/source/documentation_pages/analysis/encore.rst | 6 ++++++ .../source/documentation_pages/analysis/encore/utils.rst | 5 +++++ testsuite/MDAnalysisTests/analysis/test_encore.py | 6 ++++++ 14 files changed, 73 insertions(+) diff --git a/package/CHANGELOG b/package/CHANGELOG index 37c60672327..1763df44ec3 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -100,6 +100,8 @@ Changes numpy.testing.assert_allclose #4438) Deprecations + * The MDAnalysis.anaylsis.encore module has been deprecated in favour of the + mdaencore MDAKit and will be removed in version 3.0.0 (PR #4737) * The MMTF Reader is deprecated and will be removed in version 3.0 as the MMTF format is no longer supported (Issue #4634) * The MDAnalysis.analysis.waterdynamics module has been deprecated in favour diff --git a/package/MDAnalysis/analysis/encore/__init__.py b/package/MDAnalysis/analysis/encore/__init__.py index 2017188580f..34b70dd28d0 100644 --- a/package/MDAnalysis/analysis/encore/__init__.py +++ b/package/MDAnalysis/analysis/encore/__init__.py @@ -34,6 +34,14 @@ __all__ = ['covariance', 'similarity', 'confdistmatrix', 'clustering'] +import warnings + +wmsg = ("Deprecation in version 2.8.0\n" + "MDAnalysis.analysis.encore is deprecated in favour of the " + "MDAKit mdaencore (https://www.mdanalysis.org/mdaencore/) " + "and will be removed in MDAnalysis version 3.0.0.") +warnings.warn(wmsg, category=DeprecationWarning) + from ...due import due, Doi due.cite(Doi("10.1371/journal.pcbi.1004415"), diff --git a/package/MDAnalysis/analysis/encore/bootstrap.py b/package/MDAnalysis/analysis/encore/bootstrap.py index 22287d98fc7..2d50d486dcb 100644 --- a/package/MDAnalysis/analysis/encore/bootstrap.py +++ b/package/MDAnalysis/analysis/encore/bootstrap.py @@ -32,6 +32,11 @@ .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ import numpy as np import logging diff --git a/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py b/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py index 35b48219abf..87879ba1077 100644 --- a/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py +++ b/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py @@ -31,6 +31,11 @@ .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ import numpy as np diff --git a/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py b/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py index b18d6a54350..df13aaff570 100644 --- a/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py +++ b/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py @@ -32,6 +32,11 @@ .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ import numpy as np import warnings diff --git a/package/MDAnalysis/analysis/encore/clustering/cluster.py b/package/MDAnalysis/analysis/encore/clustering/cluster.py index 9e0ea01fc45..0ad713775d6 100644 --- a/package/MDAnalysis/analysis/encore/clustering/cluster.py +++ b/package/MDAnalysis/analysis/encore/clustering/cluster.py @@ -31,6 +31,11 @@ .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ import numpy as np from ..utils import ParallelCalculation, merge_universes diff --git a/package/MDAnalysis/analysis/encore/confdistmatrix.py b/package/MDAnalysis/analysis/encore/confdistmatrix.py index 483964d5594..2f3e83b94ff 100644 --- a/package/MDAnalysis/analysis/encore/confdistmatrix.py +++ b/package/MDAnalysis/analysis/encore/confdistmatrix.py @@ -34,6 +34,11 @@ class to compute an RMSD matrix in such a way is also available. .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ from joblib import Parallel, delayed import numpy as np diff --git a/package/MDAnalysis/analysis/encore/covariance.py b/package/MDAnalysis/analysis/encore/covariance.py index 1fb77b10785..e6768bf698d 100644 --- a/package/MDAnalysis/analysis/encore/covariance.py +++ b/package/MDAnalysis/analysis/encore/covariance.py @@ -30,6 +30,12 @@ :Author: Matteo Tiberti, Wouter Boomsma, Tone Bengtsen .. versionadded:: 0.16.0 + +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ import numpy as np diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py index 10cd28ce4d6..cef202843d7 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py @@ -32,6 +32,11 @@ .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ import logging import warnings diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py index 1432c4a06de..1a35548fbf6 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py @@ -31,6 +31,11 @@ .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + """ import numpy as np from ..confdistmatrix import get_distance_matrix diff --git a/package/MDAnalysis/analysis/encore/similarity.py b/package/MDAnalysis/analysis/encore/similarity.py index 9e1ee2a6749..2f41d233d48 100644 --- a/package/MDAnalysis/analysis/encore/similarity.py +++ b/package/MDAnalysis/analysis/encore/similarity.py @@ -29,6 +29,11 @@ .. versionadded:: 0.16.0 +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + The module contains implementations of similarity measures between protein ensembles described in :footcite:p:`LindorffLarsen2009`. The implementation and examples are described in :footcite:p:`Tiberti2015`. diff --git a/package/doc/sphinx/source/documentation_pages/analysis/encore.rst b/package/doc/sphinx/source/documentation_pages/analysis/encore.rst index 2051a2e3352..d4da4612601 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis/encore.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis/encore.rst @@ -9,6 +9,12 @@ .. versionadded:: 0.16.0 + +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + The module contains implementations of similarity measures between protein ensembles described in :footcite:p:`LindorffLarsen2009`. The implementation and examples are described in :footcite:p:`Tiberti2015`. diff --git a/package/doc/sphinx/source/documentation_pages/analysis/encore/utils.rst b/package/doc/sphinx/source/documentation_pages/analysis/encore/utils.rst index 85a2f6cd414..e1b1e27b717 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis/encore/utils.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis/encore/utils.rst @@ -2,6 +2,11 @@ Utility functions for ENCORE ============================== +.. deprecated:: 2.8.0 + This module is deprecated in favour of the + MDAKit `mdaencore `_ and will be removed + in MDAnalysis 3.0.0. + .. automodule:: MDAnalysis.analysis.encore.utils :members: diff --git a/testsuite/MDAnalysisTests/analysis/test_encore.py b/testsuite/MDAnalysisTests/analysis/test_encore.py index bc07c21af73..424aae54278 100644 --- a/testsuite/MDAnalysisTests/analysis/test_encore.py +++ b/testsuite/MDAnalysisTests/analysis/test_encore.py @@ -30,6 +30,7 @@ import os import warnings import platform +from importlib import reload import pytest from numpy.testing import assert_equal, assert_allclose @@ -46,6 +47,11 @@ def function(x): return x**2 +def test_moved_to_mdakit_warning(): + wmsg = "MDAnalysis.analysis.encore is deprecated" + with pytest.warns(DeprecationWarning, match=wmsg): + reload(encore) + class TestEncore(object): @pytest.fixture(scope='class') def ens1_template(self): From 9b6974529de5cbcfeb1e811dbaa9a69f5aac819f Mon Sep 17 00:00:00 2001 From: Aya Alaa Date: Sat, 19 Oct 2024 17:21:00 +0300 Subject: [PATCH 23/57] Introduce Guesser classes for different contexts (#3753) * Adds Guesser classes (GuesserBase and DefaultGuesser) * Adds guess_TopologyAttrs method to Universe * Modifies all Topology parsers to remove guessing in parsing and move it to Universe creation --- benchmarks/benchmarks/topology.py | 4 +- package/CHANGELOG | 17 +- package/MDAnalysis/__init__.py | 2 +- package/MDAnalysis/analysis/bat.py | 2 +- package/MDAnalysis/converters/OpenMMParser.py | 44 +- package/MDAnalysis/converters/ParmEd.py | 2 +- package/MDAnalysis/converters/ParmEdParser.py | 2 +- package/MDAnalysis/converters/RDKit.py | 4 +- package/MDAnalysis/converters/RDKitParser.py | 16 +- package/MDAnalysis/coordinates/PDB.py | 1 - package/MDAnalysis/core/groups.py | 23 +- package/MDAnalysis/core/topologyattrs.py | 13 + package/MDAnalysis/core/universe.py | 158 ++++- package/MDAnalysis/guesser/__init__.py | 50 ++ package/MDAnalysis/guesser/base.py | 202 +++++++ package/MDAnalysis/guesser/default_guesser.py | 568 ++++++++++++++++++ .../{topology => guesser}/tables.py | 2 +- package/MDAnalysis/topology/CRDParser.py | 18 +- package/MDAnalysis/topology/DLPolyParser.py | 19 +- package/MDAnalysis/topology/DMSParser.py | 10 +- .../MDAnalysis/topology/ExtendedPDBParser.py | 2 - package/MDAnalysis/topology/FHIAIMSParser.py | 16 +- package/MDAnalysis/topology/GMSParser.py | 14 +- package/MDAnalysis/topology/GROParser.py | 16 +- package/MDAnalysis/topology/GSDParser.py | 1 - package/MDAnalysis/topology/HoomdXMLParser.py | 1 - package/MDAnalysis/topology/ITPParser.py | 54 +- package/MDAnalysis/topology/LAMMPSParser.py | 21 +- package/MDAnalysis/topology/MMTFParser.py | 11 +- package/MDAnalysis/topology/MOL2Parser.py | 17 +- package/MDAnalysis/topology/PDBParser.py | 35 +- package/MDAnalysis/topology/PDBQTParser.py | 12 +- package/MDAnalysis/topology/PQRParser.py | 21 +- package/MDAnalysis/topology/PSFParser.py | 2 +- package/MDAnalysis/topology/TOPParser.py | 9 +- package/MDAnalysis/topology/TPRParser.py | 1 - package/MDAnalysis/topology/TXYZParser.py | 21 +- package/MDAnalysis/topology/XYZParser.py | 17 +- package/MDAnalysis/topology/__init__.py | 7 +- package/MDAnalysis/topology/core.py | 11 - package/MDAnalysis/topology/guessers.py | 526 ---------------- package/MDAnalysis/topology/tpr/obj.py | 2 +- .../documentation_pages/guesser_modules.rst | 61 ++ .../guesser_modules/base.rst | 1 + .../guesser_modules/default_guesser.rst | 1 + .../guesser_modules/init.rst | 1 + .../guesser_modules/tables.rst | 1 + .../documentation_pages/topology/guessers.rst | 2 - .../documentation_pages/topology/tables.rst | 1 - .../documentation_pages/topology_modules.rst | 6 +- package/doc/sphinx/source/index.rst | 18 +- .../MDAnalysisTests/analysis/test_base.py | 2 +- .../analysis/test_dielectric.py | 2 +- .../converters/test_openmm_parser.py | 58 +- .../MDAnalysisTests/converters/test_rdkit.py | 8 +- .../converters/test_rdkit_parser.py | 39 +- testsuite/MDAnalysisTests/coordinates/base.py | 12 +- .../coordinates/test_chainreader.py | 44 +- .../MDAnalysisTests/coordinates/test_h5md.py | 2 +- .../coordinates/test_netcdf.py | 10 +- .../coordinates/test_timestep_api.py | 4 +- .../MDAnalysisTests/coordinates/test_trz.py | 2 +- .../MDAnalysisTests/core/test_atomgroup.py | 9 +- .../core/test_topologyattrs.py | 17 +- .../MDAnalysisTests/core/test_universe.py | 68 ++- .../MDAnalysisTests/guesser/test_base.py | 102 ++++ .../guesser/test_default_guesser.py | 302 ++++++++++ .../parallelism/test_multiprocessing.py | 2 +- testsuite/MDAnalysisTests/topology/base.py | 28 +- .../MDAnalysisTests/topology/test_crd.py | 13 + .../MDAnalysisTests/topology/test_dlpoly.py | 21 +- .../MDAnalysisTests/topology/test_dms.py | 6 +- .../MDAnalysisTests/topology/test_fhiaims.py | 19 +- .../MDAnalysisTests/topology/test_gms.py | 12 +- .../MDAnalysisTests/topology/test_gro.py | 15 +- .../MDAnalysisTests/topology/test_gsd.py | 9 +- .../MDAnalysisTests/topology/test_guessers.py | 205 ------- .../MDAnalysisTests/topology/test_hoomdxml.py | 2 +- .../MDAnalysisTests/topology/test_itp.py | 81 ++- .../topology/test_lammpsdata.py | 27 +- .../MDAnalysisTests/topology/test_minimal.py | 6 +- .../MDAnalysisTests/topology/test_mmtf.py | 15 +- .../MDAnalysisTests/topology/test_mol2.py | 23 +- .../MDAnalysisTests/topology/test_pdb.py | 34 +- .../MDAnalysisTests/topology/test_pdbqt.py | 9 + .../MDAnalysisTests/topology/test_pqr.py | 19 +- .../MDAnalysisTests/topology/test_psf.py | 2 +- .../topology/test_tprparser.py | 2 + .../MDAnalysisTests/topology/test_txyz.py | 18 +- .../MDAnalysisTests/topology/test_xpdb.py | 12 + .../MDAnalysisTests/topology/test_xyz.py | 17 +- .../transformations/test_positionaveraging.py | 6 +- 92 files changed, 2160 insertions(+), 1190 deletions(-) create mode 100644 package/MDAnalysis/guesser/__init__.py create mode 100644 package/MDAnalysis/guesser/base.py create mode 100644 package/MDAnalysis/guesser/default_guesser.py rename package/MDAnalysis/{topology => guesser}/tables.py (99%) delete mode 100644 package/MDAnalysis/topology/guessers.py create mode 100644 package/doc/sphinx/source/documentation_pages/guesser_modules.rst create mode 100644 package/doc/sphinx/source/documentation_pages/guesser_modules/base.rst create mode 100644 package/doc/sphinx/source/documentation_pages/guesser_modules/default_guesser.rst create mode 100644 package/doc/sphinx/source/documentation_pages/guesser_modules/init.rst create mode 100644 package/doc/sphinx/source/documentation_pages/guesser_modules/tables.rst delete mode 100644 package/doc/sphinx/source/documentation_pages/topology/guessers.rst delete mode 100644 package/doc/sphinx/source/documentation_pages/topology/tables.rst create mode 100644 testsuite/MDAnalysisTests/guesser/test_base.py create mode 100644 testsuite/MDAnalysisTests/guesser/test_default_guesser.py delete mode 100644 testsuite/MDAnalysisTests/topology/test_guessers.py diff --git a/benchmarks/benchmarks/topology.py b/benchmarks/benchmarks/topology.py index 8691515a938..45c3fcf95bf 100644 --- a/benchmarks/benchmarks/topology.py +++ b/benchmarks/benchmarks/topology.py @@ -1,6 +1,6 @@ import MDAnalysis import numpy as np -from MDAnalysis.topology import guessers +from MDAnalysis.guesser import DefaultGuesser try: from MDAnalysisTests.datafiles import GRO @@ -26,7 +26,7 @@ def setup(self, num_atoms): def time_guessbonds(self, num_atoms): """Benchmark for guessing bonds""" - guessers.guess_bonds(self.ag, self.ag.positions, + DefaultGuesser(None).guess_bonds(self.ag, self.ag.positions, box=self.ag.dimensions, vdwradii=self.vdwradii) diff --git a/package/CHANGELOG b/package/CHANGELOG index 1763df44ec3..64fcb63fe0e 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -16,13 +16,14 @@ The rules for this file: ------------------------------------------------------------------------------- ??/??/?? IAlibay, HeetVekariya, marinegor, lilyminium, RMeli, ljwoods2, aditya292002, pstaerk, PicoCentauri, BFedder, - tyler.je.reddy, SampurnaM, leonwehrhan, kainszs, orionarcher, + tyler.je.reddy, SampurnaM, leonwehrhan, kainszs, orionarcher, yuxuanzhuang, PythonFZ, laksh-krishna-sharma, orbeckst, MattTDavies, - talagayev + talagayev, aya9aladdin * 2.8.0 Fixes + * Fix Bohrium (Bh) atomic mass in tables.py (PR #3753) * set `n_parts` to the total number of frames being analyzed if `n_parts` is bigger. (Issue #4685) * Catch higher dimensional indexing in GroupBase & ComponentBase (Issue #4647) @@ -56,6 +57,15 @@ Fixes * Fix groups.py doctests using sphinx directives (Issue #3925, PR #4374) Enhancements + * Removed type and mass guessing from all topology parsers (PR #3753) + * Added guess_TopologyAttrs() API to the Universe to handle attribute + guessing (PR #3753) + * Added the DefaultGuesser class, which is a general-purpose guesser with + the same functionalities as the existing guesser.py methods (PR #3753) + * Added is_value_missing() to `TopologyAttrs` to check for missing + values (PR #3753) + * Added guessed `Element` attribute to the ITPParser to preserve old mass + partial guessing behavior from being broken (PR #3753) * MDAnalysis now supports Python 3.13 (PR #4732) * Introduce parallelization API to `AnalysisBase` and to `analysis.rms.RMSD` class (Issue #4158, PR #4304) @@ -100,6 +110,9 @@ Changes numpy.testing.assert_allclose #4438) Deprecations + * Unknown masses are set to 0.0 for current version, this will be depracated + in version 3.0.0 and replaced by :class:`Masses`' no_value_label attribute(np.nan) + (PR #3753) * The MDAnalysis.anaylsis.encore module has been deprecated in favour of the mdaencore MDAKit and will be removed in version 3.0.0 (PR #4737) * The MMTF Reader is deprecated and will be removed in version 3.0 diff --git a/package/MDAnalysis/__init__.py b/package/MDAnalysis/__init__.py index 0e9e5574607..ca11be4bdf2 100644 --- a/package/MDAnalysis/__init__.py +++ b/package/MDAnalysis/__init__.py @@ -181,7 +181,7 @@ _TOPOLOGY_ATTRS: Dict = {} # {attrname: cls} _TOPOLOGY_TRANSPLANTS: Dict = {} # {name: [attrname, method, transplant class]} _TOPOLOGY_ATTRNAMES: Dict = {} # {lower case name w/o _ : name} - +_GUESSERS: Dict = {} # custom exceptions and warnings from .exceptions import ( diff --git a/package/MDAnalysis/analysis/bat.py b/package/MDAnalysis/analysis/bat.py index 5186cb6c882..c8a908f9ea5 100644 --- a/package/MDAnalysis/analysis/bat.py +++ b/package/MDAnalysis/analysis/bat.py @@ -175,7 +175,7 @@ class to calculate dihedral angles for a given set of atoms or residues def _sort_atoms_by_mass(atoms, reverse=False): - r"""Sorts a list of atoms by name and then by index + r"""Sorts a list of atoms by mass and then by index The atom index is used as a tiebreaker so that the ordering is reproducible. diff --git a/package/MDAnalysis/converters/OpenMMParser.py b/package/MDAnalysis/converters/OpenMMParser.py index eef92873783..b3402c448eb 100644 --- a/package/MDAnalysis/converters/OpenMMParser.py +++ b/package/MDAnalysis/converters/OpenMMParser.py @@ -25,6 +25,9 @@ =================================================================== .. versionadded:: 2.0.0 +.. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place + now through universe.guess_TopologyAttrs() API) Converts an @@ -59,8 +62,7 @@ import warnings from ..topology.base import TopologyReaderBase -from ..topology.tables import SYMB2Z -from ..topology.guessers import guess_types, guess_masses +from ..guesser.tables import SYMB2Z from ..core.topology import Topology from ..core.topologyattrs import ( Atomids, @@ -108,11 +110,6 @@ def _mda_topology_from_omm_topology(self, omm_topology): ------- top : MDAnalysis.core.topology.Topology - Note - ---- - When none of the elements are present in the openmm topolgy, their - atomtypes are guessed using their names and their masses are - then guessed using their atomtypes. When partial elements are present, values from available elements are used whereas the absent elements are assigned an empty string @@ -184,21 +181,32 @@ def _mda_topology_from_omm_topology(self, omm_topology): warnings.warn("Element information missing for some atoms. " "These have been given an empty element record ") if any(i == 'X' for i in atomtypes): - warnings.warn("For absent elements, atomtype has been " - "set to 'X' and mass has been set to 0.0. " - "If needed these can be guessed using " - "MDAnalysis.topology.guessers.") + warnings.warn( + "For absent elements, atomtype has been " + "set to 'X' and mass has been set to 0.0. " + "If needed these can be guessed using " + "universe.guess_TopologyAttrs(" + "to_guess=['masses', 'types']). " + "(for MDAnalysis version 2.x " + "this is done automatically," + " but it will be removed in 3.0).") + attrs.append(Elements(np.array(validated_elements, dtype=object))) else: - atomtypes = guess_types(atomnames) - masses = guess_masses(atomtypes) - wmsg = ("Element information is missing for all the atoms. " - "Elements attribute will not be populated. " - "Atomtype attribute will be guessed using atom " - "name and mass will be guessed using atomtype." - "See MDAnalysis.topology.guessers.") + wmsg = ( + "Element information is missing for all the atoms. " + "Elements attribute will not be populated. " + "Atomtype attribute will be guessed using atom " + "name and mass will be guessed using atomtype." + "For MDAnalysis version 2.x this is done automatically, " + "but it will be removed in MDAnalysis v3.0. " + "These can be guessed using " + "universe.guess_TopologyAttrs(" + "to_guess=['masses', 'types']) " + "See MDAnalysis.guessers.") + warnings.warn(wmsg) else: attrs.append(Elements(np.array(validated_elements, dtype=object))) diff --git a/package/MDAnalysis/converters/ParmEd.py b/package/MDAnalysis/converters/ParmEd.py index 174dc9fad3b..cc2e7a4cc52 100644 --- a/package/MDAnalysis/converters/ParmEd.py +++ b/package/MDAnalysis/converters/ParmEd.py @@ -81,12 +81,12 @@ import itertools import warnings +from ..guesser.tables import SYMB2Z import numpy as np from numpy.lib import NumpyVersion from . import base from ..coordinates.base import SingleFrameReaderBase -from ..topology.tables import SYMB2Z from ..core.universe import Universe from ..exceptions import NoDataError diff --git a/package/MDAnalysis/converters/ParmEdParser.py b/package/MDAnalysis/converters/ParmEdParser.py index 55dc48b2f1c..86de585fe53 100644 --- a/package/MDAnalysis/converters/ParmEdParser.py +++ b/package/MDAnalysis/converters/ParmEdParser.py @@ -90,7 +90,7 @@ import numpy as np from ..topology.base import TopologyReaderBase, change_squash -from ..topology.tables import Z2SYMB +from ..guesser.tables import Z2SYMB from ..core.topologyattrs import ( Atomids, Atomnames, diff --git a/package/MDAnalysis/converters/RDKit.py b/package/MDAnalysis/converters/RDKit.py index 139528440ab..b6d806df4c0 100644 --- a/package/MDAnalysis/converters/RDKit.py +++ b/package/MDAnalysis/converters/RDKit.py @@ -255,9 +255,7 @@ class RDKitConverter(base.ConverterBase): from MDAnalysisTests.datafiles import PSF, DCD from rdkit.Chem.Descriptors3D import Asphericity - u = mda.Universe(PSF, DCD) - elements = mda.topology.guessers.guess_types(u.atoms.names) - u.add_TopologyAttr('elements', elements) + u = mda.Universe(PSF, DCD, to_guess=['elements']) ag = u.select_atoms("resid 1-10") for ts in u.trajectory: diff --git a/package/MDAnalysis/converters/RDKitParser.py b/package/MDAnalysis/converters/RDKitParser.py index 841f979eeca..6bca57a43fe 100644 --- a/package/MDAnalysis/converters/RDKitParser.py +++ b/package/MDAnalysis/converters/RDKitParser.py @@ -1,5 +1,5 @@ # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 # # MDAnalysis --- https://www.mdanalysis.org # Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors @@ -47,7 +47,6 @@ import numpy as np from ..topology.base import TopologyReaderBase, change_squash -from ..topology import guessers from ..core.topologyattrs import ( Atomids, Atomnames, @@ -90,6 +89,7 @@ class RDKitParser(TopologyReaderBase): - Atomnames - Aromaticities - Elements + - Types - Masses - Bonds - Resids @@ -97,9 +97,6 @@ class RDKitParser(TopologyReaderBase): - RSChirality - Segids - Guesses the following: - - Atomtypes - Depending on RDKit's input, the following Attributes might be present: - Charges - Resnames @@ -156,6 +153,12 @@ class RDKitParser(TopologyReaderBase): .. versionadded:: 2.0.0 .. versionchanged:: 2.1.0 Added R/S chirality support + .. versionchanged:: 2.8.0 + Removed type guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). If atoms types is not + present in the input rdkit molecule as a _TriposAtomType property, + the type attribute get the same values as the element attribute. + """ format = 'RDKIT' @@ -303,8 +306,7 @@ def parse(self, **kwargs): if atomtypes: attrs.append(Atomtypes(np.array(atomtypes, dtype=object))) else: - atomtypes = guessers.guess_types(names) - attrs.append(Atomtypes(atomtypes, guessed=True)) + atomtypes = np.char.upper(elements) # Partial charges if charges: diff --git a/package/MDAnalysis/coordinates/PDB.py b/package/MDAnalysis/coordinates/PDB.py index bce51c43cdc..5e9530cac8a 100644 --- a/package/MDAnalysis/coordinates/PDB.py +++ b/package/MDAnalysis/coordinates/PDB.py @@ -155,7 +155,6 @@ from ..lib.util import store_init_arguments from . import base from .timestep import Timestep -from ..topology.core import guess_atom_element from ..exceptions import NoDataError diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 91b2c779304..7c9a3650dfd 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -3453,7 +3453,6 @@ def guess_bonds(self, vdwradii=None, fudge_factor=0.55, lower_bound=0.1): ---------- vdwradii : dict, optional Dict relating atom types: vdw radii - fudge_factor : float, optional The factor by which atoms must overlap each other to be considered a bond. Larger values will increase the number of bonds found. [0.55] @@ -3477,8 +3476,8 @@ def guess_bonds(self, vdwradii=None, fudge_factor=0.55, lower_bound=0.1): Corrected misleading docs, and now allows passing of `fudge_factor` and `lower_bound` arguments. """ - from ..topology.core import guess_bonds, guess_angles, guess_dihedrals from .topologyattrs import Bonds, Angles, Dihedrals + from ..guesser.default_guesser import DefaultGuesser def get_TopAttr(u, name, cls): """either get *name* or create one from *cls*""" @@ -3490,22 +3489,20 @@ def get_TopAttr(u, name, cls): return attr # indices of bonds - b = guess_bonds( - self.atoms, - self.atoms.positions, - vdwradii=vdwradii, - box=self.dimensions, - fudge_factor=fudge_factor, - lower_bound=lower_bound, - ) - bondattr = get_TopAttr(self.universe, "bonds", Bonds) + guesser = DefaultGuesser(None, fudge_factor=fudge_factor, + lower_bound=lower_bound, + box=self.dimensions, + vdwradii=vdwradii) + b = guesser.guess_bonds(self.atoms, self.atoms.positions) + + bondattr = get_TopAttr(self.universe, 'bonds', Bonds) bondattr._add_bonds(b, guessed=True) - a = guess_angles(self.bonds) + a = guesser.guess_angles(self.bonds) angleattr = get_TopAttr(self.universe, 'angles', Angles) angleattr._add_bonds(a, guessed=True) - d = guess_dihedrals(self.angles) + d = guesser.guess_dihedrals(self.angles) diheattr = get_TopAttr(self.universe, 'dihedrals', Dihedrals) diheattr._add_bonds(d) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 92fa25a5e5d..5e2621dc63d 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -518,6 +518,18 @@ def set_segments(self, sg, values): """Set segmentattributes for a given SegmentGroup""" raise NotImplementedError + @classmethod + def are_values_missing(cls, values): + """check if an attribute has a missing value + + .. versionadded:: 2.8.0 + """ + missing_value_label = getattr(cls, 'missing_value_label', None) + + if missing_value_label is np.nan: + return np.isnan(values) + else: + return values == missing_value_label # core attributes @@ -1441,6 +1453,7 @@ class Masses(AtomAttr): attrname = 'masses' singular = 'mass' per_object = 'atom' + missing_value_label = np.nan target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue, Segment] transplants = defaultdict(list) diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index 739e0483395..c0ac6bcf6fd 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -86,7 +86,7 @@ from .topology import Topology from .topologyattrs import AtomAttr, ResidueAttr, SegmentAttr, BFACTOR_WARNING from .topologyobjects import TopologyObject - +from ..guesser.base import get_guesser logger = logging.getLogger("MDAnalysis.core.universe") @@ -238,6 +238,25 @@ class Universe(object): vdwradii: dict, ``None``, default ``None`` For use with *guess_bonds*. Supply a dict giving a vdwradii for each atom type which are used in guessing bonds. + context: str or :mod:`Guesser`, default ``'default'`` + Type of the Guesser to be used in guessing TopologyAttrs + to_guess: list[str] (optional, default ``['types', 'masses']``) + TopologyAttrs to be guessed. These TopologyAttrs will be wholly + guessed if they don't exist in the Universe. If they already exist in + the Universe, only empty or missing values will be guessed. + + .. warning:: + In MDAnalysis 2.x, types and masses are being automatically guessed + if they are missing (``to_guess=('types, 'masses')``). + However, starting with release 3.0 **no guessing will be done + by default** and it will be up to the user to request guessing + using ``to_guess`` and ``force_guess``. + + force_guess: list[str], (optional) + TopologyAttrs in this list will be force guessed. If the TopologyAttr + does not already exist in the Universe, this has no effect. If the + TopologyAttr does already exist, all values will be overwritten + by guessed values. fudge_factor: float, default [0.55] For use with *guess_bonds*. Supply the factor by which atoms must overlap each other to be considered a bond. @@ -267,7 +286,7 @@ class Universe(object): dimensions : numpy.ndarray system dimensions (simulation unit cell, if set in the trajectory) at the *current time step* - (see :attr:`MDAnalysis.coordinates.timestep.Timestep.dimensions`). + (see :attr:`MDAnalysis.coordinates.base.Timestep.dimensions`). The unit cell can be set for the current time step (but the change is not permanent unless written to a file). atoms : AtomGroup @@ -320,11 +339,18 @@ class Universe(object): .. versionchanged:: 2.5.0 Added fudge_factor and lower_bound parameters for use with *guess_bonds*. + + .. versionchanged:: 2.8.0 + Added :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` API + guessing masses and atom types after topology + is read from a registered parser. + """ def __init__(self, topology=None, *coordinates, all_coordinates=False, format=None, topology_format=None, transformations=None, guess_bonds=False, vdwradii=None, fudge_factor=0.55, - lower_bound=0.1, in_memory=False, + lower_bound=0.1, in_memory=False, context='default', + to_guess=('types', 'masses'), force_guess=(), in_memory_step=1, **kwargs): self._trajectory = None # managed attribute holding Reader @@ -333,7 +359,7 @@ def __init__(self, topology=None, *coordinates, all_coordinates=False, self.residues = None self.segments = None self.filename = None - + self._context = get_guesser(context) self._kwargs = { 'transformations': transformations, 'guess_bonds': guess_bonds, @@ -381,12 +407,17 @@ def __init__(self, topology=None, *coordinates, all_coordinates=False, self._trajectory.add_transformations(*transformations) if guess_bonds: - self.atoms.guess_bonds(vdwradii=vdwradii, fudge_factor=fudge_factor, - lower_bound=lower_bound) + force_guess = list(force_guess) + ['bonds', 'angles', 'dihedrals'] + + self.guess_TopologyAttrs( + context, to_guess, force_guess, vdwradii=vdwradii, **kwargs) + def copy(self): """Return an independent copy of this Universe""" - new = self.__class__(self._topology.copy()) + context = self._context.copy() + new = self.__class__(self._topology.copy(), + to_guess=(), context=context) new.trajectory = self.trajectory.copy() return new @@ -475,7 +506,7 @@ def empty(cls, n_atoms, n_residues=1, n_segments=1, n_frames=1, residue_segindex=residue_segindex, ) - u = cls(top) + u = cls(top, to_guess=()) if n_frames > 1 or trajectory: coords = np.zeros((n_frames, n_atoms, 3), dtype=np.float32) @@ -714,10 +745,10 @@ def __repr__(self): n_atoms=len(self.atoms)) @classmethod - def _unpickle_U(cls, top, traj): + def _unpickle_U(cls, top, traj, context): """Special method used by __reduce__ to deserialise a Universe""" # top is a Topology obj at this point, but Universe can handle that. - u = cls(top) + u = cls(top, to_guess=(), context=context) u.trajectory = traj return u @@ -727,7 +758,7 @@ def __reduce__(self): # transformation (that has AtomGroup inside). Use __reduce__ instead. # Universe's two "legs" of top and traj both serialise themselves. return (self._unpickle_U, (self._topology, - self._trajectory)) + self._trajectory, self._context.copy())) # Properties @property @@ -1459,13 +1490,114 @@ def from_smiles(cls, smiles, sanitize=True, addHs=True, "hydrogens with `addHs=True`") numConfs = rdkit_kwargs.pop("numConfs", numConfs) - if not (type(numConfs) is int and numConfs > 0): + if not (isinstance(numConfs, int) and numConfs > 0): raise SyntaxError("numConfs must be a non-zero positive " - "integer instead of {0}".format(numConfs)) + "integer instead of {0}".format(numConfs)) AllChem.EmbedMultipleConfs(mol, numConfs, **rdkit_kwargs) return cls(mol, **kwargs) + def guess_TopologyAttrs( + self, context=None, to_guess=None, force_guess=None, **kwargs): + """ + Guess and add attributes through a specific context-aware guesser. + + Parameters + ---------- + context: str or :mod:`Guesser` class + For calling a matching guesser class for this specific context + to_guess: Optional[list[str]] + TopologyAttrs to be guessed. These TopologyAttrs will be wholly + guessed if they don't exist in the Universe. If they already exist in + the Universe, only empty or missing values will be guessed. + + .. warning:: + In MDAnalysis 2.x, types and masses are being automatically guessed + if they are missing (``to_guess=('types, 'masses')``). + However, starting with release 3.0 **no guessing will be done + by default** and it will be up to the user to request guessing + using ``to_guess`` and ``force_guess``. + + force_guess: Optional[list[str]] + TopologyAttrs in this list will be force guessed. If the + TopologyAttr does not already exist in the Universe, this has no + effect. If the TopologyAttr does already exist, all values will + be overwritten by guessed values. + **kwargs: extra arguments to be passed to the guesser class + + Examples + -------- + To guess ``masses`` and ``types`` attributes:: + + u.guess_TopologyAttrs(context='default', to_guess=['masses', 'types']) + + .. versionadded:: 2.8.0 + + """ + if not context: + context = self._context + + guesser = get_guesser(context, self.universe, **kwargs) + self._context = guesser + + if to_guess is None: + to_guess = [] + if force_guess is None: + force_guess = [] + + total_guess = list(to_guess) + list(force_guess) + + # Removing duplicates from the guess list while keeping attributes + # order as it is more convenient to guess attributes + # in the same order that the user provided + total_guess = list(dict.fromkeys(total_guess)) + + objects = ['bonds', 'angles', 'dihedrals', 'impropers'] + + # Checking if the universe is empty to avoid errors + # from guesser methods + if self._topology.n_atoms > 0: + + topology_attrs = [att.attrname for att in + self._topology.read_attributes] + + common_attrs = set(to_guess) & set(topology_attrs) + common_attrs = ", ".join(attr for attr in common_attrs) + + if len(common_attrs) > 0: + logger.info( + f'The attribute(s) {common_attrs} have already been read ' + 'from the topology file. The guesser will ' + 'only guess empty values for this attribute, ' + 'if any exists. To overwrite it by completely ' + 'guessed values, you can pass the attribute to' + ' the force_guess parameter instead of ' + 'the to_guess one') + + for attr in total_guess: + if guesser.is_guessable(attr): + fg = attr in force_guess + values = guesser.guess_attr(attr, fg) + + if values is not None: + if attr in objects: + self._add_topology_objects( + attr, values, guessed=True) + else: + guessed_attr = _TOPOLOGY_ATTRS[attr](values, True) + self.add_TopologyAttr(guessed_attr) + logger.info( + f'attribute {attr} has been guessed' + ' successfully.') + + else: + raise ValueError(f'{context} guesser can not guess the' + f' following attribute: {attr}') + + else: + warnings.warn('Can not guess attributes ' + 'for universe with 0 atoms') + def Merge(*args): """Create a new new :class:`Universe` from one or more diff --git a/package/MDAnalysis/guesser/__init__.py b/package/MDAnalysis/guesser/__init__.py new file mode 100644 index 00000000000..b433356290f --- /dev/null +++ b/package/MDAnalysis/guesser/__init__.py @@ -0,0 +1,50 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# 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 Lesser 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 +# + +""" +Context-specific guessers --- :mod:`MDAnalysis.guesser` +======================================================== + +You can use guesser classes either directly by initiating an instance of it and use its guessing methods or through +the :meth:`guess_TopologyAttrs `: API of the universe. + +The following table lists the currently supported Context-aware Guessers along with +the attributes they can guess. + +.. table:: Table of Supported Guessers + + ============================================== ========== ===================== =================================================== + Name Context Attributes Remarks + ============================================== ========== ===================== =================================================== + :ref:`DefaultGuesser ` default types, elements, general purpose guesser + masses, bonds, + angles, dihedrals, + improper dihedrals + ============================================== ========== ===================== =================================================== + + + + +""" +from . import base +from .default_guesser import DefaultGuesser diff --git a/package/MDAnalysis/guesser/base.py b/package/MDAnalysis/guesser/base.py new file mode 100644 index 00000000000..aab0723aaa6 --- /dev/null +++ b/package/MDAnalysis/guesser/base.py @@ -0,0 +1,202 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding: utf-8 -*- +# 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 Lesser 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 +# +""" +Base guesser classes --- :mod:`MDAnalysis.guesser.base` +================================================================ + +Derive context-specific guesser classes from the base class in this module. + +Classes +------- + +.. autoclass:: GuesserBase + :members: + :inherited-members: + +.. autofunction:: get_guesser + +""" +from .. import _GUESSERS +import numpy as np +from .. import _TOPOLOGY_ATTRS +import logging +from typing import Dict +import copy + +logger = logging.getLogger("MDAnalysis.guesser.base") + + +class _GuesserMeta(type): + """Internal: guesser classes registration + + When classes which inherit from GuesserBase are *defined* + this metaclass makes it known to MDAnalysis. 'context' + attribute are read: + - `context` defines the context of the guesser class for example: + forcefield specific context as MartiniGuesser + and file specific context as PDBGuesser. + + Eg:: + + class FooGuesser(GuesserBase): + format = 'foo' + + .. versionadded:: 2.8.0 + """ + def __init__(cls, name, bases, classdict): + type.__init__(type, name, bases, classdict) + + _GUESSERS[classdict['context'].upper()] = cls + + +class GuesserBase(metaclass=_GuesserMeta): + """Base class for context-specific guessers to inherit from + + Parameters + ---------- + universe : Universe, optional + Supply a Universe to the Guesser. This then becomes the source of atom + attributes to be used in guessing processes. (this is relevant to how + the universe's guess_TopologyAttrs API works. + See :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs`). + **kwargs : dict, optional + To pass additional data to the guesser that can be used with + different methods. + + + .. versionadded:: 2.8.0 + + """ + context = 'base' + _guesser_methods: Dict = {} + + def __init__(self, universe=None, **kwargs): + self._universe = universe + self._kwargs = kwargs + + def update_kwargs(self, **kwargs): + self._kwargs.update(kwargs) + + def copy(self): + """Return a copy of this Guesser""" + kwargs = copy.deepcopy(self._kwargs) + new = self.__class__(universe=None, **kwargs) + return new + + def is_guessable(self, attr_to_guess): + """check if the passed atrribute can be guessed by the guesser class + + Parameters + ---------- + guess: str + Attribute to be guessed then added to the Universe + + Returns + ------- + bool + """ + if attr_to_guess.lower() in self._guesser_methods: + return True + + return False + + def guess_attr(self, attr_to_guess, force_guess=False): + """map the attribute to be guessed with the apporpiate guessing method + + Parameters + ---------- + attr_to_guess: str + an atrribute to be guessed then to be added to the universe + force_guess: bool + To indicate wether to only partialy guess the empty values of the + attribute or to overwrite all existing values by guessed one + + Returns + ------- + NDArray of guessed values + + """ + + # check if the topology already has the attribute to partially guess it + if hasattr(self._universe.atoms, attr_to_guess) and not force_guess: + attr_values = np.array( + getattr(self._universe.atoms, attr_to_guess, None)) + + top_attr = _TOPOLOGY_ATTRS[attr_to_guess] + + empty_values = top_attr.are_values_missing(attr_values) + + if True in empty_values: + # pass to the guesser_method boolean mask to only guess the + # empty values + attr_values[empty_values] = self._guesser_methods[attr_to_guess]( + indices_to_guess=empty_values) + return attr_values + + else: + logger.info( + f'There is no empty {attr_to_guess} values. Guesser did ' + f'not guess any new values for {attr_to_guess} attribute') + return None + else: + return np.array(self._guesser_methods[attr_to_guess]()) + + +def get_guesser(context, u=None, **kwargs): + """get an appropiate guesser to the Universe and pass + the Universe to the guesser + + Parameters + ---------- + u: Universe + to be passed to the guesser + context: str or Guesser + **kwargs : dict, optional + Extra arguments are passed to the guesser. + + Returns + ------- + Guesser class + + Raises + ------ + * :exc:`KeyError` upon failing to return a guesser class + + .. versionadded:: 2.8.0 + + """ + if isinstance(context, GuesserBase): + context._universe = u + context.update_kwargs(**kwargs) + return context + try: + if issubclass(context, GuesserBase): + return context(u, **kwargs) + except TypeError: + pass + + try: + guesser = _GUESSERS[context.upper()](u, **kwargs) + except KeyError: + raise KeyError("Unidentified guesser type {0}".format(context)) + return guesser diff --git a/package/MDAnalysis/guesser/default_guesser.py b/package/MDAnalysis/guesser/default_guesser.py new file mode 100644 index 00000000000..a64b023309e --- /dev/null +++ b/package/MDAnalysis/guesser/default_guesser.py @@ -0,0 +1,568 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding: utf-8 -*- +# 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 Lesser 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 + +r""" +Default Guesser +================ +.. _DefaultGuesser: + +DefaultGuesser is a generic guesser class that has basic guessing methods. +This class is a general purpose guesser that can be used with most topologies, +but being generic makes it the less accurate among all guessers. + + + + + +Classes +------- + +.. autoclass:: DefaultGuesser + :members: + :inherited-members: + +""" +from .base import GuesserBase +import numpy as np +import warnings +import math + +import re + +from ..lib import distances +from . import tables + + +class DefaultGuesser(GuesserBase): + """ + This guesser holds generic methods (not directed to specific contexts) for + guessing different topology attribute. It has the same methods which where + originally found in Topology.guesser.py. The attributes that can be + guessed by this class are: + - masses + - types + - elements + - angles + - dihedrals + - bonds + - improper dihedrals + - aromaticities + + You can use this guesser either directly through an instance, or through + the :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` method. + + Examples + -------- + to guess bonds for a universe:: + + import MDAnalysis as mda + from MDAnalysisTests.datafiles import two_water_gro + + u = mda.Universe(two_water_gro, context='default', to_guess=['bonds']) + + .. versionadded:: 2.8.0 + + """ + context = 'default' + + def __init__(self, universe, **kwargs): + super().__init__(universe, **kwargs) + self._guesser_methods = { + 'masses': self.guess_masses, + 'types': self.guess_types, + 'elements': self.guess_types, + 'bonds': self.guess_bonds, + 'angles': self.guess_angles, + 'dihedrals': self.guess_dihedrals, + 'impropers': self.guess_improper_dihedrals, + 'aromaticities': self.guess_aromaticities, + } + + def guess_masses(self, atom_types=None, indices_to_guess=None): + """Guess the mass of many atoms based upon their type. + For guessing masses through :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs`: + + First try to guess masses from atom elements, if not available, + try to guess masses from types and if not available, try to guess + types. + + Parameters + ---------- + atom_types : Optional[np.ndarray] + Atom types/elements to guess masses from + indices_to_guess : Optional[np.ndarray] + Mask array for partially guess masses for certain atoms + + Returns + ------- + atom_masses : np.ndarray dtype float64 + + Raises + ------ + :exc:`ValueError` + If there are no atom types or elements to guess mass from. + + """ + if atom_types is None: + try: + atom_types = self._universe.atoms.elements + except AttributeError: + try: + atom_types = self._universe.atoms.types + except AttributeError: + try: + atom_types = self.guess_types( + atom_types=self._universe.atoms.names) + except ValueError: + raise ValueError( + "there is no reference attributes" + " (elements, types, or names)" + " in this universe to guess mass from") + + if indices_to_guess is not None: + atom_types = atom_types[indices_to_guess] + + masses = np.array([self.get_atom_mass(atom) + for atom in atom_types], dtype=np.float64) + return masses + + def get_atom_mass(self, element): + """Return the atomic mass in u for *element*. + Masses are looked up in :data:`MDAnalysis.guesser.tables.masses`. + + .. Warning:: Until version 3.0.0 unknown masses are set to 0.0 + + """ + try: + return tables.masses[element] + except KeyError: + try: + return tables.masses[element.upper()] + except KeyError: + warnings.warn( + "Unknown masses are set to 0.0 for current version, " + "this will be deprecated in version 3.0.0 and replaced by" + " Masse's no_value_label (np.nan)", + PendingDeprecationWarning) + return 0.0 + + def guess_atom_mass(self, atomname): + """Guess a mass based on the atom name. + + :func:`guess_atom_element` is used to determine the kind of atom. + + .. warning:: Until version 3.0.0 anything not recognized is simply + set to 0.0; if you rely on the masses you might want to double-check. + """ + return self.get_atom_mass(self.guess_atom_element(atomname)) + + def guess_types(self, atom_types=None, indices_to_guess=None): + """Guess the atom type of many atoms based on atom name + + Parameters + ---------- + atom_types (optional) + atoms names if types guessing is desired to be from names + indices_to_guess (optional) + Mask array for partially guess types for certain atoms + + Returns + ------- + atom_types : np.ndarray dtype object + + Raises + ------ + :exc:`ValueError` + If there is no names to guess types from. + + """ + if atom_types is None: + try: + atom_types = self._universe.atoms.names + except AttributeError: + raise ValueError( + "there is no reference attributes in this universe" + "to guess types from") + + if indices_to_guess is not None: + atom_types = atom_types[indices_to_guess] + + return np.array([self.guess_atom_element(atom) + for atom in atom_types], dtype=object) + + def guess_atom_element(self, atomname): + """Guess the element of the atom from the name. + + Looks in dict to see if element is found, otherwise it uses the first + character in the atomname. The table comes from CHARMM and AMBER atom + types, where the first character is not sufficient to determine the + atom type. Some GROMOS ions have also been added. + + .. Warning: The translation table is incomplete. + This will probably result in some mistakes, + but it still better than nothing! + + See Also + -------- + :func:`guess_atom_type` + :mod:`MDAnalysis.guesser.tables` + """ + NUMBERS = re.compile(r'[0-9]') # match numbers + SYMBOLS = re.compile(r'[*+-]') # match *, +, - + if atomname == '': + return '' + try: + return tables.atomelements[atomname.upper()] + except KeyError: + # strip symbols and numbers + no_symbols = re.sub(SYMBOLS, '', atomname) + name = re.sub(NUMBERS, '', no_symbols).upper() + + # just in case + if name in tables.atomelements: + return tables.atomelements[name] + + while name: + if name in tables.elements: + return name + if name[:-1] in tables.elements: + return name[:-1] + if name[1:] in tables.elements: + return name[1:] + if len(name) <= 2: + return name[0] + name = name[:-1] # probably element is on left not right + + # if it's numbers + return no_symbols + + def guess_bonds(self, atoms=None, coords=None): + r"""Guess if bonds exist between two atoms based on their distance. + + Bond between two atoms is created, if the two atoms are within + + .. math:: + + d < f \cdot (R_1 + R_2) + + of each other, where :math:`R_1` and :math:`R_2` are the VdW radii + of the atoms and :math:`f` is an ad-hoc *fudge_factor*. This is + the `same algorithm that VMD uses`_. + + Parameters + ---------- + atoms : AtomGroup + atoms for which bonds should be guessed + fudge_factor : float, optional + The factor by which atoms must overlap eachother to be considered a + bond. Larger values will increase the number of bonds found. [0.55] + vdwradii : dict, optional + To supply custom vdwradii for atoms in the algorithm. Must be a + dict of format {type:radii}. The default table of van der Waals + radii is hard-coded as :data:`MDAnalysis.guesser.tables.vdwradii`. + Any user defined vdwradii passed as an argument will supercede the + table values. [``None``] + lower_bound : float, optional + The minimum bond length. All bonds found shorter than this length + will be ignored. This is useful for parsing PDB with altloc records + where atoms with altloc A and B maybe very close together and + there should be no chemical bond between them. [0.1] + box : array_like, optional + Bonds are found using a distance search, if unit cell information + is given, periodic boundary conditions will be considered in the + distance search. [``None``] + + Returns + ------- + list + List of tuples suitable for use in Universe topology building. + + Warnings + -------- + No check is done after the bonds are guessed to see if Lewis + structure is correct. This is wrong and will burn somebody. + + Raises + ------ + :exc:`ValueError` + If inputs are malformed or `vdwradii` data is missing. + + + .. _`same algorithm that VMD uses`: + http://www.ks.uiuc.edu/Research/vmd/vmd-1.9.1/ug/node26.html + + """ + if atoms is None: + atoms = self._universe.atoms + + if coords is None: + coords = self._universe.atoms.positions + + if len(atoms) != len(coords): + raise ValueError("'atoms' and 'coord' must be the same length") + + fudge_factor = self._kwargs.get('fudge_factor', 0.55) + + # so I don't permanently change it + vdwradii = tables.vdwradii.copy() + user_vdwradii = self._kwargs.get('vdwradii', None) + # this should make algo use their values over defaults + if user_vdwradii: + vdwradii.update(user_vdwradii) + + # Try using types, then elements + if hasattr(atoms, 'types'): + atomtypes = atoms.types + else: + atomtypes = self.guess_types(atom_types=atoms.names) + + # check that all types have a defined vdw + if not all(val in vdwradii for val in set(atomtypes)): + raise ValueError(("vdw radii for types: " + + ", ".join([t for t in set(atomtypes) if + t not in vdwradii]) + + ". These can be defined manually using the" + + f" keyword 'vdwradii'")) + + lower_bound = self._kwargs.get('lower_bound', 0.1) + + box = self._kwargs.get('box', None) + + if box is not None: + box = np.asarray(box) + + # to speed up checking, calculate what the largest possible bond + # atom that would warrant attention. + # then use this to quickly mask distance results later + max_vdw = max([vdwradii[t] for t in atomtypes]) + + bonds = [] + + pairs, dist = distances.self_capped_distance(coords, + max_cutoff=2.0 * max_vdw, + min_cutoff=lower_bound, + box=box) + for idx, (i, j) in enumerate(pairs): + d = (vdwradii[atomtypes[i]] + + vdwradii[atomtypes[j]]) * fudge_factor + if (dist[idx] < d): + bonds.append((atoms[i].index, atoms[j].index)) + return tuple(bonds) + + def guess_angles(self, bonds=None): + """Given a list of Bonds, find all angles that exist between atoms. + + Works by assuming that if atoms 1 & 2 are bonded, and 2 & 3 are bonded, + then (1,2,3) must be an angle. + + Parameters + ---------- + bonds : Bonds + from which angles should be guessed + + Returns + ------- + list of tuples + List of tuples defining the angles. + Suitable for use in u._topology + + + See Also + -------- + :meth:`guess_bonds` + + """ + from ..core.universe import Universe + + angles_found = set() + + if bonds is None: + if hasattr(self._universe.atoms, 'bonds'): + bonds = self._universe.atoms.bonds + else: + temp_u = Universe.empty(n_atoms=len(self._universe.atoms)) + temp_u.add_bonds(self.guess_bonds( + self._universe.atoms, self._universe.atoms.positions)) + bonds = temp_u.atoms.bonds + + for b in bonds: + for atom in b: + other_a = b.partner(atom) # who's my friend currently in Bond + for other_b in atom.bonds: + if other_b != b: # if not the same bond I start as + third_a = other_b.partner(atom) + desc = tuple( + [other_a.index, atom.index, third_a.index]) + # first index always less than last + if desc[0] > desc[-1]: + desc = desc[::-1] + angles_found.add(desc) + + return tuple(angles_found) + + def guess_dihedrals(self, angles=None): + """Given a list of Angles, find all dihedrals that exist between atoms. + + Works by assuming that if (1,2,3) is an angle, and 3 & 4 are bonded, + then (1,2,3,4) must be a dihedral. + + Parameters + ---------- + angles : Angles + from which dihedrals should be guessed + + Returns + ------- + list of tuples + List of tuples defining the dihedrals. + Suitable for use in u._topology + + """ + from ..core.universe import Universe + + if angles is None: + if hasattr(self._universe.atoms, 'angles'): + angles = self._universe.atoms.angles + + else: + temp_u = Universe.empty(n_atoms=len(self._universe.atoms)) + + temp_u.add_bonds(self.guess_bonds( + self._universe.atoms, self._universe.atoms.positions)) + + temp_u.add_angles(self.guess_angles(temp_u.atoms.bonds)) + + angles = temp_u.atoms.angles + + dihedrals_found = set() + + for b in angles: + a_tup = tuple([a.index for a in b]) # angle as tuple of numbers + # if searching with b[0], want tuple of (b[2], b[1], b[0], +new) + # search the first and last atom of each angle + for atom, prefix in zip([b.atoms[0], b.atoms[-1]], + [a_tup[::-1], a_tup]): + for other_b in atom.bonds: + if not other_b.partner(atom) in b: + third_a = other_b.partner(atom) + desc = prefix + (third_a.index,) + if desc[0] > desc[-1]: + desc = desc[::-1] + dihedrals_found.add(desc) + + return tuple(dihedrals_found) + + def guess_improper_dihedrals(self, angles=None): + """Given a list of Angles, find all improper dihedrals + that exist between atoms. + + Works by assuming that if (1,2,3) is an angle, and 2 & 4 are bonded, + then (2, 1, 3, 4) must be an improper dihedral. + ie the improper dihedral is the angle between the planes formed by + (1, 2, 3) and (1, 3, 4) + + Returns + ------- + List of tuples defining the improper dihedrals. + Suitable for use in u._topology + + """ + + from ..core.universe import Universe + + if angles is None: + if hasattr(self._universe.atoms, 'angles'): + angles = self._universe.atoms.angles + + else: + temp_u = Universe.empty(n_atoms=len(self._universe.atoms)) + + temp_u.add_bonds(self.guess_bonds( + self._universe.atoms, self._universe.atoms.positions)) + + temp_u.add_angles(self.guess_angles(temp_u.atoms.bonds)) + + angles = temp_u.atoms.angles + + dihedrals_found = set() + + for b in angles: + atom = b[1] # select middle atom in angle + # start of improper tuple + a_tup = tuple([b[a].index for a in [1, 2, 0]]) + # if searching with b[1], want tuple of (b[1], b[2], b[0], +new) + # search the first and last atom of each angle + for other_b in atom.bonds: + other_atom = other_b.partner(atom) + # if this atom isn't in the angle I started with + if other_atom not in b: + desc = a_tup + (other_atom.index,) + if desc[0] > desc[-1]: + desc = desc[::-1] + dihedrals_found.add(desc) + + return tuple(dihedrals_found) + + def guess_atom_charge(self, atoms): + """Guess atom charge from the name. + + .. Warning:: Not implemented; simply returns 0. + """ + # TODO: do something slightly smarter, at least use name/element + return 0.0 + + def guess_aromaticities(self, atomgroup=None): + """Guess aromaticity of atoms using RDKit + + Returns + ------- + aromaticities : numpy.ndarray + Array of boolean values for the aromaticity of each atom + + """ + if atomgroup is None: + atomgroup = self._universe.atoms + + mol = atomgroup.convert_to("RDKIT") + return np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) + + def guess_gasteiger_charges(self, atomgroup): + """Guess Gasteiger partial charges using RDKit + + Parameters + ---------- + atomgroup : mda.core.groups.AtomGroup + Atoms for which the charges will be guessed + + Returns + ------- + charges : numpy.ndarray + Array of float values representing the charge of each atom + + """ + + mol = atomgroup.convert_to("RDKIT") + from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges + ComputeGasteigerCharges(mol, throwOnParamFailure=True) + return np.array([atom.GetDoubleProp("_GasteigerCharge") + for atom in mol.GetAtoms()], + dtype=np.float32) diff --git a/package/MDAnalysis/topology/tables.py b/package/MDAnalysis/guesser/tables.py similarity index 99% rename from package/MDAnalysis/topology/tables.py rename to package/MDAnalysis/guesser/tables.py index ea46421cc42..5e373616b7e 100644 --- a/package/MDAnalysis/topology/tables.py +++ b/package/MDAnalysis/guesser/tables.py @@ -200,7 +200,7 @@ def kv2dict(s, convertor: Any = str): Bk 247 Be 9.012182 Bi 208.98037 -Bh 262 +Bh 264 B 10.811 BR 79.90400 Cd 112.411 diff --git a/package/MDAnalysis/topology/CRDParser.py b/package/MDAnalysis/topology/CRDParser.py index 05dbe9d4298..5e4406732f3 100644 --- a/package/MDAnalysis/topology/CRDParser.py +++ b/package/MDAnalysis/topology/CRDParser.py @@ -28,8 +28,7 @@ Read a list of atoms from a CHARMM CARD coordinate file (CRD_) to build a basic topology. Reads atom ids (ATOMNO), atom names (TYPES), resids (RESID), residue numbers (RESNO), residue names (RESNames), segment ids -(SEGID) and tempfactor (Weighting). Atom element and mass are guessed based on -the name of the atom. +(SEGID) and tempfactor (Weighting). Residues are detected through a change is either resid or resname while segments are detected according to changes in segid. @@ -49,13 +48,10 @@ from ..lib.util import openany, FORTRANReader from .base import TopologyReaderBase, change_squash -from . import guessers from ..core.topology import Topology from ..core.topologyattrs import ( Atomids, Atomnames, - Atomtypes, - Masses, Resids, Resnames, Resnums, @@ -76,9 +72,9 @@ class CRDParser(TopologyReaderBase): - Resnums - Segids - Guesses the following attributes: - - Atomtypes - - Masses + .. versionchanged:: 2.8.0 + Type and mass are not longer guessed here. Until 3.0 these will still be + set by default through through universe.guess_TopologyAttrs() API. """ format = 'CRD' @@ -141,10 +137,6 @@ def parse(self, **kwargs): resnums = np.array(resnums, dtype=np.int32) segids = np.array(segids, dtype=object) - # Guess some attributes - atomtypes = guessers.guess_types(atomnames) - masses = guessers.guess_masses(atomtypes) - atom_residx, (res_resids, res_resnames, res_resnums, res_segids) = change_squash( (resids, resnames), (resids, resnames, resnums, segids)) res_segidx, (seg_segids,) = change_squash( @@ -154,8 +146,6 @@ def parse(self, **kwargs): attrs=[ Atomids(atomids), Atomnames(atomnames), - Atomtypes(atomtypes, guessed=True), - Masses(masses, guessed=True), Tempfactors(tempfactors), Resids(res_resids), Resnames(res_resnames), diff --git a/package/MDAnalysis/topology/DLPolyParser.py b/package/MDAnalysis/topology/DLPolyParser.py index 489e6675bde..b85a0d188cc 100644 --- a/package/MDAnalysis/topology/DLPolyParser.py +++ b/package/MDAnalysis/topology/DLPolyParser.py @@ -29,9 +29,6 @@ DLPoly files have the following Attributes: - Atomnames - Atomids -Guesses the following attributes: - - Atomtypes - - Masses .. _Poly: http://www.stfc.ac.uk/SCD/research/app/ccg/software/DL_POLY/44516.aspx @@ -46,14 +43,11 @@ """ import numpy as np -from . import guessers from .base import TopologyReaderBase from ..core.topology import Topology from ..core.topologyattrs import ( Atomids, Atomnames, - Atomtypes, - Masses, Resids, Resnums, Segids, @@ -65,6 +59,9 @@ class ConfigParser(TopologyReaderBase): """DL_Poly CONFIG file parser .. versionadded:: 0.10.1 + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). """ format = 'CONFIG' @@ -109,14 +106,9 @@ def parse(self, **kwargs): else: ids = np.arange(n_atoms) - atomtypes = guessers.guess_types(names) - masses = guessers.guess_masses(atomtypes) - attrs = [ Atomnames(names), Atomids(ids), - Atomtypes(atomtypes, guessed=True), - Masses(masses, guessed=True), Resids(np.array([1])), Resnums(np.array([1])), Segids(np.array(['SYSTEM'], dtype=object)), @@ -176,14 +168,9 @@ def parse(self, **kwargs): else: ids = np.arange(n_atoms) - atomtypes = guessers.guess_types(names) - masses = guessers.guess_masses(atomtypes) - attrs = [ Atomnames(names), Atomids(ids), - Atomtypes(atomtypes, guessed=True), - Masses(masses, guessed=True), Resids(np.array([1])), Resnums(np.array([1])), Segids(np.array(['SYSTEM'], dtype=object)), diff --git a/package/MDAnalysis/topology/DMSParser.py b/package/MDAnalysis/topology/DMSParser.py index 1240c9b7574..f165272fc26 100644 --- a/package/MDAnalysis/topology/DMSParser.py +++ b/package/MDAnalysis/topology/DMSParser.py @@ -44,7 +44,6 @@ import sqlite3 import os -from . import guessers from .base import TopologyReaderBase, change_squash from ..core.topology import Topology from ..core.topologyattrs import ( @@ -53,7 +52,6 @@ Bonds, Charges, ChainIDs, - Atomtypes, Masses, Resids, Resnums, @@ -87,12 +85,14 @@ class DMSParser(TopologyReaderBase): - Resids Segment: - Segids - Guesses the following attributes - - Atomtypes .. _DESRES: http://www.deshawresearch.com .. _Desmond: http://www.deshawresearch.com/resources_desmond.html .. _DMS: http://www.deshawresearch.com/Desmond_Users_Guide-0.7.pdf + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = 'DMS' @@ -161,7 +161,6 @@ def dict_factory(cursor, row): attrs['bond'] = bondlist attrs['bondorder'] = bondorder - atomtypes = guessers.guess_types(attrs['name']) topattrs = [] # Bundle in Atom level objects for attr, cls in [ @@ -173,7 +172,6 @@ def dict_factory(cursor, row): ('chain', ChainIDs), ]: topattrs.append(cls(attrs[attr])) - topattrs.append(Atomtypes(atomtypes, guessed=True)) # Residues atom_residx, (res_resids, diff --git a/package/MDAnalysis/topology/ExtendedPDBParser.py b/package/MDAnalysis/topology/ExtendedPDBParser.py index 2138d386923..ec6e1e527d2 100644 --- a/package/MDAnalysis/topology/ExtendedPDBParser.py +++ b/package/MDAnalysis/topology/ExtendedPDBParser.py @@ -78,8 +78,6 @@ class ExtendedPDBParser(PDBParser.PDBParser): - bonds - formalcharges - Guesses the following Attributes: - - masses See Also -------- diff --git a/package/MDAnalysis/topology/FHIAIMSParser.py b/package/MDAnalysis/topology/FHIAIMSParser.py index 688a3e1626c..fcf95691f33 100644 --- a/package/MDAnalysis/topology/FHIAIMSParser.py +++ b/package/MDAnalysis/topology/FHIAIMSParser.py @@ -47,15 +47,12 @@ """ import numpy as np -from . import guessers from ..lib.util import openany from .base import TopologyReaderBase from ..core.topology import Topology from ..core.topologyattrs import ( Atomnames, Atomids, - Atomtypes, - Masses, Resids, Resnums, Segids, @@ -69,10 +66,9 @@ class FHIAIMSParser(TopologyReaderBase): Creates the following attributes: - Atomnames - Guesses the following attributes: - - Atomtypes - - Masses - + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). """ format = ['IN', 'FHIAIMS'] @@ -100,14 +96,8 @@ def parse(self, **kwargs): names = np.asarray(names) natoms = len(names) - # Guessing time - atomtypes = guessers.guess_types(names) - masses = guessers.guess_masses(names) - attrs = [Atomnames(names), Atomids(np.arange(natoms) + 1), - Atomtypes(atomtypes, guessed=True), - Masses(masses, guessed=True), Resids(np.array([1])), Resnums(np.array([1])), Segids(np.array(['SYSTEM'], dtype=object)), diff --git a/package/MDAnalysis/topology/GMSParser.py b/package/MDAnalysis/topology/GMSParser.py index 022fb990708..812207ed674 100644 --- a/package/MDAnalysis/topology/GMSParser.py +++ b/package/MDAnalysis/topology/GMSParser.py @@ -48,15 +48,12 @@ import re import numpy as np -from . import guessers from ..lib.util import openany from .base import TopologyReaderBase from ..core.topology import Topology from ..core.topologyattrs import ( Atomids, Atomnames, - Atomtypes, - Masses, Resids, Resnums, Segids, @@ -75,11 +72,12 @@ class GMSParser(TopologyReaderBase): Creates the following Attributes: - names - atomic charges - Guesses: - - types - - masses .. versionadded:: 0.9.1 + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = 'GMS' @@ -112,15 +110,11 @@ def parse(self, **kwargs): at_charges.append(at_charge) #TODO: may be use coordinates info from _m.group(3-5) ?? - atomtypes = guessers.guess_types(names) - masses = guessers.guess_masses(atomtypes) n_atoms = len(names) attrs = [ Atomids(np.arange(n_atoms) + 1), Atomnames(np.array(names, dtype=object)), AtomicCharges(np.array(at_charges, dtype=np.int32)), - Atomtypes(atomtypes, guessed=True), - Masses(masses, guessed=True), Resids(np.array([1])), Resnums(np.array([1])), Segids(np.array(['SYSTEM'], dtype=object)), diff --git a/package/MDAnalysis/topology/GROParser.py b/package/MDAnalysis/topology/GROParser.py index 23fb0af5655..6bcaec24cb5 100644 --- a/package/MDAnalysis/topology/GROParser.py +++ b/package/MDAnalysis/topology/GROParser.py @@ -28,7 +28,6 @@ Read a list of atoms from a GROMOS/Gromacs GRO coordinate file to build a basic topology. -Atom types and masses are guessed. See Also -------- @@ -49,9 +48,7 @@ from ..lib.util import openany from ..core.topologyattrs import ( Atomnames, - Atomtypes, Atomids, - Masses, Resids, Resnames, Resnums, @@ -59,7 +56,6 @@ ) from ..core.topology import Topology from .base import TopologyReaderBase, change_squash -from . import guessers class GROParser(TopologyReaderBase): @@ -71,9 +67,10 @@ class GROParser(TopologyReaderBase): - atomids - atomnames - Guesses the following attributes - - atomtypes - - masses + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = 'GRO' @@ -129,9 +126,6 @@ def parse(self, **kwargs): for s in starts: resids[s:] += 100000 - # Guess types and masses - atomtypes = guessers.guess_types(names) - masses = guessers.guess_masses(atomtypes) residx, (new_resids, new_resnames) = change_squash( (resids, resnames), (resids, resnames)) @@ -141,11 +135,9 @@ def parse(self, **kwargs): attrs = [ Atomnames(names), Atomids(indices), - Atomtypes(atomtypes, guessed=True), Resids(new_resids), Resnums(new_resids.copy()), Resnames(new_resnames), - Masses(masses, guessed=True), Segids(np.array(['SYSTEM'], dtype=object)) ] diff --git a/package/MDAnalysis/topology/GSDParser.py b/package/MDAnalysis/topology/GSDParser.py index f1ae72d287d..bd62d0f5f98 100644 --- a/package/MDAnalysis/topology/GSDParser.py +++ b/package/MDAnalysis/topology/GSDParser.py @@ -54,7 +54,6 @@ import os import numpy as np -from . import guessers from .base import TopologyReaderBase from ..core.topology import Topology from ..core.topologyattrs import ( diff --git a/package/MDAnalysis/topology/HoomdXMLParser.py b/package/MDAnalysis/topology/HoomdXMLParser.py index b8e7baa0613..f2d1cea9526 100644 --- a/package/MDAnalysis/topology/HoomdXMLParser.py +++ b/package/MDAnalysis/topology/HoomdXMLParser.py @@ -49,7 +49,6 @@ import xml.etree.ElementTree as ET import numpy as np -from . import guessers from ..lib.util import openany from .base import TopologyReaderBase from ..core.topology import Topology diff --git a/package/MDAnalysis/topology/ITPParser.py b/package/MDAnalysis/topology/ITPParser.py index 932430c0a45..d8552160278 100644 --- a/package/MDAnalysis/topology/ITPParser.py +++ b/package/MDAnalysis/topology/ITPParser.py @@ -1,5 +1,5 @@ # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 # # MDAnalysis --- https://www.mdanalysis.org # Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors @@ -27,7 +27,8 @@ Reads a GROMACS ITP_ or TOP_ file to build the system. The topology will contain atom IDs, segids, residue IDs, residue names, atom names, atom types, -charges, chargegroups, masses (guessed if not found), moltypes, and molnums. +charges, chargegroups, masses, moltypes, and molnums. +Any masses that are in the file will be read; any missing values will be guessed. Bonds, angles, dihedrals and impropers are also read from the file. If an ITP file is passed without a ``[ molecules ]`` directive, passing @@ -106,7 +107,7 @@ u = mda.Universe(ITP_tip5p, EXTRA_ATOMS=True, HW1_CHARGE=3, HW2_CHARGE=3) -These keyword variables are **case-sensitive**. Note that if you set keywords to +These keyword variables are **case-sensitive**. Note that if you set keywords to ``False`` or ``None``, they will be treated as if they are not defined in #ifdef conditions. For example, the universe below will only have 5 atoms. :: @@ -132,8 +133,10 @@ import logging import numpy as np +import warnings from ..lib.util import openany -from . import guessers +from ..guesser.tables import SYMB2Z +from ..guesser.default_guesser import DefaultGuesser from .base import TopologyReaderBase, change_squash, reduce_singular from ..core.topologyattrs import ( Atomids, @@ -152,6 +155,7 @@ Dihedrals, Impropers, AtomAttr, + Elements, ) from ..core.topology import Topology @@ -216,7 +220,7 @@ def define(self, line): except ValueError: _, variable = line.split() value = True - + # kwargs overrides files if variable not in self._original_defines: self.defines[variable] = value @@ -252,7 +256,7 @@ def skip_until_else(self, infile): break else: raise IOError('Missing #endif in {}'.format(self.current_file)) - + def skip_until_endif(self, infile): """Skip lines until #endif""" for line in self.clean_file_lines(infile): @@ -333,9 +337,9 @@ def parse_atoms(self, line): lst.append('') def parse_bonds(self, line): - self.add_param(line, self.bonds, n_funct=2, + self.add_param(line, self.bonds, n_funct=2, funct_values=(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) - + def parse_angles(self, line): self.add_param(line, self.angles, n_funct=3, funct_values=(1, 2, 3, 4, 5, 6, 8, 10)) @@ -355,7 +359,7 @@ def parse_settles(self, line): # water molecules. # In ITP files this is defined with only the # oxygen atom index. The next two atoms are - # assumed to be hydrogens. Unlike TPRParser, + # assumed to be hydrogens. Unlike TPRParser, # the manual only lists this format (as of 2019). # These are treated as 2 bonds. # No angle component is included to avoid discrepancies @@ -471,6 +475,12 @@ class ITPParser(TopologyReaderBase): .. versionchanged:: 2.2.0 no longer adds angles for water molecules with SETTLE constraint + .. versionchanged:: 2.8.0 + Removed mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + Added guessed elements if all elements are valid to preserve partial + mass guessing behavior + """ format = 'ITP' @@ -503,7 +513,7 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', self._molecules = [] # for order self.current_mol = None self.parser = self._pass - self.system_molecules = [] + self.system_molecules = [] # Open and check itp validity with openany(self.filename) as itpfile: @@ -523,10 +533,10 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', elif self.current_mol: self.parser = self.current_mol.parsers.get(section, self._pass) - + else: self.parser = self._pass - + else: self.parser(line) @@ -575,13 +585,21 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', if not all(self.masses): empty = self.masses == '' - self.masses[empty] = guessers.guess_masses( - guessers.guess_types(self.types)[empty]) - attrs.append(Masses(np.array(self.masses, dtype=np.float64), - guessed=True)) + self.masses[empty] = Masses.missing_value_label + + attrs.append(Masses(np.array(self.masses, dtype=np.float64), + guessed=False)) + + self.elements = DefaultGuesser(None).guess_types(self.types) + if all(e.capitalize() in SYMB2Z for e in self.elements): + attrs.append(Elements(np.array(self.elements, + dtype=object), guessed=True)) + else: - attrs.append(Masses(np.array(self.masses, dtype=np.float64), - guessed=False)) + warnings.warn("Element information is missing, elements attribute " + "will not be populated. If needed these can be " + "guessed using universe.guess_TopologyAttrs(" + "to_guess=['elements']).") # residue stuff resids = np.array(self.resids, dtype=np.int32) diff --git a/package/MDAnalysis/topology/LAMMPSParser.py b/package/MDAnalysis/topology/LAMMPSParser.py index 85bf2dec8f6..62664b568bc 100644 --- a/package/MDAnalysis/topology/LAMMPSParser.py +++ b/package/MDAnalysis/topology/LAMMPSParser.py @@ -81,7 +81,6 @@ import functools import warnings -from . import guessers from ..lib.util import openany, conv_float from ..lib.mdamath import triclinic_box from .base import TopologyReaderBase, squash_by @@ -182,6 +181,10 @@ class as the topology and coordinate reader share many common see :ref:`atom_style_kwarg`. .. versionadded:: 0.9.0 + .. versionchanged:: 2.8.0 + Removed mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = 'DATA' @@ -252,9 +255,9 @@ def _interpret_atom_style(atom_style): if missing_attrs: raise ValueError("atom_style string missing required field(s): {}" "".format(', '.join(missing_attrs))) - + return style_dict - + def parse(self, **kwargs): """Parses a LAMMPS_ DATA file. @@ -300,7 +303,7 @@ def parse(self, **kwargs): type, sect = self._parse_bond_section(sects[L], nentries, mapping) except KeyError: type, sect = [], [] - + top.add_TopologyAttr(attr(sect, type)) return top @@ -323,7 +326,7 @@ def read_DATA_timestep(self, n_atoms, TS_class, TS_kwargs, self.style_dict = None else: self.style_dict = self._interpret_atom_style(atom_style) - + header, sects = self.grab_datafile() unitcell = self._parse_box(header) @@ -361,7 +364,7 @@ def _parse_pos(self, datalines): style_dict = {'id': 0, 'x': 3, 'y': 4, 'z': 5} else: style_dict = self.style_dict - + for i, line in enumerate(datalines): line = line.split() @@ -520,10 +523,6 @@ def _parse_atoms(self, datalines, massdict=None): for i, at in enumerate(types): masses[i] = massdict[at] attrs.append(Masses(masses)) - else: - # Guess them - masses = guessers.guess_masses(types) - attrs.append(Masses(masses, guessed=True)) residx, resids = squash_by(resids)[:2] n_residues = len(resids) @@ -610,7 +609,7 @@ def parse(self, **kwargs): indices = np.zeros(natoms, dtype=int) types = np.zeros(natoms, dtype=object) - + atomline = fin.readline() # ITEM ATOMS attrs = atomline.split()[2:] # attributes on coordinate line col_ids = {attr: i for i, attr in enumerate(attrs)} # column ids diff --git a/package/MDAnalysis/topology/MMTFParser.py b/package/MDAnalysis/topology/MMTFParser.py index 51bc16c8ac0..e9332e9d689 100644 --- a/package/MDAnalysis/topology/MMTFParser.py +++ b/package/MDAnalysis/topology/MMTFParser.py @@ -1,5 +1,5 @@ # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 # # MDAnalysis --- https://www.mdanalysis.org # Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors @@ -38,6 +38,9 @@ .. versionchanged:: 2.0.0 Aliased ``bfactors`` topologyattribute to ``tempfactors``. ``tempfactors`` is deprecated and will be removed in 3.0 (Issue #1901) +.. versionchanged:: 2.8.0 + Removed mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). Reads the following topology attributes: @@ -47,7 +50,6 @@ - tempfactor - bonds - charge - - masses (guessed) - name - occupancy - type @@ -76,7 +78,6 @@ from . import base -from . import guessers from ..core.topology import Topology from ..core.topologyattrs import ( AltLocs, @@ -87,7 +88,6 @@ Bonds, Charges, ICodes, - Masses, Occupancies, Resids, Resnames, @@ -188,8 +188,7 @@ def iter_atoms(field): charges = Charges(list(iter_atoms('formalChargeList'))) names = Atomnames(list(iter_atoms('atomNameList'))) types = Atomtypes(list(iter_atoms('elementList'))) - masses = Masses(guessers.guess_masses(types.values), guessed=True) - attrs.extend([charges, names, types, masses]) + attrs.extend([charges, names, types]) #optional are empty list if empty, sometimes arrays if len(mtop.atom_id_list): diff --git a/package/MDAnalysis/topology/MOL2Parser.py b/package/MDAnalysis/topology/MOL2Parser.py index 96d9dbdbd40..4345ca0efe7 100644 --- a/package/MDAnalysis/topology/MOL2Parser.py +++ b/package/MDAnalysis/topology/MOL2Parser.py @@ -1,5 +1,5 @@ # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 # # MDAnalysis --- https://www.mdanalysis.org # Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors @@ -44,7 +44,6 @@ import os import numpy as np -from . import guessers from ..lib.util import openany from .base import TopologyReaderBase, squash_by from ..core.topologyattrs import ( @@ -54,14 +53,13 @@ Bonds, Charges, Elements, - Masses, Resids, Resnums, Resnames, Segids, ) from ..core.topology import Topology -from .tables import SYBYL2SYMB +from ..guesser.tables import SYBYL2SYMB import warnings @@ -79,8 +77,6 @@ class MOL2Parser(TopologyReaderBase): - Bonds - Elements - Guesses the following: - - masses Notes ----- @@ -95,7 +91,7 @@ class MOL2Parser(TopologyReaderBase): 2. If no atoms have ``resname`` field, resnames attribute will not be set; If some atoms have ``resname`` while some do not, :exc:`ValueError` will occur. - + 3. If "NO_CHARGES" shows up in "@MOLECULE" section and no atoms have the ``charge`` field, charges attribute will not be set; If "NO_CHARGES" shows up while ``charge`` field appears, @@ -129,6 +125,10 @@ class MOL2Parser(TopologyReaderBase): Parse elements from atom types. .. versionchanged:: 2.2.0 Read MOL2 files with optional columns omitted. + .. versionchanged:: 2.8.0 + Removed mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = 'MOL2' @@ -235,15 +235,12 @@ def parse(self, **kwargs): f"atoms: {invalid_elements}. " "These have been given an empty element record.") - masses = guessers.guess_masses(validated_elements) - attrs = [] attrs.append(Atomids(np.array(ids, dtype=np.int32))) attrs.append(Atomnames(np.array(names, dtype=object))) attrs.append(Atomtypes(np.array(types, dtype=object))) if has_charges: attrs.append(Charges(np.array(charges, dtype=np.float32))) - attrs.append(Masses(masses, guessed=True)) if not np.all(validated_elements == ''): attrs.append(Elements(validated_elements)) diff --git a/package/MDAnalysis/topology/PDBParser.py b/package/MDAnalysis/topology/PDBParser.py index e1e08dd04c6..bad6d2bc6d5 100644 --- a/package/MDAnalysis/topology/PDBParser.py +++ b/package/MDAnalysis/topology/PDBParser.py @@ -39,8 +39,8 @@ .. Note:: - The parser processes atoms and their names. Masses are guessed and set to 0 - if unknown. Partial charges are not set. Elements are parsed if they are + The parser processes atoms and their names. + Partial charges are not set. Elements are parsed if they are valid. If partially missing or incorrect, empty records are assigned. See Also @@ -61,8 +61,7 @@ import numpy as np import warnings -from .guessers import guess_masses, guess_types -from .tables import SYMB2Z +from ..guesser.tables import SYMB2Z from ..lib import util from .base import TopologyReaderBase, change_squash from ..core.topology import Topology @@ -75,7 +74,6 @@ Atomtypes, Elements, ICodes, - Masses, Occupancies, RecordTypes, Resids, @@ -169,9 +167,6 @@ class PDBParser(TopologyReaderBase): - bonds - formalcharges - Guesses the following Attributes: - - masses - See Also -------- :class:`MDAnalysis.coordinates.PDB.PDBReader` @@ -197,6 +192,9 @@ class PDBParser(TopologyReaderBase): .. versionchanged:: 2.5.0 Formal charges will not be populated if an unknown entry is encountered, instead a UserWarning is emitted. + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). """ format = ['PDB', 'ENT'] @@ -322,16 +320,8 @@ def _parseatoms(self): (occupancies, Occupancies, np.float32), ): attrs.append(Attr(np.array(vals, dtype=dtype))) - # Guessed attributes - # masses from types if they exist # OPT: We do this check twice, maybe could refactor to avoid this - if not any(elements): - atomtypes = guess_types(names) - attrs.append(Atomtypes(atomtypes, guessed=True)) - warnings.warn("Element information is missing, elements attribute " - "will not be populated. If needed these can be " - "guessed using MDAnalysis.topology.guessers.") - else: + if any(elements): # Feed atomtypes as raw element column, but validate elements atomtypes = elements attrs.append(Atomtypes(np.array(elements, dtype=object))) @@ -344,10 +334,16 @@ def _parseatoms(self): wmsg = (f"Unknown element {elem} found for some atoms. " f"These have been given an empty element record. " f"If needed they can be guessed using " - f"MDAnalysis.topology.guessers.") + f"universe.guess_TopologyAttrs(context='default'," + " to_guess=['elements']).") warnings.warn(wmsg) validated_elements.append('') attrs.append(Elements(np.array(validated_elements, dtype=object))) + else: + warnings.warn("Element information is missing, elements attribute " + "will not be populated. If needed these can be" + " guessed using universe.guess_TopologyAttrs(" + "context='default', to_guess=['elements']).") if any(formalcharges): try: @@ -374,9 +370,6 @@ def _parseatoms(self): else: attrs.append(FormalCharges(np.array(formalcharges, dtype=int))) - masses = guess_masses(atomtypes) - attrs.append(Masses(masses, guessed=True)) - # Residue level stuff from here resids = np.array(resids, dtype=np.int32) resnames = np.array(resnames, dtype=object) diff --git a/package/MDAnalysis/topology/PDBQTParser.py b/package/MDAnalysis/topology/PDBQTParser.py index a05ca35267a..97640820218 100644 --- a/package/MDAnalysis/topology/PDBQTParser.py +++ b/package/MDAnalysis/topology/PDBQTParser.py @@ -35,7 +35,7 @@ Notes ----- Only reads atoms and their names; connectivity is not -deduced. Masses are guessed and set to 0 if unknown. +deduced. See Also @@ -57,7 +57,6 @@ """ import numpy as np -from . import guessers from ..lib import util from .base import TopologyReaderBase, change_squash from ..core.topology import Topology @@ -68,7 +67,6 @@ Atomtypes, Charges, ICodes, - Masses, Occupancies, RecordTypes, Resids, @@ -97,14 +95,15 @@ class PDBQTParser(TopologyReaderBase): - tempfactors - charges - Guesses the following: - - masses .. versionchanged:: 0.18.0 Added parsing of Record types .. versionchanged:: 2.7.0 Columns 67 - 70 in ATOM records, corresponding to the field *footnote*, are now ignored. See Autodock's `reference`_. + .. versionchanged:: 2.8.0 + Removed mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). .. _reference: https://autodock.scripps.edu/wp-content/uploads/sites/56/2021/10/AutoDock4.2.6_UserGuide.pdf @@ -151,8 +150,6 @@ def parse(self, **kwargs): n_atoms = len(serials) - masses = guessers.guess_masses(atomtypes) - attrs = [] for attrlist, Attr, dtype in ( (record_types, RecordTypes, object), @@ -165,7 +162,6 @@ def parse(self, **kwargs): (atomtypes, Atomtypes, object), ): attrs.append(Attr(np.array(attrlist, dtype=dtype))) - attrs.append(Masses(masses, guessed=True)) resids = np.array(resids, dtype=np.int32) icodes = np.array(icodes, dtype=object) diff --git a/package/MDAnalysis/topology/PQRParser.py b/package/MDAnalysis/topology/PQRParser.py index e5f7b6415b4..1adcd7fba2a 100644 --- a/package/MDAnalysis/topology/PQRParser.py +++ b/package/MDAnalysis/topology/PQRParser.py @@ -49,7 +49,6 @@ """ import numpy as np -from . import guessers from ..lib.util import openany from ..core.topologyattrs import ( Atomids, @@ -57,7 +56,6 @@ Atomtypes, Charges, ICodes, - Masses, Radii, RecordTypes, Resids, @@ -82,9 +80,6 @@ class PQRParser(TopologyReaderBase): - Resnames - Segids - Guesses the following: - - atomtypes (if not present, Gromacs generated PQR files have these) - - masses .. versionchanged:: 0.9.0 Read chainID from a PQR file and use it as segid (before we always used @@ -95,6 +90,10 @@ class PQRParser(TopologyReaderBase): Added parsing of Record types Can now read PQR files from Gromacs, these provide atom type as last column but don't have segids + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = 'PQR' @@ -191,20 +190,14 @@ def parse(self, **kwargs): n_atoms = len(serials) - if not elements: - atomtypes = guessers.guess_types(names) - guessed_types = True - else: + attrs = [] + if elements: atomtypes = elements - guessed_types = False - masses = guessers.guess_masses(atomtypes) + attrs.append(Atomtypes(atomtypes, False)) - attrs = [] attrs.append(Atomids(np.array(serials, dtype=np.int32))) attrs.append(Atomnames(np.array(names, dtype=object))) attrs.append(Charges(np.array(charges, dtype=np.float32))) - attrs.append(Atomtypes(atomtypes, guessed=guessed_types)) - attrs.append(Masses(masses, guessed=True)) attrs.append(RecordTypes(np.array(record_types, dtype=object))) attrs.append(Radii(np.array(radii, dtype=np.float32))) diff --git a/package/MDAnalysis/topology/PSFParser.py b/package/MDAnalysis/topology/PSFParser.py index 1961d21c7b0..70cd38d51fa 100644 --- a/package/MDAnalysis/topology/PSFParser.py +++ b/package/MDAnalysis/topology/PSFParser.py @@ -49,7 +49,7 @@ import numpy as np from ..lib.util import openany, atoi -from . import guessers + from .base import TopologyReaderBase, squash_by, change_squash from ..core.topologyattrs import ( Atomids, diff --git a/package/MDAnalysis/topology/TOPParser.py b/package/MDAnalysis/topology/TOPParser.py index 3153357b21b..9113750cf95 100644 --- a/package/MDAnalysis/topology/TOPParser.py +++ b/package/MDAnalysis/topology/TOPParser.py @@ -75,7 +75,7 @@ As of version 2.0.0, elements are no longer guessed if ATOMIC_NUMBER records are missing. In those scenarios, if elements are necessary, users will have to invoke the element guessers after parsing the topology file. Please see - :mod:`MDAnalysis.topology.guessers` for more details. + :mod:`MDAnalysis.guessers` for more details. .. _`PARM parameter/topology file specification`: https://ambermd.org/FileFormats.php#topo.cntrl @@ -91,7 +91,7 @@ import numpy as np import itertools -from .tables import Z2SYMB +from ..guesser.tables import Z2SYMB from ..lib.util import openany, FORTRANReader from .base import TopologyReaderBase, change_squash from ..core.topology import Topology @@ -294,14 +294,15 @@ def next_getter(): if 'elements' not in attrs: msg = ("ATOMIC_NUMBER record not found, elements attribute will " "not be populated. If needed these can be guessed using " - "MDAnalysis.topology.guessers.") + "universe.guess_TopologyAttrs(to_guess=['elements']).") logger.warning(msg) warnings.warn(msg) elif np.any(attrs['elements'].values == ""): # only send out one warning that some elements are unknown msg = ("Unknown ATOMIC_NUMBER value found for some atoms, these " "have been given an empty element record. If needed these " - "can be guessed using MDAnalysis.topology.guessers.") + "can be guessed using " + "universe.guess_TopologyAttrs(to_guess=['elements']).") logger.warning(msg) warnings.warn(msg) diff --git a/package/MDAnalysis/topology/TPRParser.py b/package/MDAnalysis/topology/TPRParser.py index d6f3717bf1a..396211d071f 100644 --- a/package/MDAnalysis/topology/TPRParser.py +++ b/package/MDAnalysis/topology/TPRParser.py @@ -166,7 +166,6 @@ __copyright__ = "GNU Public Licence, v2" -from . import guessers from ..lib.util import openany from .tpr import utils as tpr_utils from .tpr import setting as S diff --git a/package/MDAnalysis/topology/TXYZParser.py b/package/MDAnalysis/topology/TXYZParser.py index 56e6cc59b7c..0781488c9dc 100644 --- a/package/MDAnalysis/topology/TXYZParser.py +++ b/package/MDAnalysis/topology/TXYZParser.py @@ -47,17 +47,16 @@ import numpy as np import warnings -from . import guessers -from .tables import SYMB2Z +from ..guesser.tables import SYMB2Z from ..lib.util import openany from .base import TopologyReaderBase from ..core.topology import Topology +from ..guesser.tables import SYMB2Z from ..core.topologyattrs import ( Atomnames, Atomids, Atomtypes, Bonds, - Masses, Resids, Resnums, Segids, @@ -77,6 +76,10 @@ class TXYZParser(TopologyReaderBase): .. versionadded:: 0.17.0 .. versionchanged:: 2.4.0 Adding the `Element` attribute if all names are valid element symbols. + .. versionchanged:: 2.8.0 + Removed mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = ['TXYZ', 'ARC'] @@ -95,6 +98,7 @@ def parse(self, **kwargs): names = np.zeros(natoms, dtype=object) types = np.zeros(natoms, dtype=object) bonds = [] + # Find first atom line, maybe there's box information fline = inf.readline() try: @@ -120,26 +124,23 @@ def parse(self, **kwargs): if i < other_atom: bonds.append((i, other_atom)) - # Guessing time - masses = guessers.guess_masses(names) - attrs = [Atomnames(names), Atomids(atomids), Atomtypes(types), Bonds(tuple(bonds)), - Masses(masses, guessed=True), Resids(np.array([1])), Resnums(np.array([1])), Segids(np.array(['SYSTEM'], dtype=object)), ] if all(n.capitalize() in SYMB2Z for n in names): attrs.append(Elements(np.array(names, dtype=object))) - + else: warnings.warn("Element information is missing, elements attribute " "will not be populated. If needed these can be " - "guessed using MDAnalysis.topology.guessers.") - + "guessed using universe.guess_TopologyAttrs(" + "to_guess=['elements']).") + top = Topology(natoms, 1, 1, attrs=attrs) diff --git a/package/MDAnalysis/topology/XYZParser.py b/package/MDAnalysis/topology/XYZParser.py index 4162e343517..cb0df129e08 100644 --- a/package/MDAnalysis/topology/XYZParser.py +++ b/package/MDAnalysis/topology/XYZParser.py @@ -1,5 +1,5 @@ # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 # # MDAnalysis --- https://www.mdanalysis.org # Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors @@ -40,15 +40,12 @@ """ import numpy as np -from . import guessers from ..lib.util import openany from .base import TopologyReaderBase from ..core.topology import Topology from ..core.topologyattrs import ( Atomnames, Atomids, - Atomtypes, - Masses, Resids, Resnums, Segids, @@ -62,14 +59,15 @@ class XYZParser(TopologyReaderBase): Creates the following attributes: - Atomnames - Guesses the following attributes: - - Atomtypes - - Masses .. versionadded:: 0.9.1 .. versionchanged: 1.0.0 Store elements attribute, based on XYZ atom names + .. versionchanged:: 2.8.0 + Removed type and mass guessing (attributes guessing takes place now + through universe.guess_TopologyAttrs() API). + """ format = 'XYZ' @@ -91,14 +89,9 @@ def parse(self, **kwargs): name = inf.readline().split()[0] names[i] = name - # Guessing time - atomtypes = guessers.guess_types(names) - masses = guessers.guess_masses(names) attrs = [Atomnames(names), Atomids(np.arange(natoms) + 1), - Atomtypes(atomtypes, guessed=True), - Masses(masses, guessed=True), Resids(np.array([1])), Resnums(np.array([1])), Segids(np.array(['SYSTEM'], dtype=object)), diff --git a/package/MDAnalysis/topology/__init__.py b/package/MDAnalysis/topology/__init__.py index 32df510f47e..b1b756b5386 100644 --- a/package/MDAnalysis/topology/__init__.py +++ b/package/MDAnalysis/topology/__init__.py @@ -33,9 +33,8 @@ As a minimum, all topology parsers will provide atom ids, atom types, masses, resids, resnums and segids as well as assigning all atoms to residues and all residues to segments. For systems without residues and segments, this results -in there being a single residue and segment to which all atoms belong. Often -when data is not provided by a file, it will be guessed based on other data in -the file. In the event that this happens, a UserWarning will always be issued. +in there being a single residue and segment to which all atoms belong. +In the event that this happens, a UserWarning will always be issued. The following table lists the currently supported topology formats along with the attributes they provide. @@ -134,7 +133,7 @@ :mod:`MDAnalysis.topology.XYZParser` TXYZ [#a]_ txyz, names, atomids, Tinker_ XYZ File Parser. Reads atom labels, numbers - arc masses, types, and connectivity; masses are guessed from atoms names. + arc masses, types, and connectivity. bonds :mod:`MDAnalysis.topology.TXYZParser` GAMESS [#a]_ gms, names, GAMESS_ output parser. Read only atoms of assembly diff --git a/package/MDAnalysis/topology/core.py b/package/MDAnalysis/topology/core.py index 91596a0fefc..3ed1c7a3461 100644 --- a/package/MDAnalysis/topology/core.py +++ b/package/MDAnalysis/topology/core.py @@ -26,11 +26,6 @@ The various topology parsers make use of functions and classes in this module. They are mostly of use to developers. -See Also --------- -:mod:`MDAnalysis.topology.tables` - for some hard-coded atom information that is used by functions such as - :func:`guess_atom_type` and :func:`guess_atom_mass`. """ @@ -40,12 +35,6 @@ from collections import defaultdict # Local imports -from . import tables -from .guessers import ( - guess_atom_element, guess_atom_type, - get_atom_mass, guess_atom_mass, guess_atom_charge, - guess_bonds, guess_angles, guess_dihedrals, guess_improper_dihedrals, -) from ..core._get_readers import get_parser_for from ..lib.util import cached diff --git a/package/MDAnalysis/topology/guessers.py b/package/MDAnalysis/topology/guessers.py deleted file mode 100644 index a0847036de8..00000000000 --- a/package/MDAnalysis/topology/guessers.py +++ /dev/null @@ -1,526 +0,0 @@ -# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# 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 -# -""" -Guessing unknown Topology information --- :mod:`MDAnalysis.topology.guessers` -============================================================================= - -In general `guess_atom_X` returns the guessed value for a single value, -while `guess_Xs` will work on an array of many atoms. - - -Example uses of guessers ------------------------- - -Guessing elements from atom names -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Currently, it is possible to guess elements from atom names using -:func:`guess_atom_element` (or the synonymous :func:`guess_atom_type`). This can -be done in the following manner:: - - import MDAnalysis as mda - from MDAnalysis.topology.guessers import guess_atom_element - from MDAnalysisTests.datafiles import PRM7 - - u = mda.Universe(PRM7) - - print(u.atoms.names[1]) # returns the atom name H1 - - element = guess_atom_element(u.atoms.names[1]) - - print(element) # returns element H - -In the above example, we take an atom named H1 and use -:func:`guess_atom_element` to guess the element hydrogen (i.e. H). It is -important to note that element guessing is not always accurate. Indeed in cases -where the atom type is not recognised, we may end up with the wrong element. -For example:: - - import MDAnalysis as mda - from MDAnalysis.topology.guessers import guess_atom_element - from MDAnalysisTests.datafiles import PRM19SBOPC - - u = mda.Universe(PRM19SBOPC) - - print(u.atoms.names[-1]) # returns the atom name EPW - - element = guess_atom_element(u.atoms.names[-1]) - - print(element) # returns element P - -Here we find that virtual site atom 'EPW' was given the element P, which -would not be an expected result. We therefore always recommend that users -carefully check the outcomes of any guessers. - -In some cases, one may want to guess elements for an entire universe and add -this guess as a topology attribute. This can be done using :func:`guess_types` -in the following manner:: - - import MDAnalysis as mda - from MDAnalysis.topology.guessers import guess_types - from MDAnalysisTests.datafiles import PRM7 - - u = mda.Universe(PRM7) - - guessed_elements = guess_types(u.atoms.names) - - u.add_TopologyAttr('elements', guessed_elements) - - print(u.atoms.elements) # returns an array of guessed elements - -More information on adding topology attributes can found in the `user guide`_. - - -.. Links - -.. _user guide: https://www.mdanalysis.org/UserGuide/examples/constructing_universe.html#Adding-topology-attributes - -""" -import numpy as np -import warnings -import re - -from ..lib import distances -from . import tables - - -def guess_masses(atom_types): - """Guess the mass of many atoms based upon their type - - Parameters - ---------- - atom_types - Type of each atom - - Returns - ------- - atom_masses : np.ndarray dtype float64 - """ - validate_atom_types(atom_types) - masses = np.array([get_atom_mass(atom_t) for atom_t in atom_types], dtype=np.float64) - return masses - - -def validate_atom_types(atom_types): - """Vaildates the atom types based on whether they are available in our tables - - Parameters - ---------- - atom_types - Type of each atom - - Returns - ------- - None - - .. versionchanged:: 0.20.0 - Try uppercase atom type name as well - """ - for atom_type in np.unique(atom_types): - try: - tables.masses[atom_type] - except KeyError: - try: - tables.masses[atom_type.upper()] - except KeyError: - warnings.warn("Failed to guess the mass for the following atom types: {}".format(atom_type)) - - -def guess_types(atom_names): - """Guess the atom type of many atoms based on atom name - - Parameters - ---------- - atom_names - Name of each atom - - Returns - ------- - atom_types : np.ndarray dtype object - """ - return np.array([guess_atom_element(name) for name in atom_names], dtype=object) - - -def guess_atom_type(atomname): - """Guess atom type from the name. - - At the moment, this function simply returns the element, as - guessed by :func:`guess_atom_element`. - - - See Also - -------- - :func:`guess_atom_element` - :mod:`MDAnalysis.topology.tables` - - - """ - return guess_atom_element(atomname) - - -NUMBERS = re.compile(r'[0-9]') # match numbers -SYMBOLS = re.compile(r'[*+-]') # match *, +, - - -def guess_atom_element(atomname): - """Guess the element of the atom from the name. - - Looks in dict to see if element is found, otherwise it uses the first - character in the atomname. The table comes from CHARMM and AMBER atom - types, where the first character is not sufficient to determine the atom - type. Some GROMOS ions have also been added. - - .. Warning: The translation table is incomplete. This will probably result - in some mistakes, but it still better than nothing! - - See Also - -------- - :func:`guess_atom_type` - :mod:`MDAnalysis.topology.tables` - """ - if atomname == '': - return '' - try: - return tables.atomelements[atomname.upper()] - except KeyError: - # strip symbols - no_symbols = re.sub(SYMBOLS, '', atomname) - - # split name by numbers - no_numbers = re.split(NUMBERS, no_symbols) - no_numbers = list(filter(None, no_numbers)) #remove '' - # if no_numbers is not empty, use the first element of no_numbers - name = no_numbers[0].upper() if no_numbers else '' - - # just in case - if name in tables.atomelements: - return tables.atomelements[name] - - while name: - if name in tables.elements: - return name - if name[:-1] in tables.elements: - return name[:-1] - if name[1:] in tables.elements: - return name[1:] - if len(name) <= 2: - return name[0] - name = name[:-1] # probably element is on left not right - - # if it's numbers - return no_symbols - - -def guess_bonds(atoms, coords, box=None, **kwargs): - r"""Guess if bonds exist between two atoms based on their distance. - - Bond between two atoms is created, if the two atoms are within - - .. math:: - - d < f \cdot (R_1 + R_2) - - of each other, where :math:`R_1` and :math:`R_2` are the VdW radii - of the atoms and :math:`f` is an ad-hoc *fudge_factor*. This is - the `same algorithm that VMD uses`_. - - Parameters - ---------- - atoms : AtomGroup - atoms for which bonds should be guessed - coords : array - coordinates of the atoms (i.e., `AtomGroup.positions)`) - fudge_factor : float, optional - The factor by which atoms must overlap eachother to be considered a - bond. Larger values will increase the number of bonds found. [0.55] - vdwradii : dict, optional - To supply custom vdwradii for atoms in the algorithm. Must be a dict - of format {type:radii}. The default table of van der Waals radii is - hard-coded as :data:`MDAnalysis.topology.tables.vdwradii`. Any user - defined vdwradii passed as an argument will supercede the table - values. [``None``] - lower_bound : float, optional - The minimum bond length. All bonds found shorter than this length will - be ignored. This is useful for parsing PDB with altloc records where - atoms with altloc A and B maybe very close together and there should be - no chemical bond between them. [0.1] - box : array_like, optional - Bonds are found using a distance search, if unit cell information is - given, periodic boundary conditions will be considered in the distance - search. [``None``] - - Returns - ------- - list - List of tuples suitable for use in Universe topology building. - - Warnings - -------- - No check is done after the bonds are guessed to see if Lewis - structure is correct. This is wrong and will burn somebody. - - Raises - ------ - :exc:`ValueError` if inputs are malformed or `vdwradii` data is missing. - - - .. _`same algorithm that VMD uses`: - http://www.ks.uiuc.edu/Research/vmd/vmd-1.9.1/ug/node26.html - - .. versionadded:: 0.7.7 - .. versionchanged:: 0.9.0 - Updated method internally to use more :mod:`numpy`, should work - faster. Should also use less memory, previously scaled as - :math:`O(n^2)`. *vdwradii* argument now augments table list - rather than replacing entirely. - """ - # why not just use atom.positions? - if len(atoms) != len(coords): - raise ValueError("'atoms' and 'coord' must be the same length") - - fudge_factor = kwargs.get('fudge_factor', 0.55) - - vdwradii = tables.vdwradii.copy() # so I don't permanently change it - user_vdwradii = kwargs.get('vdwradii', None) - if user_vdwradii: # this should make algo use their values over defaults - vdwradii.update(user_vdwradii) - - # Try using types, then elements - atomtypes = atoms.types - - # check that all types have a defined vdw - if not all(val in vdwradii for val in set(atomtypes)): - raise ValueError(("vdw radii for types: " + - ", ".join([t for t in set(atomtypes) if - not t in vdwradii]) + - ". These can be defined manually using the" + - " keyword 'vdwradii'")) - - lower_bound = kwargs.get('lower_bound', 0.1) - - if box is not None: - box = np.asarray(box) - - # to speed up checking, calculate what the largest possible bond - # atom that would warrant attention. - # then use this to quickly mask distance results later - max_vdw = max([vdwradii[t] for t in atomtypes]) - - bonds = [] - - pairs, dist = distances.self_capped_distance(coords, - max_cutoff=2.0*max_vdw, - min_cutoff=lower_bound, - box=box) - for idx, (i, j) in enumerate(pairs): - d = (vdwradii[atomtypes[i]] + vdwradii[atomtypes[j]])*fudge_factor - if (dist[idx] < d): - bonds.append((atoms[i].index, atoms[j].index)) - return tuple(bonds) - - -def guess_angles(bonds): - """Given a list of Bonds, find all angles that exist between atoms. - - Works by assuming that if atoms 1 & 2 are bonded, and 2 & 3 are bonded, - then (1,2,3) must be an angle. - - Returns - ------- - list of tuples - List of tuples defining the angles. - Suitable for use in u._topology - - - See Also - -------- - :meth:`guess_bonds` - - - .. versionadded 0.9.0 - """ - angles_found = set() - - for b in bonds: - for atom in b: - other_a = b.partner(atom) # who's my friend currently in Bond - for other_b in atom.bonds: - if other_b != b: # if not the same bond I start as - third_a = other_b.partner(atom) - desc = tuple([other_a.index, atom.index, third_a.index]) - if desc[0] > desc[-1]: # first index always less than last - desc = desc[::-1] - angles_found.add(desc) - - return tuple(angles_found) - - -def guess_dihedrals(angles): - """Given a list of Angles, find all dihedrals that exist between atoms. - - Works by assuming that if (1,2,3) is an angle, and 3 & 4 are bonded, - then (1,2,3,4) must be a dihedral. - - Returns - ------- - list of tuples - List of tuples defining the dihedrals. - Suitable for use in u._topology - - .. versionadded 0.9.0 - """ - dihedrals_found = set() - - for b in angles: - a_tup = tuple([a.index for a in b]) # angle as tuple of numbers - # if searching with b[0], want tuple of (b[2], b[1], b[0], +new) - # search the first and last atom of each angle - for atom, prefix in zip([b.atoms[0], b.atoms[-1]], - [a_tup[::-1], a_tup]): - for other_b in atom.bonds: - if not other_b.partner(atom) in b: - third_a = other_b.partner(atom) - desc = prefix + (third_a.index,) - if desc[0] > desc[-1]: - desc = desc[::-1] - dihedrals_found.add(desc) - - return tuple(dihedrals_found) - - -def guess_improper_dihedrals(angles): - """Given a list of Angles, find all improper dihedrals that exist between - atoms. - - Works by assuming that if (1,2,3) is an angle, and 2 & 4 are bonded, - then (2, 1, 3, 4) must be an improper dihedral. - ie the improper dihedral is the angle between the planes formed by - (1, 2, 3) and (1, 3, 4) - - Returns - ------- - List of tuples defining the improper dihedrals. - Suitable for use in u._topology - - .. versionadded 0.9.0 - """ - dihedrals_found = set() - - for b in angles: - atom = b[1] # select middle atom in angle - # start of improper tuple - a_tup = tuple([b[a].index for a in [1, 2, 0]]) - # if searching with b[1], want tuple of (b[1], b[2], b[0], +new) - # search the first and last atom of each angle - for other_b in atom.bonds: - other_atom = other_b.partner(atom) - # if this atom isn't in the angle I started with - if not other_atom in b: - desc = a_tup + (other_atom.index,) - if desc[0] > desc[-1]: - desc = desc[::-1] - dihedrals_found.add(desc) - - return tuple(dihedrals_found) - - -def get_atom_mass(element): - """Return the atomic mass in u for *element*. - - Masses are looked up in :data:`MDAnalysis.topology.tables.masses`. - - .. Warning:: Unknown masses are set to 0.0 - - .. versionchanged:: 0.20.0 - Try uppercase atom type name as well - """ - try: - return tables.masses[element] - except KeyError: - try: - return tables.masses[element.upper()] - except KeyError: - return 0.0 - - -def guess_atom_mass(atomname): - """Guess a mass based on the atom name. - - :func:`guess_atom_element` is used to determine the kind of atom. - - .. warning:: Anything not recognized is simply set to 0; if you rely on the - masses you might want to double check. - """ - return get_atom_mass(guess_atom_element(atomname)) - - -def guess_atom_charge(atomname): - """Guess atom charge from the name. - - .. Warning:: Not implemented; simply returns 0. - """ - # TODO: do something slightly smarter, at least use name/element - return 0.0 - - -def guess_aromaticities(atomgroup): - """Guess aromaticity of atoms using RDKit - - Parameters - ---------- - atomgroup : mda.core.groups.AtomGroup - Atoms for which the aromaticity will be guessed - - Returns - ------- - aromaticities : numpy.ndarray - Array of boolean values for the aromaticity of each atom - - - .. versionadded:: 2.0.0 - """ - mol = atomgroup.convert_to("RDKIT") - return np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) - - -def guess_gasteiger_charges(atomgroup): - """Guess Gasteiger partial charges using RDKit - - Parameters - ---------- - atomgroup : mda.core.groups.AtomGroup - Atoms for which the charges will be guessed - - Returns - ------- - charges : numpy.ndarray - Array of float values representing the charge of each atom - - - .. versionadded:: 2.0.0 - """ - mol = atomgroup.convert_to("RDKIT") - from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges - ComputeGasteigerCharges(mol, throwOnParamFailure=True) - return np.array([atom.GetDoubleProp("_GasteigerCharge") - for atom in mol.GetAtoms()], - dtype=np.float32) diff --git a/package/MDAnalysis/topology/tpr/obj.py b/package/MDAnalysis/topology/tpr/obj.py index 0524d77c6ec..5f5040c7db8 100644 --- a/package/MDAnalysis/topology/tpr/obj.py +++ b/package/MDAnalysis/topology/tpr/obj.py @@ -32,7 +32,7 @@ """ from collections import namedtuple -from ..tables import Z2SYMB +from ...guesser.tables import Z2SYMB TpxHeader = namedtuple( "TpxHeader", [ diff --git a/package/doc/sphinx/source/documentation_pages/guesser_modules.rst b/package/doc/sphinx/source/documentation_pages/guesser_modules.rst new file mode 100644 index 00000000000..7747fdc380f --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/guesser_modules.rst @@ -0,0 +1,61 @@ +.. Contains the formatted docstrings from the guesser modules located in 'mdanalysis/package/MDAnalysis/guesser' + +************************** +Guesser modules +************************** +This module contains the context-aware guessers, which are used by the :meth:`~MDAnalysis.core.Universe.Universe.guess_TopologyAttrs` API. Context-aware guessers' main purpose +is to be tailored guesser classes that target specific file format or force field (eg. PDB file format, or Martini forcefield). +Having such guessers makes attribute guessing more accurate and reliable than having generic guessing methods that doesn't fit all topologies. + +Example uses of guessers +------------------------ + +Guessing using :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Guessing can be done through the Universe's :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` as following:: + + import MDAnalysis as mda + from MDAnalysisTests.datafiles import PDB + + u = mda.Universe(PDB) + print(hasattr(u.atoms, 'elements')) # returns False + u.guess_TopologyAttrs(to_guess=['elements']) + print(u.atoms.elements) # print ['N' 'H' 'H' ... 'NA' 'NA' 'NA'] + +In the above example, we passed ``elements`` as the attribute we want to guess, and +:meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` guess then add it as a topology +attribute to the ``AtomGroup`` of the universe. + +If the attribute already exist in the universe, passing the attribute of interest to the ``to_guess`` parameter will only fill the empty values of the attribute if any exists. +To override all the attribute values, you can pass the attribute to the ``force_guess`` parameter instead of the to_guess one as following:: + + import MDAnalysis as mda + from MDAnalysisTests.datafiles import PRM12 +  + u = mda.Universe(PRM12, context='default', to_guess=()) # types ['HW', 'OW', ..] + + u.guess_TopologyAttrs(force_guess=['types']) # types ['H', 'O', ..] + +N.B.: If you didn't pass any ``context`` to the API, it will use the :class:`~MDAnalysis.guesser.default_guesser.DefaultGuesser` + +.. rubric:: available guessers +.. toctree:: + :maxdepth: 1 + + guesser_modules/init + guesser_modules/default_guesser + + +.. rubric:: guesser core modules + +The remaining pages are primarily of interest to developers as they +contain functions and classes that are used in the implementation of +the context-specific guessers. + +.. toctree:: + :maxdepth: 1 + + guesser_modules/base + guesser_modules/tables diff --git a/package/doc/sphinx/source/documentation_pages/guesser_modules/base.rst b/package/doc/sphinx/source/documentation_pages/guesser_modules/base.rst new file mode 100644 index 00000000000..cfba20f17be --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/guesser_modules/base.rst @@ -0,0 +1 @@ +.. automodule:: MDAnalysis.guesser.base diff --git a/package/doc/sphinx/source/documentation_pages/guesser_modules/default_guesser.rst b/package/doc/sphinx/source/documentation_pages/guesser_modules/default_guesser.rst new file mode 100644 index 00000000000..a3f3f897152 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/guesser_modules/default_guesser.rst @@ -0,0 +1 @@ +.. automodule:: MDAnalysis.guesser.default_guesser diff --git a/package/doc/sphinx/source/documentation_pages/guesser_modules/init.rst b/package/doc/sphinx/source/documentation_pages/guesser_modules/init.rst new file mode 100644 index 00000000000..6fa6449c5c3 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/guesser_modules/init.rst @@ -0,0 +1 @@ +.. automodule:: MDAnalysis.guesser.__init__ diff --git a/package/doc/sphinx/source/documentation_pages/guesser_modules/tables.rst b/package/doc/sphinx/source/documentation_pages/guesser_modules/tables.rst new file mode 100644 index 00000000000..6116739fe89 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/guesser_modules/tables.rst @@ -0,0 +1 @@ +.. automodule:: MDAnalysis.guesser.tables diff --git a/package/doc/sphinx/source/documentation_pages/topology/guessers.rst b/package/doc/sphinx/source/documentation_pages/topology/guessers.rst deleted file mode 100644 index e6449f5ddc8..00000000000 --- a/package/doc/sphinx/source/documentation_pages/topology/guessers.rst +++ /dev/null @@ -1,2 +0,0 @@ -.. automodule:: MDAnalysis.topology.guessers - :members: diff --git a/package/doc/sphinx/source/documentation_pages/topology/tables.rst b/package/doc/sphinx/source/documentation_pages/topology/tables.rst deleted file mode 100644 index f4d579ec9c8..00000000000 --- a/package/doc/sphinx/source/documentation_pages/topology/tables.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: MDAnalysis.topology.tables diff --git a/package/doc/sphinx/source/documentation_pages/topology_modules.rst b/package/doc/sphinx/source/documentation_pages/topology_modules.rst index ed8caba8ce6..01f3ab32e27 100644 --- a/package/doc/sphinx/source/documentation_pages/topology_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/topology_modules.rst @@ -28,10 +28,10 @@ topology file format in the *topology_format* keyword argument to topology/CRDParser topology/DLPolyParser topology/DMSParser - topology/FHIAIMSParser + topology/FHIAIMSParser topology/GMSParser topology/GROParser - topology/GSDParser + topology/GSDParser topology/HoomdXMLParser topology/ITPParser topology/LAMMPSParser @@ -59,6 +59,4 @@ the topology readers. topology/base topology/core - topology/guessers - topology/tables topology/tpr_util diff --git a/package/doc/sphinx/source/index.rst b/package/doc/sphinx/source/index.rst index e6171630c28..29e800d6a59 100644 --- a/package/doc/sphinx/source/index.rst +++ b/package/doc/sphinx/source/index.rst @@ -82,9 +82,9 @@ can be installed either with conda_ or pip_. conda ----- -First installation with conda_: +First installation with conda_: -.. code-block:: bash +.. code-block:: bash conda config --add channels conda-forge conda install mdanalysis @@ -93,7 +93,7 @@ which will automatically install a *full set of dependencies*. To upgrade later: -.. code-block:: bash +.. code-block:: bash conda update mdanalysis @@ -102,14 +102,14 @@ pip Installation with `pip`_ and a *minimal set of dependencies*: -.. code-block:: bash +.. code-block:: bash pip install --upgrade MDAnalysis To install with a *full set of dependencies* (which includes everything needed for :mod:`MDAnalysis.analysis`), add the ``[analysis]`` tag: -.. code-block:: bash +.. code-block:: bash pip install --upgrade MDAnalysis[analysis] @@ -121,7 +121,7 @@ If you want to `run the tests`_ or use example files to follow some of the examples in the documentation or the tutorials_, also install the ``MDAnalysisTests`` package: -.. code-block:: bash +.. code-block:: bash conda install mdanalysistests # with conda pip install --upgrade MDAnalysisTests # with pip @@ -187,12 +187,13 @@ Thank you! :caption: Documentation :numbered: :hidden: - + ./documentation_pages/overview ./documentation_pages/topology ./documentation_pages/selections ./documentation_pages/analysis_modules ./documentation_pages/topology_modules + ./documentation_pages/guesser_modules ./documentation_pages/coordinates_modules ./documentation_pages/converters ./documentation_pages/trajectory_transformations @@ -205,7 +206,7 @@ Thank you! ./documentation_pages/units ./documentation_pages/exceptions ./documentation_pages/references - + Indices and tables ================== @@ -213,4 +214,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 345e0dc671e..68b86fc9439 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -276,7 +276,7 @@ def test_parallelizable_transformations(): # pick any transformation that would allow # for parallelizable attribute from MDAnalysis.transformations import NoJump - u = mda.Universe(XTC) + u = mda.Universe(XTC, to_guess=()) u.trajectory.add_transformations(NoJump()) # test that serial works diff --git a/testsuite/MDAnalysisTests/analysis/test_dielectric.py b/testsuite/MDAnalysisTests/analysis/test_dielectric.py index ac3de34b659..21992cf5d5c 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dielectric.py +++ b/testsuite/MDAnalysisTests/analysis/test_dielectric.py @@ -57,7 +57,7 @@ def test_temperature(self, ag): assert_allclose(eps.results['eps_mean'], 9.621, rtol=1e-03) def test_non_charges(self): - u = mda.Universe(DCD_TRICLINIC) + u = mda.Universe(DCD_TRICLINIC, to_guess=()) with pytest.raises(NoDataError, match="No charges defined given atomgroup."): DielectricConstant(u.atoms).run() diff --git a/testsuite/MDAnalysisTests/converters/test_openmm_parser.py b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py index 5b67ca5924f..d3d923294f1 100644 --- a/testsuite/MDAnalysisTests/converters/test_openmm_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py @@ -53,9 +53,15 @@ class OpenMMTopologyBase(ParserBase): "bonds", "chainIDs", "elements", + "types" ] expected_n_bonds = 0 + @pytest.fixture() + def top(self, filename): + with self.parser(filename) as p: + yield p.parse() + def test_creates_universe(self, filename): """Check that Universe works with this Parser""" u = mda.Universe(filename, topology_format="OPENMMTOPOLOGY") @@ -130,6 +136,14 @@ def test_masses(self, top): else: assert top.masses.values == [] + def test_guessed_attributes(self, filename): + u = mda.Universe(filename, topology_format="OPENMMTOPOLOGY") + u_guessed_attrs = [attr.attrname for attr + in u._topology.guessed_attributes] + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + assert attr in u_guessed_attrs + class OpenMMAppTopologyBase(OpenMMTopologyBase): parser = mda.converters.OpenMMParser.OpenMMAppTopologyParser @@ -142,14 +156,25 @@ class OpenMMAppTopologyBase(OpenMMTopologyBase): "bonds", "chainIDs", "elements", + "types" ] expected_n_bonds = 0 + @pytest.fixture() + def top(self, filename): + with self.parser(filename) as p: + yield p.parse() + def test_creates_universe(self, filename): """Check that Universe works with this Parser""" u = mda.Universe(filename, topology_format="OPENMMAPP") assert isinstance(u, mda.Universe) + def test_guessed_attributes(self, filename): + u = mda.Universe(filename, topology_format="OPENMMAPP") + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + class TestOpenMMTopologyParser(OpenMMTopologyBase): ref_filename = app.PDBFile(CONECT).topology @@ -171,10 +196,13 @@ def test_with_partial_elements(self): wmsg1 = ("Element information missing for some atoms. " "These have been given an empty element record ") - wmsg2 = ("For absent elements, atomtype has been " - "set to 'X' and mass has been set to 0.0. " - "If needed these can be guessed using " - "MDAnalysis.topology.guessers.") + wmsg2 = ( + "For absent elements, atomtype has been " + "set to 'X' and mass has been set to 0.0. " + "If needed these can be guessed using " + "universe.guess_TopologyAttrs(to_guess=['masses', 'types']). " + "(for MDAnalysis version 2.x this is done automatically," + " but it will be removed in 3.0).") with pytest.warns(UserWarning) as warnings: mda_top = self.parser(self.ref_filename).parse() @@ -182,6 +210,8 @@ def test_with_partial_elements(self): assert mda_top.types.values[3388] == 'X' assert mda_top.elements.values[3344] == '' assert mda_top.elements.values[3388] == '' + assert mda_top.masses.values[3344] == 0.0 + assert mda_top.masses.values[3388] == 0.0 assert len(warnings) == 2 assert str(warnings[0].message) == wmsg1 @@ -194,14 +224,20 @@ def test_no_elements_warn(): for a in omm_top.atoms(): a.element = None - wmsg = ("Element information is missing for all the atoms. " - "Elements attribute will not be populated. " - "Atomtype attribute will be guessed using atom " - "name and mass will be guessed using atomtype." - "See MDAnalysis.topology.guessers.") - - with pytest.warns(UserWarning, match=wmsg): + wmsg = ( + "Element information is missing for all the atoms. " + "Elements attribute will not be populated. " + "Atomtype attribute will be guessed using atom " + "name and mass will be guessed using atomtype." + "For MDAnalysis version 2.x this is done automatically, " + "but it will be removed in MDAnalysis v3.0. " + "These can be guessed using " + "universe.guess_TopologyAttrs(to_guess=['masses', 'types']) " + "See MDAnalysis.guessers.") + + with pytest.warns(UserWarning) as warnings: mda_top = parser(omm_top).parse() + assert str(warnings[0].message) == wmsg def test_invalid_element_symbols(): diff --git a/testsuite/MDAnalysisTests/converters/test_rdkit.py b/testsuite/MDAnalysisTests/converters/test_rdkit.py index 85860162f7b..59c15af4c16 100644 --- a/testsuite/MDAnalysisTests/converters/test_rdkit.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit.py @@ -26,9 +26,10 @@ from io import StringIO import MDAnalysis as mda +from MDAnalysis.guesser.default_guesser import DefaultGuesser + import numpy as np import pytest -from MDAnalysis.topology.guessers import guess_atom_element from MDAnalysisTests.datafiles import GRO, PDB_full, PDB_helix, mol2_molecule from MDAnalysisTests.util import import_not_available from numpy.testing import assert_allclose, assert_equal @@ -146,7 +147,8 @@ def pdb(self): def mol2(self): u = mda.Universe(mol2_molecule) # add elements - elements = np.array([guess_atom_element(x) for x in u.atoms.types], + guesser = DefaultGuesser(None) + elements = np.array([guesser.guess_atom_element(x) for x in u.atoms.types], dtype=object) u.add_TopologyAttr('elements', elements) return u @@ -154,7 +156,7 @@ def mol2(self): @pytest.fixture def peptide(self): u = mda.Universe(GRO) - elements = mda.topology.guessers.guess_types(u.atoms.names) + elements = mda.guesser.DefaultGuesser(None).guess_types(u.atoms.names) u.add_TopologyAttr('elements', elements) return u.select_atoms("resid 2-12") diff --git a/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py index e54672902d9..e376ff09e37 100644 --- a/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py @@ -39,16 +39,19 @@ class RDKitParserBase(ParserBase): parser = mda.converters.RDKitParser.RDKitParser expected_attrs = ['ids', 'names', 'elements', 'masses', 'aromaticities', - 'resids', 'resnums', 'chiralities', - 'segids', - 'bonds', - ] - + 'resids', 'resnums', 'chiralities', 'segids', 'bonds', + ] + expected_n_atoms = 0 expected_n_residues = 1 expected_n_segments = 1 expected_n_bonds = 0 + @pytest.fixture() + def top(self, filename): + with self.parser(filename) as p: + yield p.parse() + def test_creates_universe(self, filename): u = mda.Universe(filename, format='RDKIT') assert isinstance(u, mda.Universe) @@ -56,11 +59,18 @@ def test_creates_universe(self, filename): def test_bonds_total_counts(self, top): assert len(top.bonds.values) == self.expected_n_bonds + def test_guessed_attributes(self, filename): + u = mda.Universe(filename, format='RDKIT') + u_guessed_attrs = [a.attrname for a in u._topology.guessed_attributes] + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + assert attr in u_guessed_attrs + class TestRDKitParserMOL2(RDKitParserBase): ref_filename = mol2_molecule - expected_attrs = RDKitParserBase.expected_attrs + ['charges'] + expected_attrs = RDKitParserBase.expected_attrs + ['charges', 'types'] expected_n_atoms = 49 expected_n_residues = 1 @@ -138,6 +148,10 @@ def test_aromaticity(self, top, filename): atom.GetIsAromatic() for atom in filename.GetAtoms()]) assert_equal(expected, top.aromaticities.values) + def test_guessed_types(self, filename): + u = mda.Universe(filename, format='RDKIT') + assert_equal(u.atoms.types[:7], ['N.am', 'S.o2', + 'N.am', 'N.am', 'O.2', 'O.2', 'C.3']) class TestRDKitParserPDB(RDKitParserBase): ref_filename = PDB_helix @@ -145,7 +159,6 @@ class TestRDKitParserPDB(RDKitParserBase): expected_attrs = RDKitParserBase.expected_attrs + [ 'resnames', 'altLocs', 'chainIDs', 'occupancies', 'icodes', 'tempfactors'] - guessed_attrs = ['types'] expected_n_atoms = 137 expected_n_residues = 13 @@ -165,12 +178,14 @@ def test_partial_residueinfo_raise_error(self, filename): mh = Chem.AddHs(mol, addResidueInfo=True) mda.Universe(mh) + def test_guessed_types(self, filename): + u = mda.Universe(filename, format='RDKIT') + assert_equal(u.atoms.types[:7], ['N', 'H', 'C', 'H', 'C', 'H', 'H']) + class TestRDKitParserSMILES(RDKitParserBase): ref_filename = "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" - guessed_attrs = ['types'] - expected_n_atoms = 24 expected_n_residues = 1 expected_n_segments = 1 @@ -186,8 +201,6 @@ def filename(self): class TestRDKitParserSDF(RDKitParserBase): ref_filename = SDF_molecule - guessed_attrs = ['types'] - expected_n_atoms = 49 expected_n_residues = 1 expected_n_segments = 1 @@ -200,3 +213,7 @@ def filename(self): def test_bond_orders(self, top, filename): expected = [bond.GetBondTypeAsDouble() for bond in filename.GetBonds()] assert top.bonds.order == expected + + def test_guessed_types(self, filename): + u = mda.Universe(filename, format='RDKIT') + assert_equal(u.atoms.types[:7], ['CA', 'C', 'C', 'C', 'C', 'C', 'O']) diff --git a/testsuite/MDAnalysisTests/coordinates/base.py b/testsuite/MDAnalysisTests/coordinates/base.py index 3de8cfb9ff6..39770e1460b 100644 --- a/testsuite/MDAnalysisTests/coordinates/base.py +++ b/testsuite/MDAnalysisTests/coordinates/base.py @@ -504,7 +504,7 @@ def test_timeseries_values(self, reader, slice): @pytest.mark.parametrize('asel', ("index 1", "index 2", "index 1 to 3")) def test_timeseries_asel_shape(self, reader, asel): - atoms = mda.Universe(reader.filename).select_atoms(asel) + atoms = mda.Universe(reader.filename, to_guess=()).select_atoms(asel) timeseries = reader.timeseries(atoms, order='fac') assert(timeseries.shape[0] == len(reader)) assert(timeseries.shape[1] == len(atoms)) @@ -513,28 +513,28 @@ def test_timeseries_asel_shape(self, reader, asel): def test_timeseries_empty_asel(self, reader): with pytest.warns(UserWarning, match="Empty string to select atoms, empty group returned."): - atoms = mda.Universe(reader.filename).select_atoms(None) + atoms = mda.Universe(reader.filename, to_guess=()).select_atoms(None) with pytest.raises(ValueError, match="Timeseries requires at least"): reader.timeseries(asel=atoms) def test_timeseries_empty_atomgroup(self, reader): with pytest.warns(UserWarning, match="Empty string to select atoms, empty group returned."): - atoms = mda.Universe(reader.filename).select_atoms(None) + atoms = mda.Universe(reader.filename, to_guess=()).select_atoms(None) with pytest.raises(ValueError, match="Timeseries requires at least"): reader.timeseries(atomgroup=atoms) def test_timeseries_asel_warns_deprecation(self, reader): - atoms = mda.Universe(reader.filename).select_atoms("index 1") + atoms = mda.Universe(reader.filename, to_guess=()).select_atoms("index 1") with pytest.warns(DeprecationWarning, match="asel argument to"): timeseries = reader.timeseries(asel=atoms, order='fac') def test_timeseries_atomgroup(self, reader): - atoms = mda.Universe(reader.filename).select_atoms("index 1") + atoms = mda.Universe(reader.filename, to_guess=()).select_atoms("index 1") timeseries = reader.timeseries(atomgroup=atoms, order='fac') def test_timeseries_atomgroup_asel_mutex(self, reader): - atoms = mda.Universe(reader.filename).select_atoms("index 1") + atoms = mda.Universe(reader.filename, to_guess=()).select_atoms("index 1") with pytest.raises(ValueError, match="Cannot provide both"): timeseries = reader.timeseries(atomgroup=atoms, asel=atoms, order='fac') diff --git a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py index 2049b687279..9f81b8c325c 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py +++ b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py @@ -52,12 +52,12 @@ def transformed(ref): return mda.Universe(PSF, [DCD, CRD, DCD, CRD, DCD, CRD, CRD], transformations=[translate([10,10,10])]) - + def test_regular_repr(self): u = mda.Universe(PSF, [DCD, CRD, DCD]) assert_equal("", u.trajectory.__repr__()) - - + + def test_truncated_repr(self, universe): assert_equal("", universe.trajectory.__repr__()) @@ -135,8 +135,8 @@ def test_write_dcd(self, universe, tmpdir): ts_new._pos, self.prec, err_msg="Coordinates disagree at frame {0:d}".format( - ts_orig.frame)) - + ts_orig.frame)) + def test_transform_iteration(self, universe, transformed): vector = np.float32([10,10,10]) # # Are the transformations applied and @@ -151,7 +151,7 @@ def test_transform_iteration(self, universe, transformed): frame = ts.frame ref = universe.trajectory[frame].positions + vector assert_almost_equal(ts.positions, ref, decimal = 6) - + def test_transform_slice(self, universe, transformed): vector = np.float32([10,10,10]) # what happens when we slice the trajectory? @@ -159,7 +159,7 @@ def test_transform_slice(self, universe, transformed): frame = ts.frame ref = universe.trajectory[frame].positions + vector assert_almost_equal(ts.positions, ref, decimal = 6) - + def test_transform_switch(self, universe, transformed): vector = np.float32([10,10,10]) # grab a frame: @@ -170,7 +170,7 @@ def test_transform_switch(self, universe, transformed): assert_almost_equal(transformed.trajectory[10].positions, newref, decimal = 6) # what happens when we comeback to the previous frame? assert_almost_equal(transformed.trajectory[2].positions, ref, decimal = 6) - + def test_transfrom_rewind(self, universe, transformed): vector = np.float32([10,10,10]) ref = universe.trajectory[0].positions + vector @@ -221,13 +221,13 @@ def test_set_all_format_lammps(self): assert_equal(time_values, np.arange(11)) def test_set_format_tuples_and_format(self): - universe = mda.Universe(GRO, [(PDB, 'pdb'), GRO, GRO, (XTC, 'xtc'), + universe = mda.Universe(GRO, [(PDB, 'pdb'), GRO, GRO, (XTC, 'xtc'), (TRR, 'trr')], format='gro') assert universe.trajectory.n_frames == 23 assert_equal(universe.trajectory.filenames, [PDB, GRO, GRO, XTC, TRR]) - + with pytest.raises(TypeError) as errinfo: - mda.Universe(GRO, [(PDB, 'pdb'), GRO, GRO, (XTC, 'xtc'), + mda.Universe(GRO, [(PDB, 'pdb'), GRO, GRO, (XTC, 'xtc'), (TRR, 'trr')], format='pdb') assert 'Unable to read' in str(errinfo.value) @@ -268,7 +268,7 @@ def build_trajectories(folder, sequences, fmt='xtc'): fnames = [] for index, subseq in enumerate(sequences): coords = np.zeros((len(subseq), 1, 3), dtype=np.float32) + index - u = mda.Universe(utop._topology, coords) + u = mda.Universe(utop._topology, coords, to_guess=()) out_traj = mda.Writer(template.format(index), n_atoms=len(u.atoms)) fnames.append(out_traj.filename) with out_traj: @@ -320,7 +320,7 @@ def __init__(self, seq, n_frames, order): def test_order(self, seq_info, tmpdir, fmt): folder = str(tmpdir) utop, fnames = build_trajectories(folder, sequences=seq_info.seq, fmt=fmt) - u = mda.Universe(utop._topology, fnames, continuous=True) + u = mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) assert u.trajectory.n_frames == seq_info.n_frames for i, ts in enumerate(u.trajectory): assert_almost_equal(i, ts.time, decimal=4) @@ -331,14 +331,14 @@ def test_start_frames(self, tmpdir): folder = str(tmpdir) sequences = ([0, 1, 2, 3], [2, 3, 4, 5], [4, 5, 6, 7]) utop, fnames = build_trajectories(folder, sequences=sequences,) - u = mda.Universe(utop._topology, fnames, continuous=True) + u = mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) assert_equal(u.trajectory._start_frames, [0, 2, 4]) def test_missing(self, tmpdir): folder = str(tmpdir) sequences = ([0, 1, 2, 3], [5, 6, 7, 8, 9]) utop, fnames = build_trajectories(folder, sequences=sequences,) - u = mda.Universe(utop._topology, fnames, continuous=True) + u = mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) assert u.trajectory.n_frames == 9 def test_warning(self, tmpdir): @@ -347,7 +347,7 @@ def test_warning(self, tmpdir): sequences = ([0, 1, 2, 3], [5, 6, 7]) utop, fnames = build_trajectories(folder, sequences=sequences,) with pytest.warns(UserWarning): - mda.Universe(utop._topology, fnames, continuous=True) + mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) def test_interleaving_error(self, tmpdir): folder = str(tmpdir) @@ -355,7 +355,7 @@ def test_interleaving_error(self, tmpdir): sequences = ([0, 2, 4, 6], [1, 3, 5, 7]) utop, fnames = build_trajectories(folder, sequences=sequences,) with pytest.raises(RuntimeError): - mda.Universe(utop._topology, fnames, continuous=True) + mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) def test_easy_trigger_warning(self, tmpdir): folder = str(tmpdir) @@ -372,14 +372,14 @@ def test_easy_trigger_warning(self, tmpdir): warnings.filterwarnings( action='ignore', category=ImportWarning) - mda.Universe(utop._topology, fnames, continuous=True) + mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) def test_single_frames(self, tmpdir): folder = str(tmpdir) sequences = ([0, 1, 2, 3], [5, ]) utop, fnames = build_trajectories(folder, sequences=sequences,) with pytest.raises(RuntimeError): - mda.Universe(utop._topology, fnames, continuous=True) + mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) def test_mixed_filetypes(self): with pytest.raises(ValueError): @@ -388,9 +388,9 @@ def test_mixed_filetypes(self): def test_unsupported_filetypes(self): with pytest.raises(NotImplementedError): mda.Universe(PSF, [DCD, DCD], continuous=True) - # see issue 2353. The PDB reader has multiple format endings. To ensure - # the not implemented error is thrown we do a check here. A more - # careful test in the future would be a dummy reader with multiple + # see issue 2353. The PDB reader has multiple format endings. To ensure + # the not implemented error is thrown we do a check here. A more + # careful test in the future would be a dummy reader with multiple # formats, just in case PDB will allow continuous reading in the future. with pytest.raises(ValueError): mda.Universe(PDB, [PDB, XTC], continuous=True) diff --git a/testsuite/MDAnalysisTests/coordinates/test_h5md.py b/testsuite/MDAnalysisTests/coordinates/test_h5md.py index d3fec51f818..76e80a2a46d 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_h5md.py +++ b/testsuite/MDAnalysisTests/coordinates/test_h5md.py @@ -449,7 +449,7 @@ def test_parse_n_atoms(self, h5md_file, outfile, group1, group2): except KeyError: continue - u = mda.Universe(outfile) + u = mda.Universe(outfile, to_guess=()) assert_equal(u.atoms.n_atoms, n_atoms_in_dset) def test_parse_n_atoms_error(self, h5md_file, outfile): diff --git a/testsuite/MDAnalysisTests/coordinates/test_netcdf.py b/testsuite/MDAnalysisTests/coordinates/test_netcdf.py index c009682421e..7e365dad51a 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_netcdf.py +++ b/testsuite/MDAnalysisTests/coordinates/test_netcdf.py @@ -492,7 +492,7 @@ def test_scale_factor_coordinates(self, tmpdir): expected = np.asarray(range(3), dtype=np.float32) * 2.0 with tmpdir.as_cwd(): self.create_ncdf(params) - u = mda.Universe(params['filename']) + u = mda.Universe(params['filename'], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.positions[0], expected, self.prec) @@ -502,7 +502,7 @@ def test_scale_factor_velocities(self, tmpdir): expected = np.asarray(range(3), dtype=np.float32) * 3.0 with tmpdir.as_cwd(): self.create_ncdf(params) - u = mda.Universe(params['filename']) + u = mda.Universe(params['filename'], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.velocities[0], expected, self.prec) @@ -512,7 +512,7 @@ def test_scale_factor_forces(self, tmpdir): expected = np.asarray(range(3), dtype=np.float32) * 10.0 * 4.184 with tmpdir.as_cwd(): self.create_ncdf(params) - u = mda.Universe(params['filename']) + u = mda.Universe(params['filename'], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.forces[0], expected, self.prec) @@ -526,7 +526,7 @@ def test_scale_factor_box(self, tmpdir, mutation, expected): params = self.gen_params(keypair=mutation, restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) - u = mda.Universe(params['filename']) + u = mda.Universe(params['filename'], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.dimensions, expected, self.prec) @@ -599,7 +599,7 @@ def test_ioerror(self, tmpdir): with tmpdir.as_cwd(): self.create_ncdf(params) with pytest.raises(IOError): - u = mda.Universe(params['filename']) + u = mda.Universe(params['filename'], to_guess=()) u.trajectory.close() u.trajectory[-1] diff --git a/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py b/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py index 2d0228fcd58..423a255cc49 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py @@ -119,7 +119,7 @@ def test_iter(self, ts): def test_repr(self, ts): assert_equal(type(repr(ts)), str) - + def test_repr_with_box(self, ts): assert("with unit cell dimensions" in repr(ts)) @@ -698,7 +698,7 @@ def test_dt(self, universe): def test_atomgroup_dims_access(uni): uni_args, uni_kwargs = uni # check that AtomGroup.dimensions always returns a copy - u = mda.Universe(*uni_args, **uni_kwargs) + u = mda.Universe(*uni_args, **uni_kwargs, to_guess=()) ag = u.atoms[:10] diff --git a/testsuite/MDAnalysisTests/coordinates/test_trz.py b/testsuite/MDAnalysisTests/coordinates/test_trz.py index e0803aa7978..e0f20f961a6 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_trz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_trz.py @@ -162,7 +162,7 @@ def test_read_zero_box(self, tmpdir): with mda.Writer(outfile, n_atoms=10) as w: w.write(u) - u2 = mda.Universe(outfile, n_atoms=10) + u2 = mda.Universe(outfile, n_atoms=10, to_guess=()) assert u2.dimensions is None diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index 4456362c498..fe51cf24073 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -129,7 +129,7 @@ def test_write_frames(self, u, tmpdir, frames): ref_positions = np.stack([ts.positions.copy() for ts in selection]) u.atoms.write(destination, frames=frames) - u_new = mda.Universe(destination) + u_new = mda.Universe(destination, to_guess=()) new_positions = np.stack([ts.positions.copy() for ts in u_new.trajectory]) assert_array_almost_equal(new_positions, ref_positions) @@ -145,7 +145,7 @@ def test_write_frame_iterator(self, u, tmpdir, frames): ref_positions = np.stack([ts.positions.copy() for ts in selection]) u.atoms.write(destination, frames=selection) - u_new = mda.Universe(destination) + u_new = mda.Universe(destination, to_guess=()) new_positions = np.stack([ts.positions.copy() for ts in u_new.trajectory]) assert_array_almost_equal(new_positions, ref_positions) @@ -155,7 +155,7 @@ def test_write_frame_iterator(self, u, tmpdir, frames): def test_write_frame_none(self, u, tmpdir, extension, compression): destination = str(tmpdir / 'test.' + extension + compression) u.atoms.write(destination, frames=None) - u_new = mda.Universe(destination) + u_new = mda.Universe(destination, to_guess=()) new_positions = np.stack([ts.positions for ts in u_new.trajectory]) # Most format only save 3 decimals; XTC even has only 2. assert_array_almost_equal(u.atoms.positions[None, ...], @@ -165,7 +165,8 @@ def test_write_frame_none(self, u, tmpdir, extension, compression): def test_write_frames_all(self, u, tmpdir, compression): destination = str(tmpdir / 'test.dcd' + compression) u.atoms.write(destination, frames='all') - u_new = mda.Universe(destination) + + u_new = mda.Universe(destination, to_guess=()) ref_positions = np.stack([ts.positions.copy() for ts in u.trajectory]) new_positions = np.stack([ts.positions.copy() for ts in u_new.trajectory]) assert_array_almost_equal(new_positions, ref_positions) diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index a0c1acdf4bd..5489c381af2 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -82,7 +82,7 @@ def attr(self, top): @pytest.fixture() def universe(self, top): - return mda.Universe(top) + return mda.Universe(top, to_guess=()) def test_len(self, attr): assert len(attr) == len(attr.values) @@ -197,6 +197,17 @@ def test_next_emptyresidue(self, u): assert groupsize == 0 assert groupsize == len(u.residues[[]].atoms) + def test_missing_values(self, attr): + assert_equal(attr.are_values_missing(self.values), np.array( + [False, False, False, False, False, False, + False, False, False, False])) + + def test_missing_value_label(self): + self.attrclass.missing_value_label = 'FOO' + values = np.array(['NA', 'C', 'N', 'FOO']) + assert_equal(self.attrclass.are_values_missing(values), + np.array([False, False, False, True])) + class AggregationMixin(TestAtomAttr): def test_get_residues(self, attr): @@ -216,6 +227,10 @@ def test_get_segment(self, attr): class TestMasses(AggregationMixin): attrclass = tpattrs.Masses + def test_missing_masses(self): + values = [1., 2., np.nan, 3.] + assert_equal(self.attrclass.are_values_missing(values), + np.array([False, False, True, False])) class TestCharges(AggregationMixin): values = np.array([+2, -1, 0, -1, +1, +2, 0, 0, 0, -1]) diff --git a/testsuite/MDAnalysisTests/core/test_universe.py b/testsuite/MDAnalysisTests/core/test_universe.py index 80259df5e79..3e41a38a967 100644 --- a/testsuite/MDAnalysisTests/core/test_universe.py +++ b/testsuite/MDAnalysisTests/core/test_universe.py @@ -47,7 +47,7 @@ GRO, TRR, two_water_gro, two_water_gro_nonames, TRZ, TRZ_psf, - PDB, MMTF, + PDB, MMTF, CONECT, ) import MDAnalysis as mda @@ -239,7 +239,7 @@ def test_from_bad_smiles(self): def test_no_Hs(self): smi = "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" - u = mda.Universe.from_smiles(smi, addHs=False, + u = mda.Universe.from_smiles(smi, addHs=False, generate_coordinates=False, format='RDKIT') assert u.atoms.n_atoms == 14 assert len(u.bonds.indices) == 15 @@ -261,7 +261,7 @@ def test_generate_coordinates_numConfs(self): def test_rdkit_kwargs(self): # test for bad kwarg: # Unfortunately, exceptions from Boost cannot be passed to python, - # we cannot `from Boost.Python import ArgumentError` and use it with + # we cannot `from Boost.Python import ArgumentError` and use it with # pytest.raises(ArgumentError), so "this is the way" try: u = mda.Universe.from_smiles("CCO", rdkit_kwargs=dict(abc=42)) @@ -274,7 +274,7 @@ def test_rdkit_kwargs(self): u1 = mda.Universe.from_smiles("C", rdkit_kwargs=dict(randomSeed=42)) u2 = mda.Universe.from_smiles("C", rdkit_kwargs=dict(randomSeed=51)) with pytest.raises(AssertionError) as e: - assert_equal(u1.trajectory.coordinate_array, + assert_equal(u1.trajectory.coordinate_array, u2.trajectory.coordinate_array) assert "Mismatched elements: 15 / 15 (100%)" in str(e.value) @@ -385,6 +385,41 @@ def test_list(self): ref = translate([10,10,10])(uref.trajectory.ts) assert_almost_equal(u.trajectory.ts.positions, ref, decimal=6) + +class TestGuessTopologyAttrs(object): + def test_automatic_type_and_mass_guessing(self): + u = mda.Universe(PDB_small) + assert_equal(len(u.atoms.masses), 3341) + assert_equal(len(u.atoms.types), 3341) + + def test_no_type_and_mass_guessing(self): + u = mda.Universe(PDB_small, to_guess=()) + assert not hasattr(u.atoms, 'masses') + assert not hasattr(u.atoms, 'types') + + def test_invalid_context(self): + u = mda.Universe(PDB_small) + with pytest.raises(KeyError): + u.guess_TopologyAttrs(context='trash', to_guess=['masses']) + + def test_invalid_attributes(self): + u = mda.Universe(PDB_small) + with pytest.raises(ValueError): + u.guess_TopologyAttrs(to_guess=['trash']) + + def test_guess_masses_before_types(self): + u = mda.Universe(PDB_small, to_guess=('masses', 'types')) + assert_equal(len(u.atoms.masses), 3341) + assert_equal(len(u.atoms.types), 3341) + + def test_guessing_read_attributes(self): + u = mda.Universe(PSF) + old_types = u.atoms.types + u.guess_TopologyAttrs(force_guess=['types']) + with pytest.raises(AssertionError): + assert_equal(old_types, u.atoms.types) + + class TestGuessMasses(object): """Tests the Mass Guesser in topology.guessers """ @@ -433,7 +468,7 @@ def test_universe_guess_bonds_no_vdwradii(self): def test_universe_guess_bonds_with_vdwradii(self, vdw): """Unknown atom types, but with vdw radii here to save the day""" u = mda.Universe(two_water_gro_nonames, guess_bonds=True, - vdwradii=vdw) + vdwradii=vdw) self._check_universe(u) assert u.kwargs['guess_bonds'] assert_equal(vdw, u.kwargs['vdwradii']) @@ -450,7 +485,7 @@ def test_universe_guess_bonds_arguments(self): are being passed correctly. """ u = mda.Universe(two_water_gro, guess_bonds=True) - + self._check_universe(u) assert u.kwargs["guess_bonds"] assert u.kwargs["fudge_factor"] @@ -516,6 +551,17 @@ def test_guess_bonds_periodicity(self): self._check_atomgroup(ag, u) + def guess_bonds_with_to_guess(self): + u = mda.Universe(two_water_gro) + has_bonds = hasattr(u.atoms, 'bonds') + u.guess_TopologyAttrs(to_guess=['bonds']) + assert not has_bonds + assert u.atoms.bonds + + def test_guess_read_bonds(self): + u = mda.Universe(CONECT) + assert len(u.bonds) == 72 + class TestInMemoryUniverse(object): def test_reader_w_timeseries(self): @@ -747,10 +793,14 @@ def test_add_connection(self, universe, attr, values): ('impropers', [(1, 2, 3)]), ) ) - def add_connection_error(self, universe, attr, values): + def test_add_connection_error(self, universe, attr, values): with pytest.raises(ValueError): universe.add_TopologyAttr(attr, values) + def test_add_attr_length_error(self, universe): + with pytest.raises(ValueError): + universe.add_TopologyAttr('masses', np.array([1, 2, 3], dtype=np.float64)) + class TestDelTopologyAttr(object): @pytest.fixture() @@ -827,7 +877,7 @@ def potatoes(self): return "potoooooooo" transplants["Universe"].append(("potatoes", potatoes)) - + universe.add_TopologyAttr("tubers") assert universe.potatoes() == "potoooooooo" universe.del_TopologyAttr("tubers") @@ -1375,6 +1425,6 @@ def test_only_top(self): with pytest.warns(UserWarning, match="No coordinate reader found for"): - u = mda.Universe(t) + u = mda.Universe(t, to_guess=()) assert len(u.atoms) == 10 diff --git a/testsuite/MDAnalysisTests/guesser/test_base.py b/testsuite/MDAnalysisTests/guesser/test_base.py new file mode 100644 index 00000000000..b429826647f --- /dev/null +++ b/testsuite/MDAnalysisTests/guesser/test_base.py @@ -0,0 +1,102 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# 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 Lesser 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 +# +import pytest +import numpy as np +import MDAnalysis as mda +from MDAnalysis.guesser.base import GuesserBase, get_guesser +from MDAnalysis.core.topology import Topology +from MDAnalysis.core.topologyattrs import Masses, Atomnames, Atomtypes +import MDAnalysis.tests.datafiles as datafiles +from numpy.testing import assert_allclose, assert_equal + + +class TesttBaseGuesser(): + + def test_get_guesser(self): + class TestGuesser1(GuesserBase): + context = 'test1' + + class TestGuesser2(GuesserBase): + context = 'test2' + + assert get_guesser(TestGuesser1).context == 'test1' + assert get_guesser('test1').context == 'test1' + assert get_guesser(TestGuesser2()).context == 'test2' + + def test_get_guesser_with_universe(self): + class TestGuesser1(GuesserBase): + context = 'test1' + + u = mda.Universe.empty(n_atoms=5) + guesser = get_guesser(TestGuesser1(), u, foo=1) + + assert len(guesser._universe.atoms) == 5 + assert 'foo' in guesser._kwargs + + def test_guess_invalid_attribute(self): + with pytest.raises(ValueError, + match='default guesser can not guess ' + 'the following attribute: foo'): + mda.Universe(datafiles.PDB, to_guess=['foo']) + + def test_guess_attribute_with_missing_parent_attr(self): + names = Atomnames(np.array(['C', 'HB', 'HA', 'O'], dtype=object)) + masses = Masses( + np.array([np.nan, np.nan, np.nan, np.nan], dtype=np.float64)) + top = Topology(4, 1, 1, attrs=[names, masses, ]) + u = mda.Universe(top, to_guess=['masses']) + assert_allclose(u.atoms.masses, np.array( + [12.01100, 1.00800, 1.00800, 15.99900]), atol=0) + + def test_force_guessing(self): + names = Atomnames(np.array(['C', 'H', 'H', 'O'], dtype=object)) + types = Atomtypes(np.array(['1', '2', '3', '4'], dtype=object)) + top = Topology(4, 1, 1, attrs=[names, types, ]) + u = mda.Universe(top, force_guess=['types']) + assert_equal(u.atoms.types, ['C', 'H', 'H', 'O']) + + def test_partial_guessing(self): + types = Atomtypes(np.array(['C', 'H', 'H', 'O'], dtype=object)) + masses = Masses(np.array([0, np.nan, np.nan, 0], dtype=np.float64)) + top = Topology(4, 1, 1, attrs=[types, masses, ]) + u = mda.Universe(top, to_guess=['masses']) + assert_allclose(u.atoms.masses, np.array( + [0, 1.00800, 1.00800, 0]), atol=0) + + def test_force_guess_priority(self): + "check that passing the attribute to force_guess have higher power" + types = Atomtypes(np.array(['C', 'H', 'H', 'O'], dtype=object)) + masses = Masses(np.array([0, np.nan, np.nan, 0], dtype=np.float64)) + top = Topology(4, 1, 1, attrs=[types, masses, ]) + u = mda.Universe(top, to_guess=['masses'], force_guess=['masses']) + assert_allclose(u.atoms.masses, np.array( + [12.01100, 1.00800, 1.00800, 15.99900]), atol=0) + + def test_partial_guess_attr_with_unknown_no_value_label(self): + "trying to partially guess attribute tha doesn't have declared" + "no_value_label should gives no effect" + names = Atomnames(np.array(['C', 'H', 'H', 'O'], dtype=object)) + types = Atomtypes(np.array(['', '', '', ''], dtype=object)) + top = Topology(4, 1, 1, attrs=[names, types, ]) + u = mda.Universe(top, to_guess=['types']) + assert_equal(u.atoms.types, ['', '', '', '']) diff --git a/testsuite/MDAnalysisTests/guesser/test_default_guesser.py b/testsuite/MDAnalysisTests/guesser/test_default_guesser.py new file mode 100644 index 00000000000..2aacb28d4f1 --- /dev/null +++ b/testsuite/MDAnalysisTests/guesser/test_default_guesser.py @@ -0,0 +1,302 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# 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 Lesser 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 +# +import pytest +from pytest import approx +import MDAnalysis as mda + +from numpy.testing import assert_equal, assert_allclose +import numpy as np +from MDAnalysis.core.topologyattrs import Angles, Atomtypes, Atomnames, Masses +from MDAnalysis.guesser.default_guesser import DefaultGuesser +from MDAnalysis.core.topology import Topology +from MDAnalysisTests import make_Universe +from MDAnalysisTests.core.test_fragments import make_starshape +import MDAnalysis.tests.datafiles as datafiles +from MDAnalysisTests.util import import_not_available + +try: + from rdkit import Chem + from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges +except ImportError: + pass + +requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), + reason="requires RDKit") + + +@pytest.fixture +def default_guesser(): + return DefaultGuesser(None) + + +class TestGuessMasses(object): + def test_guess_masses_from_universe(self): + topology = Topology(3, attrs=[Atomtypes(['C', 'C', 'H'])]) + u = mda.Universe(topology) + + assert isinstance(u.atoms.masses, np.ndarray) + assert_allclose(u.atoms.masses, np.array( + [12.011, 12.011, 1.008]), atol=0) + + def test_guess_masses_from_guesser_object(self, default_guesser): + elements = ['H', 'Ca', 'Am'] + values = np.array([1.008, 40.08000, 243.0]) + assert_allclose(default_guesser.guess_masses( + elements), values, atol=0) + + def test_guess_masses_warn(self): + topology = Topology(2, attrs=[Atomtypes(['X', 'Z'])]) + msg = "Unknown masses are set to 0.0 for current version, " + "this will be depracated in version 3.0.0 and replaced by" + " Masse's no_value_label (np.nan)" + with pytest.warns(PendingDeprecationWarning, match=msg): + u = mda.Universe(topology, to_guess=['masses']) + assert_allclose(u.atoms.masses, np.array([0.0, 0.0]), atol=0) + + @pytest.mark.parametrize('element, value', (('H', 1.008), ('XYZ', 0.0),)) + def test_get_atom_mass(self, element, value, default_guesser): + default_guesser.get_atom_mass(element) == approx(value) + + def test_guess_atom_mass(self, default_guesser): + assert default_guesser.guess_atom_mass('1H') == approx(1.008) + + def test_guess_masses_with_no_reference_elements(self): + u = mda.Universe.empty(3) + with pytest.raises(ValueError, + match=('there is no reference attributes ')): + u.guess_TopologyAttrs('default', ['masses']) + + +class TestGuessTypes(object): + + def test_guess_types(self): + topology = Topology(2, attrs=[Atomnames(['MG2+', 'C12'])]) + u = mda.Universe(topology, to_guess=['types']) + assert isinstance(u.atoms.types, np.ndarray) + assert_equal(u.atoms.types, np.array(['MG', 'C'], dtype=object)) + + def test_guess_atom_element(self, default_guesser): + assert default_guesser.guess_atom_element('MG2+') == 'MG' + + def test_guess_atom_element_empty(self, default_guesser): + assert default_guesser.guess_atom_element('') == '' + + def test_guess_atom_element_singledigit(self, default_guesser): + assert default_guesser.guess_atom_element('1') == '1' + + def test_guess_atom_element_1H(self, default_guesser): + assert default_guesser.guess_atom_element('1H') == 'H' + assert default_guesser.guess_atom_element('2H') == 'H' + + def test_partial_guess_elements(self, default_guesser): + names = np.array(['BR123', 'Hk', 'C12'], dtype=object) + elements = np.array(['BR', 'C'], dtype=object) + guessed_elements = default_guesser.guess_types( + atom_types=names, indices_to_guess=[True, False, True]) + assert_equal(elements, guessed_elements) + + def test_guess_elements_from_no_data(self): + top = Topology(5) + msg = "there is no reference attributes in this universe" + "to guess types from" + with pytest.raises(ValueError, match=(msg)): + mda.Universe(top, to_guess=['types']) + + @pytest.mark.parametrize('name, element', ( + ('AO5*', 'O'), + ('F-', 'F'), + ('HB1', 'H'), + ('OC2', 'O'), + ('1he2', 'H'), + ('3hg2', 'H'), + ('OH-', 'O'), + ('HO', 'H'), + ('he', 'H'), + ('zn', 'ZN'), + ('Ca2+', 'CA'), + ('CA', 'C'), + )) + def test_guess_element_from_name(self, name, element, default_guesser): + assert default_guesser.guess_atom_element(name) == element + + +def test_guess_charge(default_guesser): + # this always returns 0.0 + assert default_guesser.guess_atom_charge('this') == approx(0.0) + + +def test_guess_bonds_Error(): + u = make_Universe(trajectory=True) + msg = "This Universe does not contain name information" + with pytest.raises(ValueError, match=msg): + u.guess_TopologyAttrs(to_guess=['bonds']) + + +def test_guess_bond_vdw_error(): + u = mda.Universe(datafiles.PDB) + with pytest.raises(ValueError, match="vdw radii for types: DUMMY"): + DefaultGuesser(u).guess_bonds(u.atoms) + + +def test_guess_bond_coord_error(default_guesser): + msg = "atoms' and 'coord' must be the same length" + with pytest.raises(ValueError, match=msg): + default_guesser.guess_bonds(['N', 'O', 'C'], [[1, 2, 3]]) + + +def test_guess_angles_with_no_bonds(): + "Test guessing angles for atoms with no bonds" + " information without adding bonds to universe " + u = mda.Universe(datafiles.two_water_gro) + u.guess_TopologyAttrs(to_guess=['angles']) + assert hasattr(u, 'angles') + assert not hasattr(u, 'bonds') + + +def test_guess_impropers(default_guesser): + u = make_starshape() + + ag = u.atoms[:5] + guessed_angles = default_guesser.guess_angles(ag.bonds) + u.add_TopologyAttr(Angles(guessed_angles)) + + vals = default_guesser.guess_improper_dihedrals(ag.angles) + assert_equal(len(vals), 12) + + +def test_guess_dihedrals_with_no_angles(): + "Test guessing dihedrals for atoms with no angles " + "information without adding bonds or angles to universe" + u = mda.Universe(datafiles.two_water_gro) + u.guess_TopologyAttrs(to_guess=['dihedrals']) + assert hasattr(u, 'dihedrals') + assert not hasattr(u, 'angles') + assert not hasattr(u, 'bonds') + + +def test_guess_impropers_with_angles(): + "Test guessing impropers for atoms with angles " + "and bonds information " + u = mda.Universe(datafiles.two_water_gro, + to_guess=['bonds', 'angles', 'impropers']) + u.guess_TopologyAttrs(to_guess=['impropers']) + assert hasattr(u, 'impropers') + assert hasattr(u, 'angles') + assert hasattr(u, 'bonds') + + +def test_guess_impropers_with_no_angles(): + "Test guessing impropers for atoms with no angles " + "information without adding bonds or angles to universe" + u = mda.Universe(datafiles.two_water_gro) + u.guess_TopologyAttrs(to_guess=['impropers']) + assert hasattr(u, 'impropers') + assert not hasattr(u, 'angles') + assert not hasattr(u, 'bonds') + + +def bond_sort(arr): + # sort from low to high, also within a tuple + # e.g. ([5, 4], [0, 1], [0, 3]) -> ([0, 1], [0, 3], [4, 5]) + out = [] + for (i, j) in arr: + if i > j: + i, j = j, i + out.append((i, j)) + return sorted(out) + + +def test_guess_bonds_water(): + u = mda.Universe(datafiles.two_water_gro) + bonds = bond_sort(DefaultGuesser( + None, box=u.dimensions).guess_bonds(u.atoms, u.atoms.positions)) + assert_equal(bonds, ((0, 1), + (0, 2), + (3, 4), + (3, 5))) + + +def test_guess_bonds_adk(): + u = mda.Universe(datafiles.PSF, datafiles.DCD) + u.guess_TopologyAttrs(force_guess=['types']) + guesser = DefaultGuesser(None) + bonds = bond_sort(guesser.guess_bonds(u.atoms, u.atoms.positions)) + assert_equal(np.sort(u.bonds.indices, axis=0), + np.sort(bonds, axis=0)) + + +def test_guess_bonds_peptide(): + u = mda.Universe(datafiles.PSF_NAMD, datafiles.PDB_NAMD) + u.guess_TopologyAttrs(force_guess=['types']) + guesser = DefaultGuesser(None) + bonds = bond_sort(guesser.guess_bonds(u.atoms, u.atoms.positions)) + assert_equal(np.sort(u.bonds.indices, axis=0), + np.sort(bonds, axis=0)) + + +@pytest.mark.parametrize("smi", [ + "c1ccccc1", + "C1=CC=CC=C1", + "CCO", + "c1ccccc1Cc1ccccc1", + "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", +]) +@requires_rdkit +def test_guess_aromaticities(smi): + mol = Chem.MolFromSmiles(smi) + mol = Chem.AddHs(mol) + expected = np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) + u = mda.Universe(mol) + guesser = DefaultGuesser(None) + values = guesser.guess_aromaticities(u.atoms) + u.guess_TopologyAttrs(to_guess=['aromaticities']) + assert_equal(values, expected) + assert_equal(u.atoms.aromaticities, expected) + + +@pytest.mark.parametrize("smi", [ + "c1ccccc1", + "C1=CC=CC=C1", + "CCO", + "c1ccccc1Cc1ccccc1", + "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", +]) +@requires_rdkit +def test_guess_gasteiger_charges(smi): + mol = Chem.MolFromSmiles(smi) + mol = Chem.AddHs(mol) + ComputeGasteigerCharges(mol, throwOnParamFailure=True) + expected = np.array([atom.GetDoubleProp("_GasteigerCharge") + for atom in mol.GetAtoms()], dtype=np.float32) + u = mda.Universe(mol) + guesser = DefaultGuesser(None) + values = guesser.guess_gasteiger_charges(u.atoms) + assert_equal(values, expected) + + +@requires_rdkit +def test_aromaticity(): + u = mda.Universe(datafiles.PDB_small, + to_guess=['elements', 'aromaticities']) + c_aromatic = u.select_atoms('resname PHE and name CD1') + assert_equal(c_aromatic.aromaticities[0], True) diff --git a/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py b/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py index 6b29aec6a57..e92ff80bf14 100644 --- a/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py +++ b/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py @@ -90,7 +90,7 @@ def u(request): if len(request.param) == 1: f = request.param[0] - return mda.Universe(f) + return mda.Universe(f, to_guess=()) else: top, trj = request.param return mda.Universe(top, trj) diff --git a/testsuite/MDAnalysisTests/topology/base.py b/testsuite/MDAnalysisTests/topology/base.py index 7833f0a51ed..142e7954abb 100644 --- a/testsuite/MDAnalysisTests/topology/base.py +++ b/testsuite/MDAnalysisTests/topology/base.py @@ -25,8 +25,7 @@ import MDAnalysis as mda from MDAnalysis.core.topology import Topology -mandatory_attrs = ['ids', 'masses', 'types', - 'resids', 'resnums', 'segids'] +mandatory_attrs = ['ids', 'resids', 'resnums', 'segids'] class ParserBase(object): @@ -62,25 +61,16 @@ def test_mandatory_attributes(self, top): def test_expected_attributes(self, top): # Extra attributes as declared in specific implementations - for attr in self.expected_attrs+self.guessed_attrs: + for attr in self.expected_attrs: assert hasattr(top, attr), 'Missing expected attribute: {}'.format(attr) - + def test_no_unexpected_attributes(self, top): attrs = set(self.expected_attrs - + self.guessed_attrs + mandatory_attrs - + ['indices', 'resindices', 'segindices']) + + ['indices', 'resindices', 'segindices'] + self.guessed_attrs) for attr in top.attrs: assert attr.attrname in attrs, 'Unexpected attribute: {}'.format(attr.attrname) - def test_guessed_attributes(self, top): - # guessed attributes must be declared as guessed - for attr in top.attrs: - val = attr.is_guessed - if not val in (True, False): # only for simple yes/no cases - continue - assert val == (attr.attrname in self.guessed_attrs), 'Attr "{}" guessed= {}'.format(attr, val) - def test_size(self, top): """Check that the Topology is correctly sized""" assert top.n_atoms == self.expected_n_atoms, '{} atoms read, {} expected in {}'.format( @@ -100,3 +90,13 @@ def test_creates_universe(self, filename): """Check that Universe works with this Parser""" u = mda.Universe(filename) assert isinstance(u, mda.Universe) + + def test_guessed_attributes(self, filename): + """check that the universe created with certain parser have the same + guessed attributes as when it was guessed inside the parser""" + u = mda.Universe(filename) + u_guessed_attrs = [attr.attrname for attr + in u._topology.guessed_attributes] + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + assert attr in u_guessed_attrs diff --git a/testsuite/MDAnalysisTests/topology/test_crd.py b/testsuite/MDAnalysisTests/topology/test_crd.py index 846bff496f1..3062ba12f3b 100644 --- a/testsuite/MDAnalysisTests/topology/test_crd.py +++ b/testsuite/MDAnalysisTests/topology/test_crd.py @@ -27,6 +27,8 @@ CRD, ) +from numpy.testing import assert_allclose + class TestCRDParser(ParserBase): parser = mda.topology.CRDParser.CRDParser @@ -35,6 +37,17 @@ class TestCRDParser(ParserBase): 'resids', 'resnames', 'resnums', 'segids'] guessed_attrs = ['masses', 'types'] + expected_n_atoms = 3341 expected_n_residues = 214 expected_n_segments = 1 + + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + expected = [14.007, 1.008, 1.008, 1.008, 12.011, 1.008, 12.011] + assert_allclose(u.atoms.masses[:7], expected) + + def test_guessed_types(self, filename): + u = mda.Universe(filename) + expected = ['N', 'H', 'H', 'H', 'C', 'H', 'C'] + assert (u.atoms.types[:7] == expected).all() diff --git a/testsuite/MDAnalysisTests/topology/test_dlpoly.py b/testsuite/MDAnalysisTests/topology/test_dlpoly.py index 01ea6632b11..a21f7134ca1 100644 --- a/testsuite/MDAnalysisTests/topology/test_dlpoly.py +++ b/testsuite/MDAnalysisTests/topology/test_dlpoly.py @@ -20,7 +20,7 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose import pytest import MDAnalysis as mda @@ -43,14 +43,30 @@ def test_creates_universe(self, filename): u = mda.Universe(filename, topology_format=self.format) assert isinstance(u, mda.Universe) + def test_guessed_attributes(self, filename): + u = mda.Universe(filename, topology_format=self.format) + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + class DLPBase2(DLPUniverse): expected_attrs = ['ids', 'names'] - guessed_attrs = ['types', 'masses'] + guessed_attrs = ['masses', 'types'] + expected_n_atoms = 216 expected_n_residues = 1 expected_n_segments = 1 + def test_guesssed_masses(self, filename): + u = mda.Universe(filename, topology_format=self.format) + assert_allclose(u.atoms.masses[0], 39.102) + assert_allclose(u.atoms.masses[4], 35.45) + + def test_guessed_types(self, filename): + u = mda.Universe(filename, topology_format=self.format) + assert u.atoms.types[0] == 'K' + assert u.atoms.types[4] == 'CL' + def test_names(self, top): assert top.names.values[0] == 'K+' assert top.names.values[4] == 'Cl-' @@ -70,7 +86,6 @@ class TestDLPConfigParser(DLPBase2): class DLPBase(DLPUniverse): expected_attrs = ['ids', 'names'] - guessed_attrs = ['types', 'masses'] expected_n_atoms = 3 expected_n_residues = 1 expected_n_segments = 1 diff --git a/testsuite/MDAnalysisTests/topology/test_dms.py b/testsuite/MDAnalysisTests/topology/test_dms.py index 2740c392d26..d9f7944aaa0 100644 --- a/testsuite/MDAnalysisTests/topology/test_dms.py +++ b/testsuite/MDAnalysisTests/topology/test_dms.py @@ -21,7 +21,6 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # import MDAnalysis as mda - from MDAnalysisTests.topology.base import ParserBase from MDAnalysisTests.datafiles import DMS_DOMAINS, DMS_NO_SEGID @@ -62,6 +61,11 @@ def test_atomsels(self, filename): s5 = u.select_atoms("resname ALA") assert len(s5) == 190 + def test_guessed_types(self, filename): + u = mda.Universe(filename) + expected = ['N', 'H', 'H', 'H', 'C', 'H', 'C'] + assert (u.atoms.types[:7] == expected).all() + class TestDMSParserNoSegid(TestDMSParser): ref_filename = DMS_NO_SEGID diff --git a/testsuite/MDAnalysisTests/topology/test_fhiaims.py b/testsuite/MDAnalysisTests/topology/test_fhiaims.py index 4596ccfa2bc..39097473871 100644 --- a/testsuite/MDAnalysisTests/topology/test_fhiaims.py +++ b/testsuite/MDAnalysisTests/topology/test_fhiaims.py @@ -20,13 +20,14 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -from numpy.testing import assert_equal, assert_almost_equal +from numpy.testing import assert_equal, assert_allclose import MDAnalysis as mda from MDAnalysisTests.topology.base import ParserBase from MDAnalysisTests.datafiles import FHIAIMS + class TestFHIAIMS(ParserBase): parser = mda.topology.FHIAIMSParser.FHIAIMSParser expected_attrs = ['names', 'elements'] @@ -40,15 +41,17 @@ def test_names(self, top): assert_equal(top.names.values, ['O', 'H', 'H', 'O', 'H', 'H']) - def test_types(self, top): - assert_equal(top.types.values, + def test_guessed_types(self, filename): + u = mda.Universe(filename) + assert_equal(u.atoms.types, ['O', 'H', 'H', 'O', 'H', 'H']) + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + assert_allclose(u.atoms.masses, + [15.999, 1.008, 1.008, 15.999, + 1.008, 1.008]) + def test_elements(self, top): assert_equal(top.elements.values, ['O', 'H', 'H', 'O', 'H', 'H']) - - def test_masses(self, top): - assert_almost_equal(top.masses.values, - [15.999, 1.008, 1.008, 15.999, - 1.008, 1.008]) diff --git a/testsuite/MDAnalysisTests/topology/test_gms.py b/testsuite/MDAnalysisTests/topology/test_gms.py index 87b1795b352..cb187adad37 100644 --- a/testsuite/MDAnalysisTests/topology/test_gms.py +++ b/testsuite/MDAnalysisTests/topology/test_gms.py @@ -20,7 +20,7 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose import MDAnalysis as mda @@ -52,6 +52,16 @@ def test_types(self, top): assert_equal(top.atomiccharges.values, [8, 1, 1, 8, 1, 1]) + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + expected = [15.999, 1.008, 1.008, 15.999, 1.008, 1.008] + assert_allclose(u.atoms.masses, expected) + + def test_guessed_types(self, filename): + u = mda.Universe(filename) + expected = ['O', 'H', 'H', 'O', 'H', 'H'] + assert (u.atoms.types == expected).all() + class TestGMSSYMOPT(GMSBase): expected_n_atoms = 4 diff --git a/testsuite/MDAnalysisTests/topology/test_gro.py b/testsuite/MDAnalysisTests/topology/test_gro.py index 6a457cecb8d..f95deea52b0 100644 --- a/testsuite/MDAnalysisTests/topology/test_gro.py +++ b/testsuite/MDAnalysisTests/topology/test_gro.py @@ -34,13 +34,13 @@ GRO_residwrap_0base, GRO_sameresid_diffresname, ) -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose class TestGROParser(ParserBase): parser = mda.topology.GROParser.GROParser ref_filename = GRO - expected_attrs = ['ids', 'names', 'resids', 'resnames', 'masses'] + expected_attrs = ['ids', 'names', 'resids', 'resnames'] guessed_attrs = ['masses', 'types'] expected_n_atoms = 47681 expected_n_residues = 11302 @@ -52,6 +52,16 @@ def test_attr_size(self, top): assert len(top.resids) == top.n_residues assert len(top.resnames) == top.n_residues + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + expected = [14.007, 1.008, 1.008, 1.008, 12.011, 1.008, 12.011] + assert_allclose(u.atoms.masses[:7], expected) + + def test_guessed_types(self, filename): + u = mda.Universe(filename) + expected = ['N', 'H', 'H', 'H', 'C', 'H', 'C'] + assert_equal(u.atoms.types[:7], expected) + class TestGROWideBox(object): """Tests for Issue #548""" @@ -75,6 +85,7 @@ def test_parse_missing_atomname_IOerror(): with pytest.raises(IOError): p.parse() + class TestGroResidWrapping(object): # resid is 5 digit field, so is limited to 100k # check that parser recognises when resids have wrapped diff --git a/testsuite/MDAnalysisTests/topology/test_gsd.py b/testsuite/MDAnalysisTests/topology/test_gsd.py index 41c3cb8b81d..b80df4ede11 100644 --- a/testsuite/MDAnalysisTests/topology/test_gsd.py +++ b/testsuite/MDAnalysisTests/topology/test_gsd.py @@ -28,7 +28,6 @@ from MDAnalysisTests.topology.base import ParserBase from MDAnalysisTests.datafiles import GSD from MDAnalysisTests.datafiles import GSD_bonds -from numpy.testing import assert_equal import os @@ -36,19 +35,19 @@ class GSDBase(ParserBase): parser = mda.topology.GSDParser.GSDParser expected_attrs = ['ids', 'names', 'resids', 'resnames', 'masses', - 'charges', 'radii', + 'charges', 'radii', 'types', 'bonds', 'angles', 'dihedrals', 'impropers'] expected_n_bonds = 0 expected_n_angles = 0 expected_n_dihedrals = 0 expected_n_impropers = 0 - + def test_attr_size(self, top): assert len(top.ids) == top.n_atoms assert len(top.names) == top.n_atoms assert len(top.resids) == top.n_residues assert len(top.resnames) == top.n_residues - + def test_atoms(self, top): assert top.n_atoms == self.expected_n_atoms @@ -72,7 +71,7 @@ def test_dihedrals(self, top): assert isinstance(top.angles.values[0], tuple) else: assert top.dihedrals.values == [] - + def test_impropers(self, top): assert len(top.impropers.values) == self.expected_n_impropers if self.expected_n_impropers: diff --git a/testsuite/MDAnalysisTests/topology/test_guessers.py b/testsuite/MDAnalysisTests/topology/test_guessers.py deleted file mode 100644 index 1d946f22c8c..00000000000 --- a/testsuite/MDAnalysisTests/topology/test_guessers.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 -# -# 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 -# -import pytest -from numpy.testing import assert_equal -import numpy as np - -import MDAnalysis as mda -from MDAnalysis.topology import guessers -from MDAnalysis.core.topologyattrs import Angles - -from MDAnalysisTests import make_Universe -from MDAnalysisTests.core.test_fragments import make_starshape -import MDAnalysis.tests.datafiles as datafiles - -from MDAnalysisTests.util import import_not_available - - -try: - from rdkit import Chem - from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges -except ImportError: - pass - -requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), - reason="requires RDKit") - - -class TestGuessMasses(object): - def test_guess_masses(self): - out = guessers.guess_masses(['C', 'C', 'H']) - - assert isinstance(out, np.ndarray) - assert_equal(out, np.array([12.011, 12.011, 1.008])) - - def test_guess_masses_warn(self): - with pytest.warns(UserWarning): - guessers.guess_masses(['X']) - - def test_guess_masses_miss(self): - out = guessers.guess_masses(['X', 'Z']) - assert_equal(out, np.array([0.0, 0.0])) - - @pytest.mark.parametrize('element, value', (('H', 1.008), ('XYZ', 0.0), )) - def test_get_atom_mass(self, element, value): - assert guessers.get_atom_mass(element) == value - - def test_guess_atom_mass(self): - assert guessers.guess_atom_mass('1H') == 1.008 - - -class TestGuessTypes(object): - # guess_types - # guess_atom_type - # guess_atom_element - def test_guess_types(self): - out = guessers.guess_types(['MG2+', 'C12']) - - assert isinstance(out, np.ndarray) - assert_equal(out, np.array(['MG', 'C'], dtype=object)) - - def test_guess_atom_element(self): - assert guessers.guess_atom_element('MG2+') == 'MG' - - def test_guess_atom_element_empty(self): - assert guessers.guess_atom_element('') == '' - - def test_guess_atom_element_singledigit(self): - assert guessers.guess_atom_element('1') == '1' - - def test_guess_atom_element_1H(self): - assert guessers.guess_atom_element('1H') == 'H' - assert guessers.guess_atom_element('2H') == 'H' - - @pytest.mark.parametrize('name, element', ( - ('AO5*', 'O'), - ('F-', 'F'), - ('HB1', 'H'), - ('OC2', 'O'), - ('1he2', 'H'), - ('3hg2', 'H'), - ('OH-', 'O'), - ('HO', 'H'), - ('he', 'H'), - ('zn', 'ZN'), - ('Ca2+', 'CA'), - ('CA', 'C'), - ('N0A', 'N'), - ('C0U', 'C'), - ('C0S', 'C'), - ('Na+', 'NA'), - ('Cu2+', 'CU') - )) - def test_guess_element_from_name(self, name, element): - assert guessers.guess_atom_element(name) == element - - -def test_guess_charge(): - # this always returns 0.0 - assert guessers.guess_atom_charge('this') == 0.0 - - -def test_guess_bonds_Error(): - u = make_Universe(trajectory=True) - with pytest.raises(ValueError): - guessers.guess_bonds(u.atoms[:4], u.atoms.positions[:5]) - - -def test_guess_impropers(): - u = make_starshape() - - ag = u.atoms[:5] - - u.add_TopologyAttr(Angles(guessers.guess_angles(ag.bonds))) - - vals = guessers.guess_improper_dihedrals(ag.angles) - assert_equal(len(vals), 12) - - -def bond_sort(arr): - # sort from low to high, also within a tuple - # e.g. ([5, 4], [0, 1], [0, 3]) -> ([0, 1], [0, 3], [4, 5]) - out = [] - for (i, j) in arr: - if i > j: - i, j = j, i - out.append((i, j)) - return sorted(out) - -def test_guess_bonds_water(): - u = mda.Universe(datafiles.two_water_gro) - bonds = bond_sort(guessers.guess_bonds(u.atoms, u.atoms.positions, u.dimensions)) - assert_equal(bonds, ((0, 1), - (0, 2), - (3, 4), - (3, 5))) - -def test_guess_bonds_adk(): - u = mda.Universe(datafiles.PSF, datafiles.DCD) - u.atoms.types = guessers.guess_types(u.atoms.names) - bonds = bond_sort(guessers.guess_bonds(u.atoms, u.atoms.positions)) - assert_equal(np.sort(u.bonds.indices, axis=0), - np.sort(bonds, axis=0)) - -def test_guess_bonds_peptide(): - u = mda.Universe(datafiles.PSF_NAMD, datafiles.PDB_NAMD) - u.atoms.types = guessers.guess_types(u.atoms.names) - bonds = bond_sort(guessers.guess_bonds(u.atoms, u.atoms.positions)) - assert_equal(np.sort(u.bonds.indices, axis=0), - np.sort(bonds, axis=0)) - - -@pytest.mark.parametrize("smi", [ - "c1ccccc1", - "C1=CC=CC=C1", - "CCO", - "c1ccccc1Cc1ccccc1", - "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", -]) -@requires_rdkit -def test_guess_aromaticities(smi): - mol = Chem.MolFromSmiles(smi) - mol = Chem.AddHs(mol) - expected = np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) - u = mda.Universe(mol) - values = guessers.guess_aromaticities(u.atoms) - assert_equal(values, expected) - - -@pytest.mark.parametrize("smi", [ - "c1ccccc1", - "C1=CC=CC=C1", - "CCO", - "c1ccccc1Cc1ccccc1", - "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", -]) -@requires_rdkit -def test_guess_gasteiger_charges(smi): - mol = Chem.MolFromSmiles(smi) - mol = Chem.AddHs(mol) - ComputeGasteigerCharges(mol, throwOnParamFailure=True) - expected = np.array([atom.GetDoubleProp("_GasteigerCharge") - for atom in mol.GetAtoms()], dtype=np.float32) - u = mda.Universe(mol) - values = guessers.guess_gasteiger_charges(u.atoms) - assert_equal(values, expected) diff --git a/testsuite/MDAnalysisTests/topology/test_hoomdxml.py b/testsuite/MDAnalysisTests/topology/test_hoomdxml.py index 018470647f4..d85a25c8465 100644 --- a/testsuite/MDAnalysisTests/topology/test_hoomdxml.py +++ b/testsuite/MDAnalysisTests/topology/test_hoomdxml.py @@ -21,7 +21,6 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # from numpy.testing import assert_almost_equal - import MDAnalysis as mda from MDAnalysisTests.topology.base import ParserBase @@ -34,6 +33,7 @@ class TestHoomdXMLParser(ParserBase): expected_attrs = [ 'types', 'masses', 'charges', 'radii', 'bonds', 'angles', 'dihedrals', 'impropers' ] + expected_n_atoms = 769 expected_n_residues = 1 expected_n_segments = 1 diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index 035c05bb98a..e5cea0e215d 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -49,6 +49,9 @@ class BaseITP(ParserBase): 'resids', 'resnames', 'segids', 'moltypes', 'molnums', 'bonds', 'angles', 'dihedrals', 'impropers'] + + guessed_attrs = ['elements', ] + expected_n_atoms = 63 expected_n_residues = 10 expected_n_segments = 1 @@ -64,13 +67,13 @@ def universe(self, filename): def test_bonds_total_counts(self, top): assert len(top.bonds.values) == self.expected_n_bonds - + def test_angles_total_counts(self, top): assert len(top.angles.values) == self.expected_n_angles def test_dihedrals_total_counts(self, top): assert len(top.dihedrals.values) == self.expected_n_dihedrals - + def test_impropers_total_counts(self, top): assert len(top.impropers.values) == self.expected_n_impropers @@ -86,7 +89,7 @@ class TestITP(BaseITP): expected_n_angles = 91 expected_n_dihedrals = 30 expected_n_impropers = 29 - + def test_bonds_atom_counts(self, universe): assert len(universe.atoms[[0]].bonds) == 3 assert len(universe.atoms[[42]].bonds) == 1 @@ -95,7 +98,7 @@ def test_bonds_values(self, top): vals = top.bonds.values for b in ((0, 1), (0, 2), (0, 3), (3, 4)): assert b in vals - + def test_bonds_type(self, universe): assert universe.bonds[0].type == 2 @@ -107,7 +110,7 @@ def test_angles_values(self, top): vals = top.angles.values for b in ((1, 0, 2), (1, 0, 3), (2, 0, 3)): assert (b in vals) or (b[::-1] in vals) - + def test_angles_type(self, universe): assert universe.angles[0].type == 2 @@ -123,7 +126,7 @@ def test_dihedrals_values(self, top): vals = top.dihedrals.values for b in ((1, 0, 3, 5), (0, 3, 5, 7)): assert (b in vals) or (b[::-1] in vals) - + def test_dihedrals_type(self, universe): assert universe.dihedrals[0].type == (1, 1) @@ -134,7 +137,7 @@ def test_impropers_values(self, top): vals = top.impropers.values for b in ((3, 0, 5, 4), (5, 3, 7, 6)): assert (b in vals) or (b[::-1] in vals) - + def test_impropers_type(self, universe): assert universe.impropers[0].type == 2 @@ -142,12 +145,13 @@ def test_impropers_type(self, universe): class TestITPNoMass(ParserBase): parser = mda.topology.ITPParser.ITPParser ref_filename = ITP_nomass - expected_attrs = ['ids', 'names', 'types', 'masses', + expected_attrs = ['ids', 'names', 'types', 'charges', 'chargegroups', 'resids', 'resnames', 'segids', 'moltypes', 'molnums', - 'bonds', 'angles', 'dihedrals', 'impropers'] - guessed_attrs = ['masses'] + 'bonds', 'angles', 'dihedrals', 'impropers', 'masses', ] + guessed_attrs = ['elements', ] + expected_n_atoms = 60 expected_n_residues = 1 expected_n_segments = 1 @@ -157,18 +161,18 @@ def universe(self, filename): return mda.Universe(filename) def test_mass_guess(self, universe): - assert universe.atoms[0].mass not in ('', None) + assert not np.isnan(universe.atoms[0].mass) class TestITPAtomtypes(ParserBase): parser = mda.topology.ITPParser.ITPParser ref_filename = ITP_atomtypes - expected_attrs = ['ids', 'names', 'types', 'masses', + expected_attrs = ['ids', 'names', 'types', 'charges', 'chargegroups', - 'resids', 'resnames', + 'resids', 'resnames', 'masses', 'segids', 'moltypes', 'molnums', 'bonds', 'angles', 'dihedrals', 'impropers'] - guessed_attrs = ['masses'] + expected_n_atoms = 4 expected_n_residues = 1 expected_n_segments = 1 @@ -202,7 +206,8 @@ class TestITPCharges(ParserBase): 'resids', 'resnames', 'segids', 'moltypes', 'molnums', 'bonds', 'angles', 'dihedrals', 'impropers'] - guessed_attrs = [] + guessed_attrs = ['elements', ] + expected_n_atoms = 9 expected_n_residues = 3 expected_n_segments = 1 @@ -220,6 +225,7 @@ def test_charge_parse(self, universe): def test_masses_are_read(self, universe): assert_allclose(universe.atoms.masses, [100] * 9) + class TestDifferentDirectivesITP(BaseITP): ref_filename = ITP_edited @@ -245,6 +251,13 @@ def test_dihedrals_identity(self, universe): class TestITPNoKeywords(BaseITP): + expected_attrs = ['ids', 'names', 'types', + 'charges', 'chargegroups', + 'resids', 'resnames', + 'segids', 'moltypes', 'molnums', + 'bonds', 'angles', 'dihedrals', 'impropers', 'masses', ] + guessed_attrs = ['elements', 'masses', ] + """ Test reading ITP files *without* defined keywords. @@ -253,7 +266,7 @@ class TestITPNoKeywords(BaseITP): #ifndef HW1_CHARGE #define HW1_CHARGE 0.241 #endif - + [ atoms ] 1 opls_118 1 SOL OW 1 0 2 opls_119 1 SOL HW1 1 HW1_CHARGE @@ -263,8 +276,6 @@ class TestITPNoKeywords(BaseITP): expected_n_residues = 1 expected_n_segments = 1 - guessed_attrs = ['masses'] - expected_n_bonds = 2 # FLEXIBLE not set -> SETTLE constraint -> water has no angle expected_n_angles = 0 @@ -284,7 +295,12 @@ def test_defines(self, top): assert_allclose(top.charges.values[1], 0.241) assert_allclose(top.charges.values[2], 0.241) - + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + assert_allclose(u.atoms.masses, + [15.999, 15.999, 15.999, 15.999, 15.999]) + + class TestITPKeywords(TestITPNoKeywords): """ Test reading ITP files *with* defined keywords. @@ -296,13 +312,13 @@ class TestITPKeywords(TestITPNoKeywords): @pytest.fixture def universe(self, filename): - return mda.Universe(filename, FLEXIBLE=True, EXTRA_ATOMS=True, + return mda.Universe(filename, FLEXIBLE=True, EXTRA_ATOMS=True, HW1_CHARGE=1, HW2_CHARGE=3) @pytest.fixture() def top(self, filename): with self.parser(filename) as p: - yield p.parse(FLEXIBLE=True, EXTRA_ATOMS=True, + yield p.parse(FLEXIBLE=True, EXTRA_ATOMS=True, HW1_CHARGE=1, HW2_CHARGE=3) def test_whether_settles_types(self, universe): @@ -341,7 +357,7 @@ def universe(self, filename): def top(self, filename): with self.parser(filename) as p: yield p.parse(HEAVY_H=True, EXTRA_ATOMS=True, HEAVY_SIX=True) - + def test_heavy_atom(self, universe): assert universe.atoms[5].mass > 40 @@ -380,6 +396,13 @@ def test_creates_universe(self, filename): """Check that Universe works with this Parser""" u = mda.Universe(filename, topology_format='ITP', include_dir=GMX_DIR) + def test_guessed_attributes(self, filename): + """check that the universe created with certain parser have the same + guessed attributes as when it was guessed inside the parser""" + u = mda.Universe(filename, topology_format='ITP', include_dir=GMX_DIR) + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + def test_sequential(self, universe): resids = np.array(list(range(2, 12)) + list(range(13, 23))) assert_equal(universe.residues.resids[:20], resids) @@ -453,3 +476,17 @@ def test_relative_path(self, tmpdir): with subsubdir.as_cwd(): u = mda.Universe("../test.itp") assert len(u.atoms) == 1 + + +def test_missing_elements_no_attribute(): + """Check that: + + 1) a warning is raised if elements are missing + 2) the elements attribute is not set + """ + wmsg = ("Element information is missing, elements attribute " + "will not be populated. If needed these can be ") + with pytest.warns(UserWarning, match=wmsg): + u = mda.Universe(ITP_atomtypes) + with pytest.raises(AttributeError): + _ = u.atoms.elements diff --git a/testsuite/MDAnalysisTests/topology/test_lammpsdata.py b/testsuite/MDAnalysisTests/topology/test_lammpsdata.py index 44a8af7236d..2f65eda3cbe 100644 --- a/testsuite/MDAnalysisTests/topology/test_lammpsdata.py +++ b/testsuite/MDAnalysisTests/topology/test_lammpsdata.py @@ -21,7 +21,7 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # import pytest -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose import numpy as np from io import StringIO @@ -92,6 +92,11 @@ def test_improper_member(self, top): def test_creates_universe(self, filename): u = mda.Universe(filename, format='DATA') + def test_guessed_attributes(self, filename): + u = mda.Universe(filename, format='DATA') + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + class TestLammpsData(LammpsBase): """Tests the reading of lammps .data topology files. @@ -263,8 +268,7 @@ def test_interpret_atom_style_missing(): class TestDumpParser(ParserBase): - expected_attrs = ['types'] - guessed_attrs = ['masses'] + expected_attrs = ['types', 'masses'] expected_n_atoms = 24 expected_n_residues = 1 expected_n_segments = 1 @@ -284,13 +288,28 @@ def test_masses_warning(self): with self.parser(self.ref_filename) as p: with pytest.warns(UserWarning, match='Guessed all Masses to 1.0'): p.parse() - + + def test_guessed_attributes(self, filename): + u = mda.Universe(filename, format='LAMMPSDUMP') + for attr in self.guessed_attrs: + assert hasattr(u.atoms, attr) + def test_id_ordering(self): # ids are nonsequential in file, but should get rearranged u = mda.Universe(self.ref_filename, format='LAMMPSDUMP') # the 4th in file has id==13, but should have been sorted assert u.atoms[3].id == 4 + def test_guessed_masses(self, filename): + u = mda.Universe(filename, format='LAMMPSDUMP') + expected = [1., 1., 1., 1., 1., 1., 1.] + assert_allclose(u.atoms.masses[:7], expected) + + def test_guessed_types(self, filename): + u = mda.Universe(filename, format='LAMMPSDUMP') + expected = ['2', '1', '1', '2', '1', '1', '2'] + assert (u.atoms.types[:7] == expected).all() + # this tests that topology can still be constructed if non-standard or uneven # column present. class TestDumpParserLong(TestDumpParser): diff --git a/testsuite/MDAnalysisTests/topology/test_minimal.py b/testsuite/MDAnalysisTests/topology/test_minimal.py index 6c275f0217c..60f009b44b0 100644 --- a/testsuite/MDAnalysisTests/topology/test_minimal.py +++ b/testsuite/MDAnalysisTests/topology/test_minimal.py @@ -60,7 +60,7 @@ def test_minimal_parser(filename, expected_n_atoms): @working_readers def test_universe_with_minimal(filename, expected_n_atoms): - u = mda.Universe(filename) + u = mda.Universe(filename, to_guess=()) assert len(u.atoms) == expected_n_atoms @@ -81,7 +81,7 @@ def test_minimal_parser_fail(filename,n_atoms): @nonworking_readers def test_minimal_n_atoms_kwarg(filename, n_atoms): # test that these can load when we supply the number of atoms - u = mda.Universe(filename, n_atoms=n_atoms) + u = mda.Universe(filename, n_atoms=n_atoms, to_guess=()) assert len(u.atoms) == n_atoms @@ -107,6 +107,6 @@ def test_memory_minimal_parser(array, order): @memory_reader def test_memory_universe(array, order): - u = mda.Universe(array, order=order) + u = mda.Universe(array, order=order, to_guess=()) assert len(u.atoms) == 10 diff --git a/testsuite/MDAnalysisTests/topology/test_mmtf.py b/testsuite/MDAnalysisTests/topology/test_mmtf.py index b05bd1767a4..9e2a85f7784 100644 --- a/testsuite/MDAnalysisTests/topology/test_mmtf.py +++ b/testsuite/MDAnalysisTests/topology/test_mmtf.py @@ -1,5 +1,5 @@ import pytest -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose import mmtf from unittest import mock @@ -9,6 +9,7 @@ from MDAnalysisTests.topology.base import ParserBase from MDAnalysisTests.datafiles import MMTF, MMTF_gz, MMTF_skinny, MMTF_skinny2 + class MMTFBase(ParserBase): expected_attrs = [ 'ids', 'names', 'types', 'altLocs', 'tempfactors', 'occupancies', @@ -44,7 +45,7 @@ class TestMMTFSkinny(MMTFBase): expected_n_residues = 134 expected_n_segments = 2 - + class TestMMTFSkinny2(MMTFBase): parser = mda.topology.MMTFParser.MMTFParser ref_filename = MMTF_skinny2 @@ -96,6 +97,10 @@ def test_icodes(self, u): def test_altlocs(self, u): assert all(u.atoms.altLocs[:3] == '') + def test_guessed_masses(self, u): + expected = [15.999, 12.011, 12.011, 15.999, 12.011, 15.999, 12.011] + assert_allclose(u.atoms.masses[:7], expected) + class TestMMTFUniverseFromDecoder(TestMMTFUniverse): @pytest.fixture() @@ -119,6 +124,10 @@ def test_universe_models(self, u): assert isinstance(m, AtomGroup) assert len(m) == 570 + def test_guessed_masses(self, u): + expected = [15.999, 12.011, 12.011, 15.999, 12.011, 15.999, 12.011] + assert_allclose(u.atoms.masses[:7], expected) + class TestMMTFgzUniverseFromDecoder(TestMMTFgzUniverse): @pytest.fixture() @@ -128,7 +137,7 @@ def u(self): class TestSelectModels(object): - # tests for 'model' keyword in select_atoms + # tests for 'model' keyword in select_atoms @pytest.fixture() def u(self): return mda.Universe(MMTF_gz) diff --git a/testsuite/MDAnalysisTests/topology/test_mol2.py b/testsuite/MDAnalysisTests/topology/test_mol2.py index 7378cdb01f9..b6084b861ef 100644 --- a/testsuite/MDAnalysisTests/topology/test_mol2.py +++ b/testsuite/MDAnalysisTests/topology/test_mol2.py @@ -23,11 +23,12 @@ from io import StringIO import numpy as np -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose import pytest import MDAnalysis as mda from MDAnalysisTests.topology.base import ParserBase + from MDAnalysisTests.datafiles import ( mol2_molecule, mol2_molecules, @@ -178,6 +179,7 @@ class TestMOL2Base(ParserBase): 'ids', 'names', 'types', 'charges', 'resids', 'resnames', 'bonds', 'elements', ] + guessed_attrs = ['masses'] expected_n_atoms = 49 expected_n_residues = 1 @@ -235,11 +237,16 @@ def test_wrong_elements_warnings(): with pytest.warns(UserWarning, match='Unknown elements found') as record: u = mda.Universe(StringIO(mol2_wrong_element), format='MOL2') - # One warning from invalid elements, one from invalid masses - assert len(record) == 2 + # One warning from invalid elements, one from masses PendingDeprecationWarning + assert len(record) == 3 - expected = np.array(['N', '', ''], dtype=object) - assert_equal(u.atoms.elements, expected) + expected_elements = np.array(['N', '', ''], dtype=object) + guseed_masses = np.array([14.007, 0.0, 0.0], dtype=float) + gussed_types = np.array(['N.am', 'X.o2', 'XX.am']) + + assert_equal(u.atoms.elements, expected_elements) + assert_equal(u.atoms.types, gussed_types) + assert_allclose(u.atoms.masses, guseed_masses) def test_all_wrong_elements_warnings(): @@ -301,3 +308,9 @@ def test_unformat(): with pytest.raises(ValueError, match='Some atoms in the mol2 file'): u = mda.Universe(StringIO(mol2_resname_unformat), format='MOL2') + + +def test_guessed_masses(): + u = mda.Universe(mol2_molecules) + assert_allclose(u.atoms.masses[:7], [14.007, 32.06, + 14.007, 14.007, 15.999, 15.999, 12.011]) diff --git a/testsuite/MDAnalysisTests/topology/test_pdb.py b/testsuite/MDAnalysisTests/topology/test_pdb.py index 01fbcd8a5a4..c176d50be13 100644 --- a/testsuite/MDAnalysisTests/topology/test_pdb.py +++ b/testsuite/MDAnalysisTests/topology/test_pdb.py @@ -24,7 +24,7 @@ import pytest import numpy as np -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose import MDAnalysis as mda from MDAnalysisTests.topology.base import ParserBase @@ -43,6 +43,7 @@ ) from MDAnalysis.topology.PDBParser import PDBParser from MDAnalysis import NoDataError +from MDAnalysis.guesser import tables _PDBPARSER = mda.topology.PDBParser.PDBParser @@ -252,8 +253,6 @@ def test_PDB_hex(): @pytest.mark.filterwarnings("error:Failed to guess the mass") def test_PDB_metals(): - from MDAnalysis.topology import tables - u = mda.Universe(StringIO(PDB_metals), format='PDB') assert len(u.atoms) == 4 @@ -310,11 +309,32 @@ def test_wrong_elements_warnings(): column which have been parsed and returns an appropriate warning. """ with pytest.warns(UserWarning, match='Unknown element XX found'): - u = mda.Universe(StringIO(PDB_wrong_ele), format='PDB') + u = mda.Universe(StringIO(PDB_wrong_ele,), format='PDB') + + expected_elements = np.array(['N', '', 'C', 'O', '', 'Cu', 'Fe', 'Mg'], + dtype=object) + gussed_types = np.array(['N', '', 'C', 'O', 'XX', 'CU', 'Fe', 'MG']) + guseed_masses = np.array([14.007, 0.0, 12.011, 15.999, 0.0, + 63.546, 55.847, 24.305], dtype=float) + + assert_equal(u.atoms.elements, expected_elements) + assert_equal(u.atoms.types, gussed_types) + assert_allclose(u.atoms.masses, guseed_masses) - expected = np.array(['N', '', 'C', 'O', '', 'Cu', 'Fe', 'Mg'], - dtype=object) - assert_equal(u.atoms.elements, expected) + +def test_guessed_masses_and_types_values(): + """Test that guessed masses and types have the expected values for universe + constructed from PDB file. + """ + u = mda.Universe(PDB, format='PDB') + gussed_types = np.array(['N', 'H', 'H', 'H', 'C', 'H', 'C', 'H', 'H', 'C']) + guseed_masses = [14.007, 1.008, 1.008, 1.008, + 12.011, 1.008, 12.011, 1.008, 1.008, 12.011] + failed_type_guesses = u.atoms.types == "" + + assert_allclose(u.atoms.masses[:10], guseed_masses) + assert_equal(u.atoms.types[:10], gussed_types) + assert not failed_type_guesses.any() def test_nobonds_error(): diff --git a/testsuite/MDAnalysisTests/topology/test_pdbqt.py b/testsuite/MDAnalysisTests/topology/test_pdbqt.py index c81f60cdc80..b2511a889e2 100644 --- a/testsuite/MDAnalysisTests/topology/test_pdbqt.py +++ b/testsuite/MDAnalysisTests/topology/test_pdbqt.py @@ -23,11 +23,14 @@ import MDAnalysis as mda from MDAnalysisTests.topology.base import ParserBase + from MDAnalysisTests.datafiles import ( PDBQT_input, # pdbqt_inputpdbqt.pdbqt PDBQT_tyrosol, # tyrosol.pdbqt.bz2 ) +from numpy.testing import assert_allclose + class TestPDBQT(ParserBase): parser = mda.topology.PDBQTParser.PDBQTParser @@ -47,11 +50,17 @@ class TestPDBQT(ParserBase): "occupancies", "tempfactors", ] + guessed_attrs = ['masses'] expected_n_atoms = 1805 expected_n_residues = 199 # resids go 2-102 then 2-99 expected_n_segments = 2 # res2-102 are A, 2-99 are B + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + assert_allclose(u.atoms.masses[:7], [14.007, 0., + 0., 12.011, 12.011, 0., 12.011]) + def test_footnote(): """just test that the Universe is built even in the presence of a diff --git a/testsuite/MDAnalysisTests/topology/test_pqr.py b/testsuite/MDAnalysisTests/topology/test_pqr.py index 569b964e3ba..aa03d789ac4 100644 --- a/testsuite/MDAnalysisTests/topology/test_pqr.py +++ b/testsuite/MDAnalysisTests/topology/test_pqr.py @@ -21,8 +21,7 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # from io import StringIO -from numpy.testing import assert_equal, assert_almost_equal -import pytest +from numpy.testing import assert_equal, assert_almost_equal, assert_allclose import MDAnalysis as mda from MDAnalysisTests.topology.base import ParserBase @@ -52,12 +51,26 @@ def test_attr_size(self, top): assert len(top.resnames) == top.n_residues assert len(top.segids) == top.n_segments + expected_masses = [14.007, 1.008, 1.008, 1.008, 12.011, 1.008, 12.011] + expected_types = ['N', 'H', 'H', 'H', 'C', 'H', 'C'] + + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + assert_allclose(u.atoms.masses[:7], self.expected_masses) + + def test_guessed_types(self, filename): + u = mda.Universe(filename) + assert (u.atoms.types[:7] == self.expected_types).all() + class TestPQRParser2(TestPQRParser): ref_filename = PQR_icodes expected_n_atoms = 5313 expected_n_residues = 474 + expected_masses = [14.007, 12.011, 12.011, 15.999, 12.011, 12.011, 12.011] + expected_types = ['N', 'C', 'C', 'O', 'C', 'C', 'C'] + def test_record_types(): u = mda.Universe(PQR_icodes) @@ -81,6 +94,7 @@ def test_record_types(): ENDMDL ''' + def test_gromacs_flavour(): u = mda.Universe(StringIO(GROMACS_PQR), format='PQR') @@ -88,7 +102,6 @@ def test_gromacs_flavour(): # topology things assert u.atoms[0].type == 'O' assert u.atoms[0].segid == 'SYSTEM' - assert not u._topology.types.is_guessed assert_almost_equal(u.atoms[0].radius, 1.48, decimal=5) assert_almost_equal(u.atoms[0].charge, -0.67, decimal=5) # coordinatey things diff --git a/testsuite/MDAnalysisTests/topology/test_psf.py b/testsuite/MDAnalysisTests/topology/test_psf.py index 72a4622c1b7..895f4185146 100644 --- a/testsuite/MDAnalysisTests/topology/test_psf.py +++ b/testsuite/MDAnalysisTests/topology/test_psf.py @@ -148,7 +148,7 @@ def test_angles_total_counts(self, top): def test_dihedrals_total_counts(self, top): assert len(top.dihedrals.values) == 0 - + def test_impropers_total_counts(self, top): assert len(top.impropers.values) == 0 diff --git a/testsuite/MDAnalysisTests/topology/test_tprparser.py b/testsuite/MDAnalysisTests/topology/test_tprparser.py index 023a52dee4e..bd1444a5661 100644 --- a/testsuite/MDAnalysisTests/topology/test_tprparser.py +++ b/testsuite/MDAnalysisTests/topology/test_tprparser.py @@ -73,6 +73,8 @@ class TPRAttrs(ParserBase): parser = MDAnalysis.topology.TPRParser.TPRParser expected_attrs = [ "ids", + "types", + "masses", "names", "elements", "resids", diff --git a/testsuite/MDAnalysisTests/topology/test_txyz.py b/testsuite/MDAnalysisTests/topology/test_txyz.py index 17ca447e9bc..72ab11d6525 100644 --- a/testsuite/MDAnalysisTests/topology/test_txyz.py +++ b/testsuite/MDAnalysisTests/topology/test_txyz.py @@ -26,12 +26,14 @@ from MDAnalysisTests.topology.base import ParserBase from MDAnalysisTests.datafiles import TXYZ, ARC, ARC_PBC -from numpy.testing import assert_equal +from numpy.testing import assert_equal, assert_allclose + class TestTXYZParser(ParserBase): parser = mda.topology.TXYZParser.TXYZParser guessed_attrs = ['masses'] expected_attrs = ['ids', 'names', 'bonds', 'types', 'elements'] + expected_n_residues = 1 expected_n_atoms = 9 expected_n_segments = 1 @@ -60,18 +62,24 @@ def test_TXYZ_elements(): u = mda.Universe(TXYZ, format='TXYZ') element_list = np.array(['C', 'H', 'H', 'O', 'H', 'C', 'H', 'H', 'H'], dtype=object) assert_equal(u.atoms.elements, element_list) - - + + def test_missing_elements_noattribute(): """Check that: 1) a warning is raised if elements are missing 2) the elements attribute is not set """ - wmsg = ("Element information is missing, elements attribute will not be " - "populated") + wmsg = ("Element information is missing, elements attribute " + "will not be populated. If needed these can be ") with pytest.warns(UserWarning, match=wmsg): u = mda.Universe(ARC_PBC) with pytest.raises(AttributeError): _ = u.atoms.elements + +def test_guessed_masses(): + u = mda.Universe(TXYZ) + expected = [12.011, 1.008, 1.008, 15.999, 1.008, 12.011, + 1.008, 1.008, 1.008] + assert_allclose(u.atoms.masses, expected) diff --git a/testsuite/MDAnalysisTests/topology/test_xpdb.py b/testsuite/MDAnalysisTests/topology/test_xpdb.py index 9bce73cd444..617d5caf7bf 100644 --- a/testsuite/MDAnalysisTests/topology/test_xpdb.py +++ b/testsuite/MDAnalysisTests/topology/test_xpdb.py @@ -27,6 +27,8 @@ XPDB_small, ) +from numpy.testing import assert_equal, assert_allclose + class TestXPDBParser(ParserBase): parser = mda.topology.ExtendedPDBParser.ExtendedPDBParser @@ -38,3 +40,13 @@ class TestXPDBParser(ParserBase): expected_n_atoms = 5 expected_n_residues = 5 expected_n_segments = 1 + + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + expected = [15.999, 15.999, 15.999, 15.999, 15.999] + assert_allclose(u.atoms.masses, expected) + + def test_guessed_types(self, filename): + u = mda.Universe(filename) + expected = ['O', 'O', 'O', 'O', 'O'] + assert_equal(u.atoms.types, expected) diff --git a/testsuite/MDAnalysisTests/topology/test_xyz.py b/testsuite/MDAnalysisTests/topology/test_xyz.py index e45591a238c..8ce6ce45c72 100644 --- a/testsuite/MDAnalysisTests/topology/test_xyz.py +++ b/testsuite/MDAnalysisTests/topology/test_xyz.py @@ -30,13 +30,16 @@ XYZ_mini, ) +from numpy.testing import assert_equal, assert_allclose + class XYZBase(ParserBase): parser = mda.topology.XYZParser.XYZParser expected_n_residues = 1 expected_n_segments = 1 - expected_attrs = ['names', "elements"] - guessed_attrs = ['types', 'masses'] + expected_attrs = ['names', 'elements'] + guessed_attrs = ['masses', 'types'] + class TestXYZMini(XYZBase): ref_filename = XYZ_mini @@ -49,3 +52,13 @@ class TestXYZParser(XYZBase): @pytest.fixture(params=[XYZ, XYZ_bz2]) def filename(self, request): return request.param + + def test_guessed_masses(self, filename): + u = mda.Universe(filename) + expected = [1.008, 1.008, 1.008, 1.008, 1.008, 1.008, 1.008] + assert_allclose(u.atoms.masses[:7], expected) + + def test_guessed_types(self, filename): + u = mda.Universe(filename) + expected = ['H', 'H', 'H', 'H', 'H', 'H', 'H'] + assert_equal(u.atoms.types[:7], expected) diff --git a/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py b/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py index 3eec056a5bb..ba2c348bd06 100644 --- a/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py +++ b/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py @@ -13,7 +13,7 @@ def posaveraging_universes(): ''' Create the universe objects for the tests. ''' - u = md.Universe(datafiles.XTC_multi_frame) + u = md.Universe(datafiles.XTC_multi_frame, to_guess=()) transformation = PositionAverager(3) u.trajectory.add_transformations(transformation) return u @@ -24,7 +24,7 @@ def posaveraging_universes_noreset(): Create the universe objects for the tests. Position averaging reset is set to False. ''' - u = md.Universe(datafiles.XTC_multi_frame) + u = md.Universe(datafiles.XTC_multi_frame, to_guess=()) transformation = PositionAverager(3, check_reset=False) u.trajectory.add_transformations(transformation) return u @@ -106,6 +106,6 @@ def test_posavging_specific_noreset(posaveraging_universes_noreset): specr_avgd[...,idx] = ts.positions.copy() idx += 1 assert_array_almost_equal(ref_matrix_specr, specr_avgd[1,:,-1], decimal=5) - + From 101008bc98505d9fc63c73b3dd9a60b45c82c3b5 Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:04:34 +1100 Subject: [PATCH 24/57] Update guesser docs (#4743) * update docs * i've forgotten how sphinx works * replace ref with class * update changelog * fix whitespace * Update default_guesser.py Co-authored-by: Irfan Alibay --------- Co-authored-by: Irfan Alibay --- package/CHANGELOG | 2 + package/MDAnalysis/guesser/default_guesser.py | 139 ++++++++++++++---- package/MDAnalysis/topology/CRDParser.py | 13 ++ package/MDAnalysis/topology/DLPolyParser.py | 6 + package/MDAnalysis/topology/DMSParser.py | 8 +- .../MDAnalysis/topology/ExtendedPDBParser.py | 6 + package/MDAnalysis/topology/FHIAIMSParser.py | 7 + package/MDAnalysis/topology/GMSParser.py | 7 + package/MDAnalysis/topology/GROParser.py | 6 + package/MDAnalysis/topology/ITPParser.py | 7 + package/MDAnalysis/topology/LAMMPSParser.py | 6 + package/MDAnalysis/topology/MMTFParser.py | 6 + package/MDAnalysis/topology/MOL2Parser.py | 7 + package/MDAnalysis/topology/PDBParser.py | 7 + package/MDAnalysis/topology/PDBQTParser.py | 6 + package/MDAnalysis/topology/PQRParser.py | 8 + package/MDAnalysis/topology/TXYZParser.py | 6 + package/MDAnalysis/topology/XYZParser.py | 6 + .../documentation_pages/guesser_modules.rst | 34 +++-- 19 files changed, 252 insertions(+), 35 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 64fcb63fe0e..85a7208627b 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,8 @@ The rules for this file: * 2.8.0 Fixes + * Adds guessed attributes documentation back to each parser page + and updates overall guesser docs (Issue #4696) * Fix Bohrium (Bh) atomic mass in tables.py (PR #3753) * set `n_parts` to the total number of frames being analyzed if `n_parts` is bigger. (Issue #4685) diff --git a/package/MDAnalysis/guesser/default_guesser.py b/package/MDAnalysis/guesser/default_guesser.py index a64b023309e..f49e75b24c6 100644 --- a/package/MDAnalysis/guesser/default_guesser.py +++ b/package/MDAnalysis/guesser/default_guesser.py @@ -27,9 +27,70 @@ DefaultGuesser is a generic guesser class that has basic guessing methods. This class is a general purpose guesser that can be used with most topologies, -but being generic makes it the less accurate among all guessers. +but being generic makes it the least accurate among all guessers. +Guessing behavior +----------------- + +This section describes how each attribute is guessed by the DefaultGuesser. + +Masses +~~~~~~ + +We first attempt to look up the mass of an atom based on its element if the +element TopologyAttr is available. If not, we attempt to lookup the mass based +on the atom type (``type``) TopologyAttr. If neither of these is available, we +attempt to guess the atom type based on the atom name (``name``) and then +lookup the mass based on the guessed atom type. + + +Types +~~~~~ + +We attempt to guess the atom type based on the atom name (``name``). +The name is first stripped of any numbers and symbols, and then looked up in +the :data:`MDAnalysis.guesser.tables.atomelements` table. If the name is not +found, we continue checking variations of the name following the logic in +:meth:`DefaultGuesser.guess_atom_element`. Ultimately, if no match is found, +the first character of the stripped name is returned. + +Elements +~~~~~~~~ + +This follows the same method as guessing atom types. + + +Bonds +~~~~~ + +Bonds are guessed based on the distance between atoms. +See :meth:`DefaultGuesser.guess_bonds` for more details. + +Angles +~~~~~~ + +Angles are guessed based on the bonds between atoms. +See :meth:`DefaultGuesser.guess_angles` for more details. + +Dihedrals +~~~~~~~~~ + +Dihedrals are guessed based on the angles between atoms. +See :meth:`DefaultGuesser.guess_dihedrals` for more details. + +Improper Dihedrals +~~~~~~~~~~~~~~~~~~ + +Improper dihedrals are guessed based on the angles between atoms. +See :meth:`DefaultGuesser.guess_improper_dihedrals` for more details. + +Aromaticities +~~~~~~~~~~~~~ + +Aromaticity is guessed using RDKit's GetIsAromatic method. +See :meth:`DefaultGuesser.guess_aromaticities` for more details. + @@ -70,6 +131,23 @@ class DefaultGuesser(GuesserBase): You can use this guesser either directly through an instance, or through the :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` method. + Parameters + ---------- + universe : Universe + The Universe to apply the guesser on + box : np.ndarray, optional + The box of the Universe. This is used for bond guessing. + vdwradii : dict, optional + Dict relating atom types: vdw radii. This is used for bond guessing + fudge_factor : float, optional + The factor by which atoms must overlap each other to be considered + a bond. Larger values will increase the number of bonds found. [0.55] + lower_bound : float, optional + The minimum bond length. All bonds found shorter than this length + will be ignored. This is useful for parsing PDB with altloc records + where atoms with altloc A and B may be very close together and + there should be no chemical bond between them. [0.1] + Examples -------- to guess bonds for a universe:: @@ -84,8 +162,23 @@ class DefaultGuesser(GuesserBase): """ context = 'default' - def __init__(self, universe, **kwargs): - super().__init__(universe, **kwargs) + def __init__( + self, + universe, + box=None, + vdwradii=None, + fudge_factor=0.55, + lower_bound=0.1, + **kwargs + ): + super().__init__( + universe, + box=box, + vdwradii=vdwradii, + fudge_factor=fudge_factor, + lower_bound=lower_bound, + **kwargs + ) self._guesser_methods = { 'masses': self.guess_masses, 'types': self.guess_types, @@ -212,8 +305,19 @@ def guess_types(self, atom_types=None, indices_to_guess=None): def guess_atom_element(self, atomname): """Guess the element of the atom from the name. - Looks in dict to see if element is found, otherwise it uses the first - character in the atomname. The table comes from CHARMM and AMBER atom + First all numbers and symbols are stripped from the name. + Then the name is looked up in the + :data:`MDAnalysis.guesser.tables.atomelements` table. + If the name is not found, we remove the last character or + first character from the name and check the table for both, + with a preference for removing the last character. If the name is + still not found, we iteratively continue to remove the last character + or first character until we find a match. If ultimately no match + is found, the first character of the stripped name is returned. + + If the input name is an empty string, an empty string is returned. + + The table comes from CHARMM and AMBER atom types, where the first character is not sufficient to determine the atom type. Some GROMOS ions have also been added. @@ -270,26 +374,11 @@ def guess_bonds(self, atoms=None, coords=None): Parameters ---------- - atoms : AtomGroup - atoms for which bonds should be guessed - fudge_factor : float, optional - The factor by which atoms must overlap eachother to be considered a - bond. Larger values will increase the number of bonds found. [0.55] - vdwradii : dict, optional - To supply custom vdwradii for atoms in the algorithm. Must be a - dict of format {type:radii}. The default table of van der Waals - radii is hard-coded as :data:`MDAnalysis.guesser.tables.vdwradii`. - Any user defined vdwradii passed as an argument will supercede the - table values. [``None``] - lower_bound : float, optional - The minimum bond length. All bonds found shorter than this length - will be ignored. This is useful for parsing PDB with altloc records - where atoms with altloc A and B maybe very close together and - there should be no chemical bond between them. [0.1] - box : array_like, optional - Bonds are found using a distance search, if unit cell information - is given, periodic boundary conditions will be considered in the - distance search. [``None``] + atoms: AtomGroup + atoms for which bonds should be guessed + coords: np.ndarray, optional + coordinates of the atoms. If not provided, the coordinates + of the ``atoms`` in the universe are used. Returns ------- diff --git a/package/MDAnalysis/topology/CRDParser.py b/package/MDAnalysis/topology/CRDParser.py index 5e4406732f3..9a1fc72ec00 100644 --- a/package/MDAnalysis/topology/CRDParser.py +++ b/package/MDAnalysis/topology/CRDParser.py @@ -33,6 +33,12 @@ Residues are detected through a change is either resid or resname while segments are detected according to changes in segid. +.. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. _CRD: https://www.charmmtutorial.org/index.php/CHARMM:The_Basics @@ -72,6 +78,13 @@ class CRDParser(TopologyReaderBase): - Resnums - Segids + + .. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionchanged:: 2.8.0 Type and mass are not longer guessed here. Until 3.0 these will still be set by default through through universe.guess_TopologyAttrs() API. diff --git a/package/MDAnalysis/topology/DLPolyParser.py b/package/MDAnalysis/topology/DLPolyParser.py index b85a0d188cc..4148a38c064 100644 --- a/package/MDAnalysis/topology/DLPolyParser.py +++ b/package/MDAnalysis/topology/DLPolyParser.py @@ -30,6 +30,12 @@ - Atomnames - Atomids +.. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. _Poly: http://www.stfc.ac.uk/SCD/research/app/ccg/software/DL_POLY/44516.aspx Classes diff --git a/package/MDAnalysis/topology/DMSParser.py b/package/MDAnalysis/topology/DMSParser.py index f165272fc26..f37a854c725 100644 --- a/package/MDAnalysis/topology/DMSParser.py +++ b/package/MDAnalysis/topology/DMSParser.py @@ -86,11 +86,17 @@ class DMSParser(TopologyReaderBase): Segment: - Segids + .. note:: + + By default, atomtypes will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. _DESRES: http://www.deshawresearch.com .. _Desmond: http://www.deshawresearch.com/resources_desmond.html .. _DMS: http://www.deshawresearch.com/Desmond_Users_Guide-0.7.pdf .. versionchanged:: 2.8.0 - Removed type and mass guessing (attributes guessing takes place now + Removed type guessing (attributes guessing takes place now through universe.guess_TopologyAttrs() API). """ diff --git a/package/MDAnalysis/topology/ExtendedPDBParser.py b/package/MDAnalysis/topology/ExtendedPDBParser.py index ec6e1e527d2..b41463403e1 100644 --- a/package/MDAnalysis/topology/ExtendedPDBParser.py +++ b/package/MDAnalysis/topology/ExtendedPDBParser.py @@ -78,6 +78,12 @@ class ExtendedPDBParser(PDBParser.PDBParser): - bonds - formalcharges + .. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + See Also -------- diff --git a/package/MDAnalysis/topology/FHIAIMSParser.py b/package/MDAnalysis/topology/FHIAIMSParser.py index fcf95691f33..8738d5e3ce9 100644 --- a/package/MDAnalysis/topology/FHIAIMSParser.py +++ b/package/MDAnalysis/topology/FHIAIMSParser.py @@ -66,6 +66,13 @@ class FHIAIMSParser(TopologyReaderBase): Creates the following attributes: - Atomnames + + .. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionchanged:: 2.8.0 Removed type and mass guessing (attributes guessing takes place now through universe.guess_TopologyAttrs() API). diff --git a/package/MDAnalysis/topology/GMSParser.py b/package/MDAnalysis/topology/GMSParser.py index 812207ed674..2223cc42756 100644 --- a/package/MDAnalysis/topology/GMSParser.py +++ b/package/MDAnalysis/topology/GMSParser.py @@ -73,6 +73,13 @@ class GMSParser(TopologyReaderBase): - names - atomic charges + + .. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionadded:: 0.9.1 .. versionchanged:: 2.8.0 Removed type and mass guessing (attributes guessing takes place now diff --git a/package/MDAnalysis/topology/GROParser.py b/package/MDAnalysis/topology/GROParser.py index 6bcaec24cb5..ebb51e7cd02 100644 --- a/package/MDAnalysis/topology/GROParser.py +++ b/package/MDAnalysis/topology/GROParser.py @@ -67,6 +67,12 @@ class GROParser(TopologyReaderBase): - atomids - atomnames + .. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionchanged:: 2.8.0 Removed type and mass guessing (attributes guessing takes place now through universe.guess_TopologyAttrs() API). diff --git a/package/MDAnalysis/topology/ITPParser.py b/package/MDAnalysis/topology/ITPParser.py index d8552160278..9c9dd37976b 100644 --- a/package/MDAnalysis/topology/ITPParser.py +++ b/package/MDAnalysis/topology/ITPParser.py @@ -473,6 +473,13 @@ class ITPParser(TopologyReaderBase): .. _ITP: http://manual.gromacs.org/current/reference-manual/topologies/topology-file-formats.html#molecule-itp-file .. _TOP: http://manual.gromacs.org/current/reference-manual/file-formats.html#top + .. note:: + + By default, atomtypes and masses will be guessed on Universe creation + if they are not read from the input file. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionchanged:: 2.2.0 no longer adds angles for water molecules with SETTLE constraint .. versionchanged:: 2.8.0 diff --git a/package/MDAnalysis/topology/LAMMPSParser.py b/package/MDAnalysis/topology/LAMMPSParser.py index 62664b568bc..52a58f77291 100644 --- a/package/MDAnalysis/topology/LAMMPSParser.py +++ b/package/MDAnalysis/topology/LAMMPSParser.py @@ -27,6 +27,12 @@ Parses data_ or dump_ files produced by LAMMPS_. +.. note:: + + By default, masses will be guessed on Universe creation if they are not + read from the input file. This may change in release 3.0. + See :ref:`Guessers` for more information. + .. _LAMMPS: http://lammps.sandia.gov/ .. _data: DATA file format: :http://lammps.sandia.gov/doc/2001/data_format.html .. _dump: http://lammps.sandia.gov/doc/dump.html diff --git a/package/MDAnalysis/topology/MMTFParser.py b/package/MDAnalysis/topology/MMTFParser.py index e9332e9d689..5a58f1b2454 100644 --- a/package/MDAnalysis/topology/MMTFParser.py +++ b/package/MDAnalysis/topology/MMTFParser.py @@ -64,6 +64,12 @@ - segid - model + .. note:: + + By default, masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + Classes ------- diff --git a/package/MDAnalysis/topology/MOL2Parser.py b/package/MDAnalysis/topology/MOL2Parser.py index 4345ca0efe7..f5549858755 100644 --- a/package/MDAnalysis/topology/MOL2Parser.py +++ b/package/MDAnalysis/topology/MOL2Parser.py @@ -78,6 +78,13 @@ class MOL2Parser(TopologyReaderBase): - Elements + .. note:: + + By default, masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + + Notes ----- Elements are obtained directly from the SYBYL atom types. If some atoms have diff --git a/package/MDAnalysis/topology/PDBParser.py b/package/MDAnalysis/topology/PDBParser.py index bad6d2bc6d5..8349be9133b 100644 --- a/package/MDAnalysis/topology/PDBParser.py +++ b/package/MDAnalysis/topology/PDBParser.py @@ -35,6 +35,13 @@ :mod:`~MDAnalysis.topology.ExtendedPDBParser`) that can handle residue numbers up to 99,999. +.. note:: + + Atomtypes will be created from elements if they are present and valid. + Otherwise, they will be guessed on Universe creation. + By default, masses will also be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. .. Note:: diff --git a/package/MDAnalysis/topology/PDBQTParser.py b/package/MDAnalysis/topology/PDBQTParser.py index 97640820218..435ec5678c8 100644 --- a/package/MDAnalysis/topology/PDBQTParser.py +++ b/package/MDAnalysis/topology/PDBQTParser.py @@ -32,6 +32,12 @@ * Reads a PDBQT file line by line and does not require sequential atom numbering. * Multi-model PDBQT files are not supported. +.. note:: + + By default, masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + Notes ----- Only reads atoms and their names; connectivity is not diff --git a/package/MDAnalysis/topology/PQRParser.py b/package/MDAnalysis/topology/PQRParser.py index 1adcd7fba2a..9ef6d3e6f95 100644 --- a/package/MDAnalysis/topology/PQRParser.py +++ b/package/MDAnalysis/topology/PQRParser.py @@ -80,6 +80,14 @@ class PQRParser(TopologyReaderBase): - Resnames - Segids + .. note:: + + Atomtypes will be read from the input file if they are present + (e.g. GROMACS PQR files). Otherwise, they will be guessed on Universe + creation. By default, masses will also be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionchanged:: 0.9.0 Read chainID from a PQR file and use it as segid (before we always used diff --git a/package/MDAnalysis/topology/TXYZParser.py b/package/MDAnalysis/topology/TXYZParser.py index 0781488c9dc..206f381e9e0 100644 --- a/package/MDAnalysis/topology/TXYZParser.py +++ b/package/MDAnalysis/topology/TXYZParser.py @@ -73,6 +73,12 @@ class TXYZParser(TopologyReaderBase): - Atomtypes - Elements (if all atom names are element symbols) + .. note:: + + By default, masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionadded:: 0.17.0 .. versionchanged:: 2.4.0 Adding the `Element` attribute if all names are valid element symbols. diff --git a/package/MDAnalysis/topology/XYZParser.py b/package/MDAnalysis/topology/XYZParser.py index cb0df129e08..5fe736fec6a 100644 --- a/package/MDAnalysis/topology/XYZParser.py +++ b/package/MDAnalysis/topology/XYZParser.py @@ -59,6 +59,12 @@ class XYZParser(TopologyReaderBase): Creates the following attributes: - Atomnames + .. note:: + + By default, atomtypes and masses will be guessed on Universe creation. + This may change in release 3.0. + See :ref:`Guessers` for more information. + .. versionadded:: 0.9.1 diff --git a/package/doc/sphinx/source/documentation_pages/guesser_modules.rst b/package/doc/sphinx/source/documentation_pages/guesser_modules.rst index 7747fdc380f..96cb324270e 100644 --- a/package/doc/sphinx/source/documentation_pages/guesser_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/guesser_modules.rst @@ -1,19 +1,30 @@ .. Contains the formatted docstrings from the guesser modules located in 'mdanalysis/package/MDAnalysis/guesser' +.. _Guessers: + ************************** Guesser modules ************************** -This module contains the context-aware guessers, which are used by the :meth:`~MDAnalysis.core.Universe.Universe.guess_TopologyAttrs` API. Context-aware guessers' main purpose +This module contains the context-aware guessers, which are used by the :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` API. Context-aware guessers' main purpose is to be tailored guesser classes that target specific file format or force field (eg. PDB file format, or Martini forcefield). -Having such guessers makes attribute guessing more accurate and reliable than having generic guessing methods that doesn't fit all topologies. +Having such guessers makes attribute guessing more accurate and reliable than having generic guessing methods that don't fit all scenarios. Example uses of guessers ------------------------ +Default behavior +~~~~~~~~~~~~~~~~ + +By default, MDAnalysis will guess the "mass" and "type" (atom type) attributes for all particles in the Universe +using the :class:`~MDAnalysis.guesser.default_guesser.DefaultGuesser` at the time of Universe creation, +if they are not read from the input file. +Please see the :class:`~MDAnalysis.guesser.default_guesser.DefaultGuesser` for more information. + + + Guessing using :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Guessing can be done through the Universe's :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` as following:: import MDAnalysis as mda @@ -24,12 +35,12 @@ Guessing can be done through the Universe's :meth:`~MDAnalysis.core.universe.Uni u.guess_TopologyAttrs(to_guess=['elements']) print(u.atoms.elements) # print ['N' 'H' 'H' ... 'NA' 'NA' 'NA'] -In the above example, we passed ``elements`` as the attribute we want to guess, and +In the above example, we passed ``elements`` as the attribute we want to guess :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` guess then add it as a topology attribute to the ``AtomGroup`` of the universe. -If the attribute already exist in the universe, passing the attribute of interest to the ``to_guess`` parameter will only fill the empty values of the attribute if any exists. -To override all the attribute values, you can pass the attribute to the ``force_guess`` parameter instead of the to_guess one as following:: +If the attribute already exists in the universe, passing the attribute of interest to the ``to_guess`` parameter will only fill the empty values of the attribute if any exists. +To override all the attribute values, you can pass the attribute to the ``force_guess`` parameter instead of ``to_guess`` as following:: import MDAnalysis as mda from MDAnalysisTests.datafiles import PRM12 @@ -38,9 +49,14 @@ To override all the attribute values, you can pass the attribute to the ``force_ u.guess_TopologyAttrs(force_guess=['types']) # types ['H', 'O', ..] -N.B.: If you didn't pass any ``context`` to the API, it will use the :class:`~MDAnalysis.guesser.default_guesser.DefaultGuesser` -.. rubric:: available guessers +.. note:: + The default ``context`` will use the :class:`~MDAnalysis.guesser.default_guesser.DefaultGuesser` + + + + +.. rubric:: Available guessers .. toctree:: :maxdepth: 1 @@ -48,7 +64,7 @@ N.B.: If you didn't pass any ``context`` to the API, it will use the :class:`~MD guesser_modules/default_guesser -.. rubric:: guesser core modules +.. rubric:: Guesser core modules The remaining pages are primarily of interest to developers as they contain functions and classes that are used in the implementation of From d2729f71809f1f5fc2d3644ef48ee351e3b28f3d Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Fri, 25 Oct 2024 06:36:02 +1100 Subject: [PATCH 25/57] Add deprecation warning for ITPParser (#4744) * add deprecation warning for ITPParser * add test for no deprecation warning in itp without valid elements --------- Co-authored-by: Irfan Alibay Co-authored-by: Rocco Meli --- package/CHANGELOG | 2 ++ package/MDAnalysis/topology/ITPParser.py | 10 +++++++++- testsuite/MDAnalysisTests/topology/test_itp.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 85a7208627b..52e8eb077b6 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -112,6 +112,8 @@ Changes numpy.testing.assert_allclose #4438) Deprecations + * Element guessing in the ITPParser is deprecated and will be removed in version 3.0 + (Issue #4698) * Unknown masses are set to 0.0 for current version, this will be depracated in version 3.0.0 and replaced by :class:`Masses`' no_value_label attribute(np.nan) (PR #3753) diff --git a/package/MDAnalysis/topology/ITPParser.py b/package/MDAnalysis/topology/ITPParser.py index 9c9dd37976b..5649e5cb384 100644 --- a/package/MDAnalysis/topology/ITPParser.py +++ b/package/MDAnalysis/topology/ITPParser.py @@ -601,7 +601,15 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', if all(e.capitalize() in SYMB2Z for e in self.elements): attrs.append(Elements(np.array(self.elements, dtype=object), guessed=True)) - + warnings.warn( + "The elements attribute has been populated by guessing " + "elements from atom types. This behaviour has been " + "temporarily added to the ITPParser as we transition " + "to the new guessing API. " + "This behavior will be removed in release 3.0. " + "Please see issue #4698 for more information. ", + DeprecationWarning + ) else: warnings.warn("Element information is missing, elements attribute " "will not be populated. If needed these can be " diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index e5cea0e215d..9702141ee53 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -490,3 +490,20 @@ def test_missing_elements_no_attribute(): u = mda.Universe(ITP_atomtypes) with pytest.raises(AttributeError): _ = u.atoms.elements + + +def test_elements_deprecation_warning(): + """Test deprecation warning is present""" + with pytest.warns(DeprecationWarning, match="removed in release 3.0"): + mda.Universe(ITP_nomass) + + +def test_elements_nodeprecation_warning(): + """Test deprecation warning is not present if elements isn't guessed""" + with pytest.warns(UserWarning) as record: + mda.Universe(ITP_atomtypes) + assert len(record) == 2 + + warned = [warn.message.args[0] for warn in record] + assert "Element information is missing" in warned[0] + assert "No coordinate reader found" in warned[1] From 39eb071f16c3d220eb8820a250da85f561299506 Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Sat, 26 Oct 2024 06:03:45 +1100 Subject: [PATCH 26/57] Change error to warning on Universe creation (#4754) Changes made in this Pull Request: * This changes the error to a warning on Universe creation for types and masses guessing, which is set by default. * Also fixes kwargs not getting passed into bond guessing. * And fixes an issue where Universe kwargs were getting passed into guessing when they're documented as being for topologies. --- package/CHANGELOG | 2 ++ package/MDAnalysis/core/universe.py | 29 ++++++++++++++++--- package/MDAnalysis/guesser/default_guesser.py | 2 +- .../documentation_pages/guesser_modules.rst | 3 +- .../MDAnalysisTests/guesser/test_base.py | 19 +++++++++++- .../guesser/test_default_guesser.py | 21 ++++++++++++-- 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 52e8eb077b6..4b1c091df69 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,8 @@ The rules for this file: * 2.8.0 Fixes + * Changes error to warning on Universe creation if guessing fails + due to missing information (Issue #4750, PR #4754) * Adds guessed attributes documentation back to each parser page and updates overall guesser docs (Issue #4696) * Fix Bohrium (Bh) atomic mass in tables.py (PR #3753) diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index c0ac6bcf6fd..dcc8c634aab 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -410,7 +410,8 @@ def __init__(self, topology=None, *coordinates, all_coordinates=False, force_guess = list(force_guess) + ['bonds', 'angles', 'dihedrals'] self.guess_TopologyAttrs( - context, to_guess, force_guess, vdwradii=vdwradii, **kwargs) + context, to_guess, force_guess, error_if_missing=False + ) def copy(self): @@ -1498,7 +1499,9 @@ def from_smiles(cls, smiles, sanitize=True, addHs=True, return cls(mol, **kwargs) def guess_TopologyAttrs( - self, context=None, to_guess=None, force_guess=None, **kwargs): + self, context=None, to_guess=None, force_guess=None, + error_if_missing=True, **kwargs + ): """ Guess and add attributes through a specific context-aware guesser. @@ -1523,6 +1526,13 @@ def guess_TopologyAttrs( TopologyAttr does not already exist in the Universe, this has no effect. If the TopologyAttr does already exist, all values will be overwritten by guessed values. + error_if_missing: bool + If `True`, raise an error if the guesser cannot guess the attribute + due to missing TopologyAttrs used as the inputs for guessing. + If `False`, a warning will be raised instead. + Errors will always be raised if an attribute is in the + ``force_guess`` list, even if this parameter is set to False. + **kwargs: extra arguments to be passed to the guesser class Examples @@ -1537,7 +1547,11 @@ def guess_TopologyAttrs( if not context: context = self._context - guesser = get_guesser(context, self.universe, **kwargs) + # update iteratively to avoid multiple kwargs clashing + guesser_kwargs = {} + guesser_kwargs.update(self._kwargs) + guesser_kwargs.update(kwargs) + guesser = get_guesser(context, self.universe, **guesser_kwargs) self._context = guesser if to_guess is None: @@ -1577,7 +1591,14 @@ def guess_TopologyAttrs( for attr in total_guess: if guesser.is_guessable(attr): fg = attr in force_guess - values = guesser.guess_attr(attr, fg) + try: + values = guesser.guess_attr(attr, fg) + except ValueError as e: + if error_if_missing or fg: + raise e + else: + warnings.warn(str(e)) + continue if values is not None: if attr in objects: diff --git a/package/MDAnalysis/guesser/default_guesser.py b/package/MDAnalysis/guesser/default_guesser.py index f49e75b24c6..87da87e12cf 100644 --- a/package/MDAnalysis/guesser/default_guesser.py +++ b/package/MDAnalysis/guesser/default_guesser.py @@ -293,7 +293,7 @@ def guess_types(self, atom_types=None, indices_to_guess=None): atom_types = self._universe.atoms.names except AttributeError: raise ValueError( - "there is no reference attributes in this universe" + "there is no reference attributes in this universe " "to guess types from") if indices_to_guess is not None: diff --git a/package/doc/sphinx/source/documentation_pages/guesser_modules.rst b/package/doc/sphinx/source/documentation_pages/guesser_modules.rst index 96cb324270e..d672b748bb5 100644 --- a/package/doc/sphinx/source/documentation_pages/guesser_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/guesser_modules.rst @@ -17,7 +17,8 @@ Default behavior By default, MDAnalysis will guess the "mass" and "type" (atom type) attributes for all particles in the Universe using the :class:`~MDAnalysis.guesser.default_guesser.DefaultGuesser` at the time of Universe creation, -if they are not read from the input file. +if they are not read from the input file. If the required information is not present in the input file, +a warning will be raised. Please see the :class:`~MDAnalysis.guesser.default_guesser.DefaultGuesser` for more information. diff --git a/testsuite/MDAnalysisTests/guesser/test_base.py b/testsuite/MDAnalysisTests/guesser/test_base.py index b429826647f..4cca0de24da 100644 --- a/testsuite/MDAnalysisTests/guesser/test_base.py +++ b/testsuite/MDAnalysisTests/guesser/test_base.py @@ -30,7 +30,7 @@ from numpy.testing import assert_allclose, assert_equal -class TesttBaseGuesser(): +class TestBaseGuesser(): def test_get_guesser(self): class TestGuesser1(GuesserBase): @@ -100,3 +100,20 @@ def test_partial_guess_attr_with_unknown_no_value_label(self): top = Topology(4, 1, 1, attrs=[names, types, ]) u = mda.Universe(top, to_guess=['types']) assert_equal(u.atoms.types, ['', '', '', '']) + + +@pytest.mark.parametrize( + "universe_input", + [datafiles.DCD, datafiles.XTC, np.random.rand(3, 3), datafiles.PDB] +) +def test_universe_creation_from_coordinates(universe_input): + mda.Universe(universe_input) + + +def test_universe_creation_from_specific_array(): + a = np.array([ + [0., 0., 150.], [0., 0., 150.], [200., 0., 150.], + [0., 0., 150.], [100., 100., 150.], [200., 100., 150.], + [0., 200., 150.], [100., 200., 150.], [200., 200., 150.] + ]) + mda.Universe(a, n_atoms=9) diff --git a/testsuite/MDAnalysisTests/guesser/test_default_guesser.py b/testsuite/MDAnalysisTests/guesser/test_default_guesser.py index 2aacb28d4f1..8eb55b69529 100644 --- a/testsuite/MDAnalysisTests/guesser/test_default_guesser.py +++ b/testsuite/MDAnalysisTests/guesser/test_default_guesser.py @@ -117,9 +117,11 @@ def test_partial_guess_elements(self, default_guesser): def test_guess_elements_from_no_data(self): top = Topology(5) - msg = "there is no reference attributes in this universe" - "to guess types from" - with pytest.raises(ValueError, match=(msg)): + msg = ( + "there is no reference attributes in this " + "universe to guess types from" + ) + with pytest.warns(UserWarning, match=msg): mda.Universe(top, to_guess=['types']) @pytest.mark.parametrize('name, element', ( @@ -236,6 +238,19 @@ def test_guess_bonds_water(): (3, 5))) +@pytest.mark.parametrize( + "fudge_factor, n_bonds", + [(0, 0), (0.55, 4), (200, 6)] +) +def test_guess_bonds_water_fudge_factor_passed(fudge_factor, n_bonds): + u = mda.Universe( + datafiles.two_water_gro, + fudge_factor=fudge_factor, + to_guess=("types", "bonds") + ) + assert len(u.atoms.bonds) == n_bonds + + def test_guess_bonds_adk(): u = mda.Universe(datafiles.PSF, datafiles.DCD) u.guess_TopologyAttrs(force_guess=['types']) From 78dda9bf27ca505ba746ee67b297dc102a583559 Mon Sep 17 00:00:00 2001 From: Egor Marin Date: Fri, 25 Oct 2024 21:36:07 +0200 Subject: [PATCH 27/57] Fix error in parallelization scheme (#4760) * fix illustration * Update docs * Update package/doc/sphinx/source/documentation_pages/analysis/parallelization.rst --------- Co-authored-by: Irfan Alibay --- .../analysis/parallelization.rst | 7 ++++++- .../source/images/AnalysisBase_parallel.png | Bin 274539 -> 262103 bytes 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package/doc/sphinx/source/documentation_pages/analysis/parallelization.rst b/package/doc/sphinx/source/documentation_pages/analysis/parallelization.rst index 91ae05fceca..3070614b5a3 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis/parallelization.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis/parallelization.rst @@ -41,7 +41,8 @@ impossible with any but the ``serial`` backend. Parallelization is getting added to existing analysis classes. Initially, only :class:`MDAnalysis.analysis.rms.RMSD` supports parallel analysis, but - we aim to increase support in future releases. + we aim to increase support in future releases. Please check issues labeled + `parallelization` on the `MDAnalysis issues tracker `_. How does parallelization work @@ -106,6 +107,10 @@ If you want to write your own *parallel* analysis class, you have to implement denote if your analysis can run in parallel by following the steps under :ref:`adding-parallelization`. +.. Note:: + + Attributes that are the same for the whole trajectory, should be defined + in `__init__` method because they need to be consistent across all workers. For MDAnalysis developers ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/package/doc/sphinx/source/images/AnalysisBase_parallel.png b/package/doc/sphinx/source/images/AnalysisBase_parallel.png index 960e7cc257b0a6756d5df9f1b16bc67c46498923..9a9b1ea4d7860db146a6007433e1dcbc8e59d1cc 100644 GIT binary patch literal 262103 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

0W+IKl!rooNe0;NbQR-Ss@WX5f(^E5|3B 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%0A@!lL{U^*M!`Q}JiuNnm++?agZ=9v2F8}U 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%0AK440txV2f*#lSw|hGK+Lhbj zCtl+7p~-i;SIwnCrjvK3H$#Lsb{AM?zX5Lp&@4JpzzCoGj*dsaT;=zQ)Rz2o8sX>~LRw8rzsss?=wQ34*GlyMK!s}| z{j=OZ0Ncp+xV5iM8F6L2Z!AoAixx2;souycnHnQQ+qE6fRU}AMi z5c0B|kdD5-&ld3C>S5PclX-Ff)c793nU660BIR4I1txu_M=J1CtiCi=_M$lPexsoW zuf6QQL?!(7BudMsATi6dP6SA1=al}Y%=I-{o@2YJGQpIe^_1LNdR&$&!PR*vb|(w5 z5soKYpN$qH#kP8gChg|VJZa}$t-0r$l>+=?8!gUX_0Is?-{4^+n4d{Wog|UjZ@G+%fuRnq36k)>1tYJl+V1hHz(99pP?)4ksH6JF|s)qe<)aCe#@kyKysKh zc{Ml4N&hd3ZWAp&roZ&LeW=FzG~%&>QwYR=%@DRV$a}sIIfLPnD_yfMCv5G`GTEbR z6f7MD_-%6PR>&nv<+*Ih_OxqKtzHxfZ~T~SAB}*|$=SW*Q5-zcbW>QX#_KE87>bci zwO*5%d2LPf1j?eFv(QS_HB4XRq-#VAdso=NVm9&X)KPqudvFUmn!{>j1RZGPu~bV{gplQ zUrHpa0p1iErGj+GjkRH#VY5e#$v31KGMqNx6DIn|h$Kc)hIZT`>$KUU;;J%>WF*IA6|MNJvT43Vj~>h0vEQI zV0Gi)Qv>T>k^nA$tEUO{RF1XaE$$Q_(@z^Yvjoeqt0X4+BHXDIKZVmdn_qHkx+7Z< zi0^0wG)?2E@iixtW^LMb;}0lr`LwLkLG8$<+gc9%n)z~XLJ~kp-RezZE)aD%7lE4? z*tbQQCO}SD-6KSCWv{0VxP@3DS!w;ZcaGkqWXttalW#X>+*{xa&8y@^ZE>lv03Z=H zB~zRJK^W5u2d!`Qg|x+KpI9j1mJ(-6-SA2Hw$mo3EZk5dBx^J=rD|i?E)>{{UW<}N zoDp4OP^>E8vRRtv+GPKaitc*fU;J5;jVzbqXlKedbx6T^d>-qq@bt+ENz zuQOgzdt9-?Kg%@_9*3hH(y46u&v7QcU6@F+l9g2gI4_h4TVuBo-$j@qLsJ1Ds=GqQ zh!1Q}4{mOMsuC4FxIs+iTsL69Qh%deuVTly1*sX&?~#fhUIo-31MEVZAmI&h7rYo2~#jvgF%YAG9J zs>U3fhA$VDYQSH1)W}^-Xmn5Q%@}o3NJok`!PGA1jq6LY>{(sn$N(OILaOzsBPbMk z$V%#qt_ElRNwqy0zZi#9mj=_;@+7yzLj6f`amz;Ljk`ZVD-Xs)%zJo}AsZtc5^Hje zUT)^53E<%cqIxHm%i;LLB_rqL8?rBH+x0fVcU5?QPhq9f5B`qsC8yM76XoDnC)mK7 zivPys9njW`-iZeP_U9Mh*m!geSFFMJ|{ed!)<}VCo6=2uKC5Nhtuv_Ez z@Q|n9w&nM-Jd1IM7!PvbyXH<(Ki(KKOiFPwB|C{KiqeN{WD!O7m+v>OV_o!5Crx@m zOtjz!FNU(PmD4`;FIdh3ITPK^Jz34;fS z2sjtZ@9R;vobDla)UxE1y}R>%56}Pf#fQ8k(U;RfYYoM>Z-Fp&uz*%#a@|ihJfYiyRO(Ks+kFrYlN=jYHCO-nlzO^Qe?9jx%jP-%XS4>I1<^A?+K{Fo zW&Bv}dB6wGZd~$!3Pz#KV1wiV#K_2Qr7)LAB^`P!M#03Wwu=7BX&H<{NcAX8p}7sCtt!O^~6zUn44bcLo)9_fAS(3}vEKPca8^T5H^IibxcX{H_{g z<+gm)6*zc;{V5xXUwhng>&=nfwK_~pXsf6J{~nN{$C{=gu?r&^ctAE=4+310I+f$r zi`mtP2*=h;F^sAR<9@Eo_MFoN><;XM*l$i>AHQ%GS~y(Z&Nnb~xtM?9Vi3yB9(ce& zN8e>d+%`gy@YfgMU$GLO%jjI#J|8G#>=M9VE5uj3usR%85Ej~Ytxv4vhp`IV$@LX# z5`vj#sbg66A6)v+F4k9gILjY_LCYG_bScF3Z(k0I79)tP0y!OG3kzr;b|tP4v%~Sz zmcw7$`2nEoSV4|rx=WuUutRlk4fBj1g&@|uPmU&wYBJOchWVJXvIJL92 ztmGbfBxKfQ{8+TYl|2Tnc|GH9Jw5XBIT?c3s?kk@fE+mHD|c&0Q8{R&)ye03PS5a_ zbp}}J*|I?aPm&6;ZjZ*};8AXIsZWmE`d%HXO* z=1|d!-$6u=TwP%7-t>Bkwj{)KyhI|Bsy`vRu6Y5;Sq#XzpO&&-1j&48`&ZXiJuy>p9BPp)J_w+VpRgV1up%H=R)OzW!w&rzN z0}61G_2=~FWrO?nZLBmDYMuk zgScK?7>X1=_TBlN_2VRi;SA)k1uS*>>VN77#Da)cwDJmr);TXES@Jea=U4^c=Wzmh zzTt7Ssf}GoZlg~;1lDq2RGk6{L4K_sd>dF+!3(GTm*v9E-vGWCj0;p=z>CIEZir%H zyJ8mB^-_k;;2PxVb^kqN(U?Zf=sE&BKY)UX>j9IjIa{fz4;525N=tMn8{lr)`>OHZF zi}8VOZ1AIm_S*|?xZjP?9q(arR*JLOp~KONEOzilb-U=9vnk8R)s5z$GwH~(A1`P2 ze4EWNczm$iF&>_bt|;>plql{HCQ5fT^=J0TjDp;i0|G=fE+aj0S{Rlm8$?3Jco|fZ zAi@@waFNs3Uw#=n2l(RWL^QLt@vaj_0wpu-R|?mTD2u!On|lh0b*u9x2Jb_)rKPY9 zvz;;CAT0QiILRHsFA`1eyS9uGM@0?#!&2JU5vkBPa&l64NPLl;w2t6J>kya`gpKgCb1Yr>}H{DT~+1-Rh*9$L-j^T%h zt4uJENr+T=ady2?NJ9ZFKcSJ+LIML?LCU{;)gu^$yeqJd)3SedL&)ilzUT6~fgA&D zp_>vF3ujwphYH1XiQ9kO{5g+Sb8_}V$Y}3c#Dg+@*;cPgA|1Z0)-Wq*hqdSD$~c*m zn!nsLc8~I7oz#k<&4+!YdBKl*bxyUvLHFUQ@~%Wg3#Xlr%f3dc%w`g)ALPTXUmX%% zCypPhoJCRPfJSPc?&&T-MyM>1iAk0zT}OYT5%F_j?Wk7Wq<9yv`Xb#E?gw`h;WZap z6)AN(DNX5KG^R>uIX$|l=~uj@moqpMF(6Z}x}p%^u6Ap=PJAg_+1%U}>i2GtU)6La z(aBGwbqB-5#Aw&);yPSo>H5IR>AS=DH%j74ng*wK?i`6zoJ$ST^j$(~Hu~JFXEANu zw5}WZQ>!!AJ-obYY;}&Q|E5!TxFe9{)zo^tc*AD*{M1GKHvcfhIt35~^S1;!EKEI^ zK>5@o;0?4&0{fWe32CHT)+s_(?rVvWl8Bl~-a~KSdM`#Nz4P)kAcXT#3=?^n@d7!Y zRvn)UnZ3WJ^&;L8^LO!U&Ea)eZy;V&|JFoLQ1j0pF=jK?Gkax_kxi>WajP!$^r6LE zBz6vpTpk*ObzGQ^@?=zQkCrw=N>LZ!(vx`0yy<*#9bu1f|Kf2|yO8~%#eM|#+v~1S zEqk2Q&8)O=(6WR%vgZWEDcT1_o6PNZ?~Ur-!O7XWot1@$N2TG*G>ZUeIzA9rE+HN8 z?0UVFFjIDr1hhT&$4-LWOvcxoP3G50rYyWBEe`&s*a&_L%kd~kGYcAvLPf>=NM0S) z1eMjS4D);%uDz1^m`uZZF{QX5Fo6dWwv0VEt;;|>QiYi;Rpl$@?MdQfViXa95Td0Q zhFfayHXRrDGTkj<{L^fHk8ID@N(8vui;n)U+ghRtLn4s1SRDpFb@Dwmj z1uRWV^}K2hEfJ6KG=O)3q7r}VV3IR)cVHmks9{mgp7NXB)TEC-EYg%+$><$xtXM>gDp z8#(C2Hn2Rzsc+MZfOtfqL*qQ3quQdjZbRvw6gn&UAJMt!(#wyrJ5u3sKt{Np-y}Mt zF;JElORoNo2NOl-GvK@Rl2-oEdyl5fz@INTzF4gounE*qA`HG*bnoj23`f^79~x5t z=&J*QKRRNwiP4Y=d|Q;CGv4eTc1NT@v5kzatk(#a&FzEcUer@c9G~hRDQ`4_{b5Ey zp(T?xK*$JhV`HNa1G&cq_L%U22p!wsl`VW3L4z?ce5W8m7ezt7`dM@(Ew`wCRnX0H zrloB=9ij%U4-`fBIMn`>Zit-qk8qQD^FN6+a!nGXTm3fQ@6q79@)4O?KLW`YMU9?| zpaPj<9fBiS>9n@dfbvkFWZ+?Zw59azkBd2?f_?nw3#~1UpnHyNVxp|{Midz8XXBfy z1&pdiO81@2QQrdk(2YRhB?aFB9TN-><)x)~1%d>m9Mq{O@?{g<7XSOhd!A3vkh?m% z?B66R2aYhO%V@};d!mJR;x_3KSItvu8H;3JF5S{MM0*!)EZyX`OUoz>*W$doqPV#Gf7(2&&l2Sq~K(q*JDWunUr4Q2FgPXGHv6Fv5Hnr zUpedd1N*~iU{O9*kfm+RNeIYCAV-_!2KOv<-%X9X! z>qKdIC;72(JaMHhb#;emD1|up(cU`jBcX5T`*dCX6|9xO!Uqd+_+X4*@7K`}SMCzw z{caq|Y}{foaR#T~K8(e&bMHP`K;26@O(VbVA)fE%E9YUq+!;E`$G=jn4u+IJS_W#o zvZTn+1f`#$+CU;y!$I`)8TIMs*CSAW(r9wP_QAOIP(5H~Sq?X>H>~A_O&+R^F4*luF3O@mb1;E%?v?Ux>uH7neOW5Y4XRdo3wyT_suNP{?} zyj7+vKnAOb&4gEnHti17!81w;KRNf#N7~P zbp47)i$e|e#DeS%#$Fq@-(Ae5F25j%Blu|6l}Xh~NQ{Jl(-$YDtIU3L*rQh8o8Z<%XQ)~nubHM_c3(D`0C z|AVBc5z6`d3s3GEbVdyduV--G)_#W;*1hI;;e&1RVW4d@%Mmo^XFN|~KB!CAO-pnl zij2v*z6ZXe!}e$^&bSGmddya=hQj!4=Ffu~?{Yh?u@dW!I|0(! z;Zfi(Z0}v&#X;M-+S19WtJ0Dnx3KV*S8n~=Gs(Up!`89^f{WhE=5D`6A^}8pq$L8J zWd2SPB&MQ~Ao z1f>GEf_ElIiTTm7mWi>_vzC?ljS9Tmb;q{Gj8hjexpi=A>g!i#$qQ!XR2Qs_%IM#$8DZhby`k7&oR1P|GIJ z$|mr5LS-4U8!Q^X(#fX`-+t5X<8J323i-t)%-hSY?kQi~WPsnA*v+ttPh*mIZH>#D8jDC~3Os zto>Ce!hl<2!XzSwN|CPXW>@Wkn1${uM!l2!=3^nqcAN>^U;5B_n{UR2e8wDC;aT>* zGe*irB`rnvq}AG@i{VD_Kw{YI#y}UQB4;o$_?asvpa{NSq|P?!wM(j;(%BRe#Wym9r6cA!sq&5eyiaJm%Fk zBdg%#>q=$UNe~YcH4KPceSf_CVw#_SXG)G~U*fJpXZmY|UF_IFvE8SUgW{9ajoj;8 z72(Y`HPjpQ75QA6QV7BZp9>bTM3^w+diW+ziMOPYu?+Cq)Zu@(kG*kC(KD>S{O37Wi2 zMf?ePV|;eb+Wg3rG2xl|3(8$ZNinnx5)LzKRaG=&iNg8|u~+#%7~N(;ShgC;&z=(@ zQBq>*`%OZ#KY1V>dgVn#+w2g69S7!PP7Q~wpSUrs`ys;dRSHl{x{?EP5pv0^&_;%e zlIY{`bYqLYxr-qeM%{RHN6_<3TmDOnUnisQCJAwr-$3HQh%Z@SdHN_fPt(Y7il1Hp zE^OhRtkg?S$mEPXNv-7v&`^aXA;i6+HjkcDQ$&V;3~Wa5Xvm8S*$A1~r&*JJXvp)y z$i5BqK)h(oi@My1#GKFMsdEVr??U8w9D{tdq*COpoJfS_p-T+-;!&@;m{=Y2nT!Er z^bgM4?6+~zMk7c<4uS?Z`Z}~&CB)F%DQCdmn(*4t1Q@B8xE6rtKx$JPN#FFWG`Re63KR0aBiJA32)3HTgY*wh z6!luvMj6dkJe~;390Aw}=P2k;0|xb)I=BjaFc|R?dG2v5^U4tQ+B$u-%!5FowGrTS zHV>49OyV5LtxRsX%$zxjsm~^Y`GXv2*8F@xJI*T!&;TvpsRgw=MJeZSQ84@8;j zzkx_oua(}#IM=*^gn;D(djq2eBVOGez3@7E9~CaZPHlP4y_6TZXMZd)iUTg%0KK?g z7ds~J%an##(R{w^IVN$zsb!^BLT!2^_Hrqwyz36lH~2>A(o*vDqQhF{4P-4`;PDI+ zobgt90K?_;`aP1qG{l4EGeJ}d8y?P>v?OYc;GyhQZk#q!6%~WDJrRz0Ot=>7=OzWo z;izzqxavWxAfGGZIt)0*p}WiRPPKMup=8H%>YVU!Y$Sa|V>(9yC}mX8GGcXfxV9w3 z_)7R|Hz5un{0agc1~t8}u613g7WI+eKsp!E;#iWyJ)cuAy;2&u#yYeYe?5xokJwI4 zZA6O0f9CP&BO5g=GMqm?(RQ@(UN+J-kTNlmXZatTd)UDA6Nbu9+Ov8a<^rqA7=xeeA zRW%1wk(<|Q#-5_lplSy{cgOKc|1v(^^zPASv83|XG z+!RnHs4>R{RCk7oFu0@8PkG8P2);TyXn(=5+2Eb^vD8OD*DnZpaBx`|5zkh{ocQp0 zYumsi*aC;MYjT}TfvHG!!_N76E7wBI#L|)#lasg;+hv9U1rCP3$ndLVHImnNPhAi$ zGZaw1PI(UCx*RVxZgiPdeG2bmqu69FEhs4XJ~?>?wcU4ca8M?QdJ~}ioAR`nfo~H9 z64Zn~7&zw@FH1ojQ48+1cJFZA<&W2v9+`*;qM3-j5T(v}*0YKk0lVF7Mw+5APMi$f z8kjcMlN|o@HOH4`dJa#e=K)NjVOf$Vx#8A*S_Hj%ayG0iMv3EKgJZYLv2R)yMF~kz z{y{V@QS#+I33s_)8YVyLdUFkkoA64cJxrGf!9p8?@dF2>+=H&B5Y2R1CPiw79JVeX zQSNJ!eLoL+k|HlN3*UH3`qrg;_Nrd+&VK^*EB&E}VEQHTan`eQeOj!B zBJTQ2)t>=UR-?Ob>r zb#V;}kX&-4XlcHdetAJi;(>i85Sj=R<~bnQac-E|9XJRQRV=!6cx)uyN!nD5Ws)pI z77^|R-hV9`vsws34=g(H2@jt=4e$xll#?7rvcx|DA*t54vCJ#(-r;=D#y_^c7%Q{o>R7&~Ku{oy|E(bP-b~Kjz4|Tq z{;-E^dChqe6t$e!-OX)LFzw@?ZhtyJS8O;4qr}N}Ui}f3e|}+e*s2-*d$9lXdSdY( zjr0HO^Q71Wa@=o-c7Dy6#gf;b&)MCV>G_muwB;2PthaZQCH`)OmVJZZ{6>PO;vwu$ zlscwl%7_skMml`-2gAy53uRLOMID=95*UH>lme53;KawcLPAo}Cw}$`YI;0RZe^e< zv+w`8diK*wU6ux%@mdt&4|y=F&P{9K>9)}!60632rbj#7WGd-qpa5FgTi00??3W2z zi28`w*k{lGl8{7ZpQ)%?)9zFFkHGxD>+}Z(wJ7{hZ-oDXN0x*HV2?jD73Q?HwaI~L zCgohcJ| zCZ_i8h}DX--RG!H%JV70{!mEf#R8u6Xn1&HsP{h!6LL&Npg?l{g^^wAHQ4Vl=!+Ue zogAY;UINta)(^t~o6z`?62*@5f3)f0{|PaD%zv{_w93AG3af6p-g*L^JR$a`$zM7= zpvmJo*L5{nEyfQ}`@g=2uDF(y{2}ijBys*OX3X7ydW! zBaE`f;v^+@!YFkVSA{=|R?f`wKal^Q#OSpt&savfNFoRMFrrY)e%$jkiLNt?J)>D- zV1Z;V#d;<*aCstm`pnjP-G*Pl5&9DgllvZnkuCba-D{=(Mjy&t*a5KlrfqbQ+An@` za`I)8PeGE9#WY4Bfk0&VP?)QTtRAr>#CPRpl|(r~ix9_|!|6aJ_VphaKNgEpyO&R; zskOPkK>Z`m{zKJn^=CVX|GjoL)(zNa2!C3=GhV#y;aW)|DK_BOG5e2nnf!ZC3r#ky zZNZ1{{hh^z*6IJUf@S0_b$VuUzq((MMg5uK&%Y?Dk>U9iAOHKS&_Mmk{{|~s<$qH3 z|Mlnp+LS^{gsN=6 zB*w(V)BZ$C3+PR6R9GGb1K30-a^9yoBFrxdB#G~4b2^5@vJR-ih4M*WMzEJIPltK>~D zfICw8@tTv3pSZt&Zx8&+$f&YIh0#Ejm2is(&*xhblTUFHUm3LhL2yDX*MMI5az#-6 z2W83v*m+rljo?{jvmmP`Hfy9fF9y+^b`T~YX4Ak zD+zkeYig{*N+>@90i+8mG9Gi|8%W~AYIObvG7a6i>r zojrSc_gWp1CuusRY}A$yL^=4)I%zrp>y$kZ^|QtO>wOg}YW+Merpj7@6;Lbjnrt)>H18a-*VRi6fO^P z1175iCYx1Q=)7}h$4B=djOx4oPk&8DeD_Km5M6*Uup+GhjX0ve8q}C{ryZ^L=Ndn7 za@JD?8=k5^8_rw*ZGL4(hu?gXS|f+$9h!!rQT*_^4_&jK z0;e$O)YN%Mc*%Luy(l$eP)?u(EQW8%K3telaZ2 zAtM5#;lf!|2~lj`d{6sJlD~RoZF#w!M_wr;9+-HY=CfYUkQq1%==Z(<3{H4BXt|aG z>@wRv*v^hO-FJ;O8GZfvewwub<`M2d%*+UkI5nY6<_JV!!~1}xGb8kwlY#QeqJhYP zUkF2hq+NF$)G=jH5mFscdJ6(mk+GV^YO@OC%@g=bZf8JJMP0X0pdzR8;(-7tKjIwS zF^^ybPW~SDifNMQ9JuKD`6`~+yi3z+azMnWrD<_eC$@%yEek$65rU%w{X1># zX)Qm4SPw)ifNNA3+Y?E;E-XhfcnNi&3VTUV;+K7eMH2Z=oK8C2&Y-q7qlx}A0TQ?u zcP$_S=1Y!RtygtyqQF!~BoGvA3m!{PtFVHIpKlu`-wGTQYf!_Jyg*gHQze@(Nd&kOjSqyZl#^1tKY z5a21$tXV!RYmt0vmFlje)ru>4I^*}~Dw4j{dOiS@P2i6skau==!kpC1^#4df$Y1tG z(EL%%Bq9U{I)7CBpD&)bveakjXyskVLaI! z(a%uZDRGwD%ESC~uC*c14iEPU15S6p_sCGT+m!B5pyRZ_&zx?OKZ+MxR`m~#G3nn^ z|KzV6-Fizyk-tYWJxzQ-$BoJ|`8`YHp7_s&p=@<$baZq&+xaW8e5m*HKZBP?x<3g2 z&Dws7>-zsi-FJq=*>#OFIzbR!NR)`)Ta-yel+i+z=n^eD(d!TrM2p@@!ssnJqYQ%R ziC)Ku-n+pl=YHDzyx({JoWEy&xMuFl>{)y5)z{i}t(V(ZW+v-5t zRz=%&e{pSnZLRDt2M5%pc9FY9ZFi&FnjA8AiiJA~Qlf7c9{&C+T)IMsTepST#Kc7N zl|<;yMK8eq9`2NZD9#&f_CiJiW~BVm3c zF23KZU8LhWyaO;?P>U%i}XYYZ1TlbHPQ{(4X{nw zrS9kwG4RFF`4SvpUa#?)wxg^)UfHE6k@SyQ+P*IU-b{r6Oyua6K@7R}7kbqZT0EHx zpc_2ct-+NAtbEdAw-YOO@dhfF+ghf>9bGbZo8l;)bOwpMe#5d?Fq$6S;=VDG100Bn zVOapM19fn0R#w&?gsV)isI08aF|gWoZ72(KmZy?nXA-+H*o_}(z@1cHJRu2?M?O>l zw8xhOS>h2>`2Nm11d!UB8-_=@B_$HfFR zAS(Zl2k;k~{_oGV|M4LH{Ew%cuw!m>dsK0F>IBMy1ksLYLUd6GmEb*19FzZ#89xHy zhPmI{r#9y_H)biwVWDtl2^DfAX2RPh-M&>yREp1wzlx&?ip54_*T>TqW;>oVu&ZcW z0e&?Or~A!XN8-peh3gBP`#`Vt_3Il=Acjvt5lgy$kE1s6A88w3LkZzh$K?k-xOMUp zOB+&xU4X@cKf|3VT!*nSL3JS*n_>mjxbCu0*pT%-7Au`Qm7J5gd+JTGUG?jEtm4S? zG;zjqf2_KfB&Q3Wg-Rk-d5Opl5*4;j39?}9=G>tk=YHT579)rjmUtQ-K7`F!n3w!7 z&FE=l2>Zt?>JgMII@8BZ|xRQ^S9vWkukLVwdgYCoJ#T zRf;FSAsVVIB7=$s@uR|ff>?CkTqr||Mr`m@I>V`z{^X4jZa!EIZKc4uQ$4{CKhI$S zX&YK|7W1d(%q|ixx(d9zN^OiFKd&BR+Ej8GQZ)jnZO!e#C|lNj26LXntRQkwiqiM( z2d}AvQn39#-2`lbfEJwClh|HL3b%g#^gETtpGvrNo(j6Wnh-%GAowT_&K{!;dQvGX zrO$m!2?lV>qr;`~{dkM{V4}oRFpSMzb4s5$OBxrJ{fh9Hf`|YOcA%5luX&pY88e$t z3nbZ$KdwJ07rT^-uIIk&8Zf%04<;m+!x35Stm1#^C$n91zuAuGrI)<@MV1hGIrL6; zCW%N+*g@{Akci4&hRQV>K_?yi3lpG-1qUrlsyQ5Hv$|#v@1DBM+-;_4_GY7#4x045 zrfk>@ZqEXXtgc95)_J`>vGWo!(~d8u{76y-tx=fLo_8EBU03@*t1DgT9a_kkYH&(L zS{uD#K~ONZmt0y-k5r`j{K2`0uuq78G{MLS{~4yUv8`aObFS;WaSo^hvf-I}yA!-a zeh&mBkD0U6*0!R~&mUS#JCK ztA826tjN((W`Mii^`43N2b_8!zf7)nxjXz+N%zeoFl+X%&DkDy&$r(;AqR*XESqDI zx;&bHq(E6>Q``Qfs0*EsP~BWBA~rarF9s?CJ6;TLxCg6Y+4n%LP&0#p2h-taDmWGl z1WY*oSW}JNMZ*P&*Pu(LhK4xY$V6?%eHGN;GW&Zw&xr6fI7>ChPi!XaM1fgeHLfeH zR7whxt#v46U;kPaE2OVPfNb9vx`S84m+Z8af5P2*#DTEH>f%d4#dhBn>qe+JnQ zN6Diwz= zyCSa)D@@~qCJR>X4C;s3u^cDIlnX0U3qkW9buGrY`BtDWRi=mR+_{daXt`zWul@8L zREr;CQF=vgQw&O!=`UsM0X2O?kZN*jv{vL(se2dQq)CTUF-NKKsKJ9s^h(T8JkyJD zZvWw>o8fi+c+~}+W1{EivT++3oCe;T$BEgnxHf((;lCl^GFmg^ zwssfZVM89e#T+bPg2ZH=ick6TNxlL`zQUwWI~*n?MkMgY57mRKsu4!*K8e)#ALs@bE_Z zxLlg!u`zM6UR4OOS0D652sv1E=>h}LN0o+T&~!BG)&%VYA_c8 zn8rAfviNW`+tNNfK9a4N!>3xjSTa4^Gs^FbfPQG3BQUYrNU@I%8*agmne}>1>1nE| zf#CPjgW@QC@8p7&1mpCjBf}!tMk`s(W)LQfx^{Mma`x+22Z16X)FRE{I+o;cGRm1VS>1(IBA~5)693y!$ zN^uaKIAY(cB4l*zdaaF`r*I9tYvGiZII@9Idb1O43B^JR-5GZ;-q2XRV34xS1NZY* z#D=SGZXa18hJ&&gwk;k>1#hj9%9YPZ);-Xt-rTqh_x1z7E`J)BrM5jh)yG6l(Z+=E zZVBL2XHT6Sk;6yCYn)B(p?Yo#ThC1SIi}o>GBcm|@7>UnRm2Z@|GK*mZQGIl_GK(am}K@?SyOiF!rugQ zIFRC|8&#CjUIZPiC^4)vW4XTg)f{9t*a6!w0wZFbevCid95?+DL2U+ZKG=M)(1b5{ zQ-CBAsNHU{()dug;#Lt{(Hz)iF)Z+2;_~*j;WcBf##RN{8WUaxGQaf*dOoMq!nPuJFbjQSrWnEMh8_%h1{-p>qB zU8q=?-1)RHdXs^r^I9M#l5vYsH(C2a_WAWRW+=RHAL48b*>GIWn|cce>OpXRh2;g; z6W@vZ%T8d^F;nFp2U_n!Ip5Nq1W3Gn>s1^1Q5%x`0s_V!Dmcia_(4rt4&e%D8ae_x z$~GJIjtg!U34Wv658HLr_XXTs{Dz=zprpYw&vS%u`Qxpm3xAw_X9i86AyJ62)k*~y z%rMCyIYVs$FqKLQ5`lRRxqHB(lh>16Fo>=rJ|IAKmjkwqOajha-gafZx(};iIQMy! zd<`YPD1L;wWOtE`=8S$^j(=1QSRYL#)3=nQ1uIq9p0AvMM+Qs@G0^&N&!9inPHWf6 zlZFi@l4!*jE)%+eS_mg7$P|Nfc4)RK4RjrgM*m9jz1|7gX}58ErzQeG6Pdiio{@8% zd)>pE0|qz0+Fu=R(d^tU5Kk5bNl|>&1GZxcl;>4=Z?kv|=~o36 z?aof+0=rh|jxbDI%v{UPQZRKozHr15db#i23fWzR96PBX)!#-@UvV%;QlJXLhFd&u zj4bmhf6E+;BZ7+ymb)d1HuLCi&TRfM8!lV)=@Q6#e=x57qPxIx;>-U65;ki571F2= zYTH826ejxx;dy+=fP6Bt@joVseEf3t)qm&7b_vU~bAq|vv9x`yFu8=-rjmPEkehnY z1b`)V9Q3IWpuq`LZ+HEgzIm@O`;nR6bY{-|Org5|l%Q@1yKD^Yrh3h7 zP;)Cr3XVSIPTDr*sfDXBYJ?76D8FbuW|rJm60DEq^S0x{^Iz25Zv>wNILY1&ajtbR zLN7a#Wlt`yvZAxhR+Ds)brt2CVZiJN*kc&?!wULB0OQSi3s!&UxVX&-x|O#(QA4Cb#)$nW%D_|rEwd%!GXVThbyC@2=k z?MD(r-91hl9Z2$Yn2ed6md9g?;rv80JUJ4Fry6y1>U#ncB_@+L!7SMI32O@Hp7-kY zxBPBjCdsf!IJ$yKdXv090M(B&`|ZT)s==zgy~LVvIrXl~=SSR!?-XI^_;l5FE*ScG zz*eagf+bk`hnpu?C4J@HbB|`N_dWiA(-0ee_N*+Bz(`?WJ21;XhSrqx`MGT2M9V_t z54S@Z)EbV=K+=%SHQ}U;PUBAP>>8iE=Svo1BIYbo+66qI>Jk36Yf7u~)NcQ4?V%pM zbKt?potz7tO#Ky!!3PPb$}DsEiFtMH+wqW|wPj*9Xfw&G?o`y~6bV2)iz{L@=r&hL zH98!9a4HpRvPXO7GalqCGv&(ehD{-?8EvGVK~cHrMu9+pnG5lzy zq8G+D`9ZUlp^i^=5`#ke?3#EJs6okSygJ@-G{$BZxA33NfMVuI^%(KYT0qR{=kl|U zmX7o|Wd{v#2?7Q5M<1qWBg>b^XAzMT1H)_8B<3@2=Zy`!amXY#>w$hExyb;gnPUHV zN%TbXGj$FDBptHW=z3wiXEij0XD`g`ipN>(g*wpuq82C;?LjOc^&)U4c>ZmEm>)Cf7*&z%; zOMR$;0BiP+>(K79^hRIXbq={d!?dD zFx~>oAum#?G`*O;9C)Uc@elCb6emu=5(Tjwh=!ex0dn6Of=$pRxfaFM|1hv zdNxUmY{22T%O)dF{rst9^W_*a0%u97bCN8Cd^d?!nd2aqR zTeW$Zz*@1GY?(CnkQ6alp%66Y%mArZ5WjR9;@nd7c9vy#fSR&zrRb<>zB@;%j2w;it%r> z2(N2J0k{*$rbU)hN5`$-{S49A7_ZWp(@GUnUb%sa*O=Pv7cAqwZ{u#ne1kz3XDS?w zHT#>CIMUaklcfw`0!K7!QqoO;C*e5RufjR5fQV~b&k7Ph`PA3T72wlJe#Eq~5m6+y zXmyXt-x~QFHg`W^5$^n+uXxNlqYby!UCRGx6RE-scn1Wng!V}{r{%A%1Xe%F);9ZB z%j9QMtL6{)8oj()Y0doUcIxNLuAej&x%5@rBNi~%+VGHuo@d43g*5@{qfPaYQk^c# zg75}-hpWodfscP>Ol);euCrCx=DDc_Zrq3S;+&E`#>etu_5?|>!rEPKFQhqNQf3#(~ zF82T?nu8?S5AYF5N>FU2pvWmdLF|5yB7swdUgID0QX&=03AM`P)kK)_k#~P#`Y*yI z2_$VA=5(iJ*IL12OLPiJ#UFS-ds0WOP>abuDbK8y=`AH`Xt>Cg%torPVK;NK297F0 zRf9)gKyEC)u}2nF;n?Kl^qV^-nYy3;^-GK5sq08S8h@?b69`ypGtEMOPuvn32h12p6K zqG7k8nXfMiT`}c30&`g|Z{&Wi+1A+THKJ^U%`bybHvi4n3@}^zJ6Ghy7y^5TSh66zzSYln`oV+viiotMjmHYq7$VTnj}+^7Y%vi(8q2F3bU#)>PVyt1mk11?$ONUu~%|6~s23R{1Rx5e3M z#$u}5=2nG)#f)3W521>}0MlB$v&h{ax)#1O=B$^i^8uXEyb?}#E-&UQ`?+}ZQoXku zjSX`BF*#s8Z~j=Q#~*OdkTin2#5va-)$uz_uxdFD|H4MP0BfA#VeY3 zecxPRP`h8!@9_4d_E1Y=e^znC{GWhZk!yuj7oP2pam7;=uANcyQi$qpg1p3T#qYoK z6;sp+kasBRkJ%Jqc5G(F=yr-C3FyZ|d=qjm5cfp&9yYK_H+-UdnH7CyIZ zbK*D@f656@1^YkyUd;tl4W3L9bHwuhj3EH>bT1IH%hDD%M3=RG(s@)Dn?OkpX{EXk z8t9NTVn9kJ9an#~(RkT(U+SP3Twy`_0xgqOpSErrcCt7KA7inoS3l}_3&VW}0nXIx zeFBjMD&O|JfFd&aM9y>YsgICP?XT!AL+WRUF0anC$<7t@L{6qw1tvR~vyVI8p-ry& zr0b+-$|J#;C{KGtod3=Z6l8>3<7)#^X?eu=U(r{}rvvFK995MFj*RIgvJA-^!GDYcF!+@h z!tJq-;apzuuIdOv@b64ok!#X!Vuo^T*LolWxr2)7fYXE=#V?ewxj;tZ71{yClB7|( zD9pX!__Eg!u}K}nGGm@oE85`mSfD`R`xrKTFA$rLzRx)V80%kXL8Gj%7qNPFDwcle zD)!aKan-*<4@0S7+7L7dj2j4C&=J=ytkFpfLW_!zrF(XNkJBBYa`R=8_I`^5eWf7S zdk+1r4C}hUQ$>YIh)&puMW(ce46pW4vP~aLxeQDvmB4Q$LXOrvq(z0G4<>UYwH=4V zzkWh}-dR*?5%*!F(}&i4Q%Htzw`e#8^4{DEmF^eU$gFuO^vq}Wiz1V74nHPm;;k7TN;LKC05h88t15cV6!!`x=&dlbR8g~ zymo@$#Sy^09lHolww0F0e3gdpqx)#Y<2+CNaboFpH{_16#oNFcrDe)1Ya2?lT8}dV z(SO;Kl)S*Exb`9^MzYstjHIv4JT}UM-io_;dPSV3#PNJ`n7^u4M-NYN-+zGJ2Dr`> zEvS(MH$8Qu3VFy+M@>=YwTX?B9C-V^ydg22WIO7k*6LTQM=i0ml-qC*B*eOh)coXI zWsQilmhjejb5?i2Fakz8MsMUSzh_Y|i#$tpc0>tn>8Dr6c77q9aJRD@B71+LUtx@w zr#q4Lsh)1rV0338cJ0Q=a_fs?&dpj%hgMs#l}XDiaEWluqpj>!hKJkR{@+%DUasYp zt~Vk+Q4-e$-{c64VCd=D4DTG1-34l9qm5e00a+pkUC&Z~>9jyqF7?lcHYEkzObMd5 z0G0h$!EFOXRT_oVbKUUO~b8t%5_!=c~Al~!s=JEp<4T3b|s0;$>DfBy`3Cd3E*Bd^8AaMdREF>Az zOPWIt#1}-DRnHzCr1a&qkI?=j{M{Hs+LwA-2@*kdAomkxrKW9UbCy~Q5i=kOr0M~Y z&{#GZ?6zP8jcRp}LiNY;gh6HT^^pshKfHr9=a4Lna!F)RhwBNtzVqUz>C103?JMZ^ z7f0ruG=n)=+{If3Ct-&`{TNYqLyzb($15f`)&iEmoKGl7`ieCw~5IU$aR z_<~M%csI4$S2lqe$MokuPT#*;?dLN3vMkhIzS!DFv-onx)UY1Z&*VQIHT;>9hi(3M zRloL9`2zuBbaRi3pE)M{Z1Cr;B97}@&9Ln$+7}SoXWuOXWF=uWBrXmi0oN2~ZB-UZ zI0T9c@BLnnr-Y}bY5NjMVcV#h4`T2B_BmulNKt6Lns!&dWS%O(+_Y804S!^fYs7?O zG2;c^x&%xbT~xJI!QICOzm*ka0C$w2Qa`rSB3MG{dk^N^`6sti*|_=eAYcocO4hk> z`Tu5>#K@lLGNk(Yn3|VQwi?OhyPyL$zYBiI2Pw1P_T(l|Dtg{hS^nhKijNbtfym>)ZsTeA`%r> zmF~9{HC)k|G?2ACEGpab74K@>VDTlESM6s36`apQOwa$SPvN7bhzxe)7QJGJxhseW zy?{7!!FFCiq|wgqIGgC>@}J+xTvQ-Mt5-=}u)D!vtpH4{UEq_wEV5IaA$6{3XTgcX zZ>1U9Gr4ff)9^bKBCNj?!_UkWnjM3nF)={xzv_rG#8%c6xSU6a=U>~tb`wL~^FUV; zfBy(bf2(t4Wop}PVkQ$u02Lvyg)~$OQ09t;a5(}iex0_L(}|gBL4R{|eh-8xX{YU_ zt_;s+lA=!}nn;nlU+g-3Uja^HkeG~Ofy?y0)%iM%~yJglJZkhV{ zB3Z2k>uHLXBZyB*k=1?yDr`V}4DMfcAjdP9bL1Sd1Z{iGkpUTMplO{!UDA)^@0AKd zTV04Qn`7;0`R#*>-F+ zOS8iWN_>K5F9^eQ%H)-+ zPm5f(7sKBi>arJ&m;Noy^bG$Z9B|3$N6JQ9Bk`qUCDSk}`q&sVHnM^M= zv%0YJ=-s8q$;{Dix_u_@^0bzlo(tv}G#@P6bmL`XUb|c8_D4NvtWm7-ym@f>;awu-6|Q z4uvHSahT0!BZ;@uwJ#W4o+#)%EGOfFiFq$J?P6~OtYf1Hv2BA8kS-fvuJao*rJ;!Ql@&%e-sc&bPC1mq0J^@ zqX;mPEOJ19kE%*0kAFGvfW-Lv@KE#T+ESIQyic3~7x+F6-+MM@;wJ<)dxTd_T>{IX zsB92R49U%hSdyc(`iI-nE^OYL4VKfySdU7ZFXb;rh#U5wHCQj4bxlc;EJp94iZmBD zhLiEqZqZZG1ewW+O*gE3Vt#|=g7K^DN-v2f1a)PXMtY5*Q{MZu6zd=q;Mjm;Rl&ptvjcwmr=;*q7N}mOx zCfoBThhAwmm=cRwIeF)B!sbYqs&-^VS>0$NXn5MYOV;?cMvA$N>ud;Vlux<@Di^UN zZF*dG;dK$WnNf7K!n!(=qK-_$#5-+^hdmBscx3Z6c36`(ZnCldEl#b^o4Oji|q3g+E6#dPC11du=E4T^4v&AxYUz@VMIm#3vaP&6!*f!_O+`9V)`fD<(TmKIcz+zI5MiN4bS0rj`bggr$sGnwG33z|x ztjt`aZO0Pz-qGpfLj5b7mnEb%Z$MNwA0Ef5d|Nb6t91Yk=Mch=4FfShSzfKpikN|S z^gm?#n1XF~2qkuMd~RBvE++E!_uJPJk<6=dqu{-Z7riTA*q%;AOYU9Yt}r)P098(q#G#g2}<@og^+ofFQ?#w(jvWxgpDr?iM>U-_o zxtqY{f-8?DZ|_y{gHjSCNd5-6#ru7roYj)%VQtpjRn{1(_35KZd!n9~YTkTl!U;Mt zv-D@51|m1l`xqoCqZ{P}QwLU_5R3hoC)Z@$y4UgAn=5N^*DJX}Q{`S?M;uSc^VV`# zbmZG8j30{r^O(xk&Gs%-ld(ifu;i8n_8e+=t!GED8XXRO*YKQ#5dHa`@Nw$&eCRDD z*C9`IMzA4^B{hYr@rwT7-+fATZrvvVb z6GD$8Z1bQ<)J@L8%l;w#3y6I=@e+UR0{URMva>7=hvqPe&&Bfk_0;i2F?}p#qd9Bw zLaj}!;AJJ9cmr8rs`h2-j$uct>eJ8a#fwYkX!u)#ww2fT^-NgMIXSnCae+Z5ES+%E zoyA$Tw)b~vB;VfG6(Q!w((^(Wy&n)NyI~Bj9u*(Pe^g-2eJ5@PrZMjK&zNhKxFTHn zM!tI9rQH;)0*Y1#zNGpfv_+kbPhFL{R7%?16|KDO$Jjo>zcnW%@AZb|zN0d)?CC1&?a-ZOPZZ#oCV z;@kanX+xvGj6x}&Y>=$1xQ|}%*SDkyguLJVCaV%A0u9YgsL3 zzMlgzO=s7u?%tmxx}Txc$bRJ@;dm>S$>dUOEaeQ{7N z$9hMM>g#i0OPafKGUq|z5u4#=5j5XoB0Ba$Hy~26Ls4Pch5mdIl(BG(x49pRTi!F7 z{Q-6XB~9DW>h1!?&=&P_U`6$LeC1hNHoPZE(1+;9mdYURlB{0GH9PsQuiqWHG9GA8 zt*t&aXY^|OL6z_5iLMV;4}x>aq&g4@-jSr8Ld=)QknHXr1>184?Emma#qeS(a47F& zDd&M2WI9{;i#Wr{nbUAC3&I2Kzu!3CV#y})JJARnh>=BgGo{_Hl6u8M7s$U||74LQ zYf%54>_iAbY-s&8R`k(8h+Bu={<_wyC*mw+=^t)US7hVwWz+Y)9;?yK2y^@NEl&YI z%P8z#l!KlVh;}#G=O&o3J!}nu42X|~KaHJBdnPOt66r}!Z!NAu$7?||G$vA$3~Xi z?#kXRImTGf8tb0Lc$&81 zBBO-!mp2y(pK#9y%cKb0*vQ>iwpR9aiPHg#%u>D~Z>DmbVVTNu87x;HA0GBcQJK^2 z7s8>2%V&3kI|C_jk!FmmK?9a`JT4o3s*8y_$>jzxEfW&a~+P^75GN^u7DVtEciY+@!v; z(6F?~ZE=y}%UHh+mAT$b(p0|B%jXNej#I@v+jAu~)7lWnsh15~0e}uI=HM|DCnDLK z^$d}n4B7ERKu8ynUvx3HQ&^A6nM}PhLcv*bOaz)xy^{-}5ah2Dx8vl+D?exVhtcue zDn;o!yZZ~ysP`496X#c#w_Y+`-Eo)`wEQ*YaaXcjz}=oiq7BCeT<^~^4H?H8NF{#? z4K=fAj|UxvmQ8>z+~0W|-cl>9sR5Z@GMF;r0p{+4#)pro9CaFH9X6INWQzgFZ$PQ8 zJfJW_Y^xkr}>s;j*3&SSCHgA{~CGTiYFd%4zS1 zudVHu38Q#E>!`0Uc(Phbsd*K`!Ew$|tsK(eVWPH-Jcixf0FsRt3dOM?rGFH9-;aTG zDw-%p9FbsyUDIM=hT3;&V)t=l2RzX}LYN-xqG9Ou>MkDt>}AF9htQ*$_)pWXl1I!` zwXG-U-aIATF2>4-`x)Snd>vLXlIK2a47K+Rl9t;pGpLTh!TWrK1wl zg&j=^ql#Q!*3#G58~FQV`&+^Tkr!m1D)7Le1rAHghuw*dUz!!?-d&SAx1Zmb2F@<^ zbllIlBVnzcDlTAL63q}g+{-=KCyi`=Cg}Hyu0X}`Y-#)b&&_u$^*d8I$ZLy0i-yi+ zDZv0{Q9DA%jI|t!!bIz#urv~Rlm;fLUw_1YNdC-GLwTjvt~xzn(1%OV1s5@Z#HmxA zm0?)l*%ew0k-|tb)6rGiYkEOpL0lh|#kh}){!zmeD))%<3MtjTu|@!XKUV>^Ie?2a zB5n1D?Ww9n1|$z?XH&+&TKjt2Vn*FUv>>VK1?5-2As3~}hGK)p#TEl&#j{O`pB+?> zk~-Sb&1q;Hw+1>K$sE%;hiSCQ#9Xf|`-g5zSg+}I#Trl*V_F7J8<}Kbn|1spI`heq z^zJXEE=x!aB}i`i%B&KWIimFL>4Pc?9Je$ciuu@&ucoCLh{*f&x!{tYEO+daunus| z*~N?J^n&7egRLhyv{ZijUlP5PwZ>TaXsRjLh5k!(dAtJ-UZ{&y6K5T?@gIvvrgUs* zwudbxFSKeuooe+P98lSe=~7*cDlng#TB(=dUpf&9U}ie5c;5X$ci5BI<>AEqQ`AuE z%8iMi?u~KYkW}RJI)QOJ1WUZf&9>VkSUZUgEL&SqWozo&AmedZczgJ?5WUfW3sz7A2Q4jFP4MpW^^i_MGbJ zrx0G}!|mVi6nACsTzK&8WLkEu>{bW01k(mN2Vogq{5-rkHaAl%H6B2Z7hL;!XQq^`&EzQ9A35*c2h0ij(?t0QN&ikPa*UM4mk^6 zi73yucf@2l$E`>Bo*TNOB5_UK^F5$8f!}8op~6Y5+Dcq8FaBBk(3PW7^wq2s;_>JD zcL43Y-D=}^$WTfS%HBaG-veKui0er83ts&E4=S|<(hR)JMa+|Ksjp&N1ZmC(1lFpn zDXw18cm0M&EHf zQyYrB`zq_yD$6eG?E-;Ir;(u3Bi@(BvKHcwx8`R%p20g`W#|s4Ha2vkgaoZ2CnjJL zn$FpUPJ!xIOH>-Rf7Z!?by;&sIOXI;OdVl(d&qqgZ z$2DmNv;6}Tm+f5y>LP!xH!Uc#(37>SmDb0Kt=B5QPSqJbWPI_$Q0zS)d}8kG53suIbVjLe|7T!I6H@K@ z{PrhcP{Wwxs#sc^`ae_wz>w)wttTmB@5=!E7>9nlc;*Vr*s-WW-tLPc;DGev28PiwR;xhx;!qulM=_P-xsT^Rj zYdY%o?|H{jr>4z=GSjLy{LJrap5fJ|&EsTdR@pwRtcDBl&htL|1FB67diklYjNZlMegfqmXpFPseMO}5FGAzK50nz^<5c?2 zlrJLmve!~Zt;Mk#?m4>QRepm*4fYlO0$b{Tfc;kTeen}EEa;%b8}_$+pG=YY*eQnCs6?{(HD_l)wb$LA3?3 z2upKo^?_0=_2FEG9dh@N(}B8iS4eC%&DjYoN%)VzzaZk{;br^$_u(oJM$){h2Q^`- zk9d;fisSL)d14u2EnuTT^>EDZ!r1vBs-VEfO6Fwy)cfo%6>xzcZW__SvO)9O7Y+mi zcn?5J!IQzX^8uED6=zTjQJlI5i9v7Dzf4~xRR}%GXLJ#*B>x%W(i(`B&6XM4>l4*R z>>^mn`7>qzg5en)3i^OGj3v;3c`YJ+=S|YYpf(D);_8)?=rSjVKqB}bYkm$8-L>p! zyrT2Q_MV-meGnjPYvF)(44ScibNB@g%^qnbav^lNS1I)q`;63^wxM&LfL1tGTey1n zF-u2n>ZYH$pbLYGa3y~woj1W*2An)P#+fKn!8@7}YmjJf{x(X(3tAB*<$)bYu+1=7 zhEplL*phk6pQstPnbIolM@E_lo;Z86Q>3kM_xysgt#hdT#cgxxjsPouL8dd>{TjIb zve306;t66G@#yi^n@RZ&*;Pjrw%5(fz#}TTjEz+si(vA(0F`$_j5t>Yo;Mq~rT7DE z{)!JOx37=XM<%riMf^hc4b)GH;Le8cTAi_n)MjN!*0*=vB6rOGp!&R4t^e@hn98rO z+xDTTV97Nq2!m+UG#dNW{Y#e6XA^3z_@^%D1ESSYI!O)&uR-G-w$31)xvQkm9ieIu zLXsg@&BFpaKuHgl;<(Veu>O>MI!lk(*p++OGsT0wcH7h<19$XF$_|XRp8RFdALZ=@ zmFaH?2v!Yh2`9%=mLP2)Y8aol;do<4FS<|Y;vJJu+kiRI!sG_?1eF`ifblbb@4Ceg zoWFGY;bU?Mp)B`OG`PKH9^!?gY{igr{ zkfBdasr)?LBiK!3N3=RHG$=$q1W@H8bj)}*%kjq^V2zgu4N6hat6vvnBKBHVN~x32 ziAflWwxu(;4eoz^D^qd1%Jj~KX+43dP&FG>jrxV`bSr|Eot)vmou32qjw%eh_uYNS z;@3h^wbH_xA2@etL_uF_Ms}<{EjmtBki*E(QfkgmmojDQKZIYrPrEDsFe%abCm~T! zEGE-|)L(w_3EORmeROygRZ?HJ&@7Tq_owo5SE}P|hCzAI{Xb=~O74HOW>JX~$eQy_ zJ?`0uPr>&xx`8yw!VUIKk;>P$cvnYF*Ntq9p~x^fE0U zGu}Vj47g{rQ9`#wWZa zfS#$kjK0z;qUTQ}87%&*;y2m7SQH7ZB||G8GOY1^&VZw_eJN>Lmi%uWyQ5Et=g1 zwZXt_qOmB93&SkU05`~TT#Js;-{V9yiPfZGco%i^3x)9ckBGyt?31s7qIvT(CEO|Y zfwGcx*3A*5i0AvB1r=YIpuBoH zv%jyq8L6B=Sa`y%sFAogir)pq<)3$kl8?@Kc|J2&Go!irvs_zY^B=QO!60IkKjts9 zI_6i1!kH_zhKp43hGK-uP*3mf)j_Q>-o^d-D^E2cRJ(}`7kis zEWKzZD#y0voR-)x0%4+2tbA&EE~Rh9T7VbC6qW^&cF6WBe{^<94vWQ%%N6a8bgF(lcO=Hz&@I)>_IvS}`?d9SVVvYDb zfB5N1R4{j4p;LVux$JDH2%jPN!}zQcO;$AN0?RVY0ICyM2u(~PBwVZe?s%|58(&^I zV}Y}XK9XK{m7;$YO4BLC@V1&@l+_e+D#s%!V*b3JfVc*k4H}w;(P<;^$BL6Q7v~|v z{nsIwL_ACV*-7i3!y7>@-y^nRnna)3h4(3sO04FDJ4sI7EiV32#;A&*sK?GpjS*ty z>EVe=^N0Q0&vF99!R_Q~LIOBw%;LIHE#pILDf2OluN(N3H$3~wCnT&UpXD$ZA9-Go z`qGwB*|)o`C`*$sdxqi3*V8@ZCi6RE7Mj`ngaiZv>^*To%8jC<6(G$Uz0UPA=#i7B zz7_e%t42f<#B$e+X8sV25>`WG-A|Ud{8QPd@wjnL0s-)6n63bQ|9-9IJVQyIQh@Yt z@;Q^4w;!LAM+4Om3#aEvx1JIo)D3whfhVGxWzDB5>8BpdXutgmJS1O!{#mea|36Hl zBJVes|NkkxIkU2kZbi3K4cxGTzj9kb#er=GuDVX!tOenoHia^Q@uZ;#TM%eh&M z_|zAKPXAyPsrf-DFPITS>BVNo8lzk0lG zUvBDut+_v~rs$Ss;~Eglonkk+=yKOf5-v0@C)#k}z{;j0@a0L~6YcEMzPNjZTD01k zGm+w()i+q*vGp-z#e*mdn1TENLxuOlel0spX8ErGZ-z4Thl|O6C0E9;N*!mUA55=& zv<@+<(Re|6bekvoDA4T8s59#abO=ZEQ9J6p$Hx$bwrv|uq|C{`>fWXISN>y==1}_1 z+k=Bvgl+!pWS<7+d-mnwksYDTWf|bQX^%K0?{hjSMLPubl}!WQuw2)6Be}k={0)9# zP0(VIjZ5{s4QT(Q*0m>b;tA3t9d;ul-1R>AeS9;aRy}Y}4CyQhA&4J8jeLe4Dv~(A0&O ze^<|MrT-dTKk7aG1sx{SUM^2GX57|p>{L%KaEr)Q5y9sTR~gG=JYk;>6Q{)hi?Keu21B|nPIy%}xcz2_-@AC)XzT3L+oPmaRr zLRfw{F8D>~8g*(4yIN10u96Z>V>1hfZ^-;MzeNU2trO+;9f`TK+^2|E=oTt!KX!M* zD7)n*sT`X)x^+^cc*yi5*%95+hCWrzEeI5K?)qyNU9b3$PLV_TAlCR+4O)P3F`vr( z{YRj|bhd>p2`dLsp~$6mzMCW&h_=u-z0m2$gaq^PgsQ1~hy=q3()XZi>i&OZBm<vNerqEw1 zRWQh_{lCS>GxQh%=ZY_FiFL-QX#~5|Im+h?`n4R6ps5AKz2F)V-AA9d)aDoqW}e3G zN@=MOJ{U})v)&T!KgmG2(5UpSbxfnDK1FsGHZw5h43L@#hH14k58dhRqTjfUCw%(# zn0=0%cZ*xYgYTC}~D-e$-NII*DmPnekf|=byPW48PBuV3XP=6-kXpOxy7_ut2{t@kN zN-B>SUX#>_n@9iQ*mciB*ntuV1I-*nz$NGRefOxr8WmXp%JpYy7qwlT~=VIfbl{F1;$ zg~aKkBVP5#)*u0z^-6d!O)&_T6#41^I?Dd+#+?~ajbKhs`Zdlb%b7{N<(=$@%RGRa zPyyvn5N|~lpM!vLuq7+;BW8n3x^PG4jGuF$0UEK!+xop2mtpngFQ~i1QQ5R*r#l6| zCk7f@J!4y(-<2(y9)7;QG6E&(`}NPW>595O_OEaD|9Yb~Foh&%%b-fzHi0QekIAZO zARa&;zAWMh!4t~m*8IFo9(Btaw>DuS0N9pk{;=*9j0rd06t(p{%8pj2EiSdNnQr{^ zbF^JxH0*1bRL>5N;B`&@m?EN{$VJ`Ohb+_fa?kV|WEKxSm6rFImOd?9^7A%n6DYb= zEXrpXU9-$1y>7@>ynF%y{Z@!T{x*2{Z$$2!oQgG%|0X z^o>1zE;A4r7pYtPo1}6ZW_+Yq?E)ME`taavoF9ZlBlj{?e1&lT?r+}AO*`DjreSP5 z`CPTg#p5Zf>bmkjW-=s-1?r#tqP{)u=vP4-D(A992l=W!;`pD`JnZu;FLNB7&s!i( z@vAt28uXh{1Chf$e?=aU+l=iZrqzLAN{el7bmq_1!WpSfJ!%T)@dT!-hm%ZunR~bb z6WxZp2rlI&I*nC!igkQHE*}G{r-y0MB+yLRs{ZIEP@U3HIZi+cFCz{5tg3U9n2r0( zH%~}%5Avd=B)(%{?LLoQIem7mWu%<=9ukez^SJx!(xiThPo~3=5D-^0lf->8U!2>C zmE0ER$x?O+9U`%0<+T$@T1>TDg&*q3(FeZGZCRC#HhTGu`VXK}d@ir(UkqH@bev%} zl&@v32Q31aLb3k$1)`7mzwrr`LNlubc;E9?Dg1n{i8_Ex$-78JBRSe$eDuTL;)H^Q z4<3I0`cAK!O(co?)Vs(W*!JnI=;&3MwaW8D0KU7AmtI*t00V4HNSDtwm-wy95MT(} z)f^(U-(+3rqQNw4`M$RPJpK-|kGW4eUCv+@E`G4ds;UNwyCaZmawy>pNHFm;_D6qt z+s=dkNZ_hEk*iBlYRI5>N3&sR2ZaD5pPZex}CZ4Ff6^6{$uxWPJbh^w48E34xh`*6U>xIxOUZ zOKoMj7U!@$sZ9~2xunv>a$fOr8wO(d(p~NZ=_dt1%>cYCtMn=pySz_k)kfe!0hZ^J zI=`35S#<&3kC?~aI`8ITEJP@}?&9#9NHv)G(gJyrKs9AK5IE+~HPQd|Q~ji>fA2t!|_JP~vCTORhZqKJf}u@Cs9zBDX&=2NP({7*@RofdSPj zA};5Cp!ZQDvJgdx^28*B%k^JK8R*#tg)w2*vx`DC^5D<-l<8cTmgHBLhQI(U7s7_} zON?Xg76HGD*Lzq}9W>q_LPw$1<@i-Fa2w9JaH?c_8Uu5Gs=w^gt^)XjB>U5#)S3R+fAMk8zPCFsx@KA4HjMdV9a#IOeAiwxG|c^p>I39E^w22`k~(z^$F$ab5fOluS%V!A8qD%31{)Cz>Hb@8>cIq z>qG7zY5-QHrXX@8YNDMdoZ8tk`w<<*xGU>v4xpgzz!H3PZU8-XivH^M>3FkQUXZpt z_A^Tg>TdY+5b;Rc6F!X!i&`yG^Gq+elWP`iSc2y6pQbZ+=nt_y!`~zWFtQaz#1ZAn z2syj(yvd-`lOWcyUd#=FZ}?zTSYYu0-TBJg3;!ska{twzEZhobPV_8AR!d+$c)Zv> zz4Nf}uGV8P2W+pkI@nA|@TEOmHyg~#C9p>$8vzIQ@hCADPDuY- z4)WfKVe~;m{1|fzulnitXil{`^{3x!6y{+kh}Y3|Mul$q@dxUSjHtu{FC$uc`+p=q z{r~o*!0jTgzrYLZ0{DA_6f{;jN=`Pe7XL1@D)i)3ygWB`vQ2R`fs;}D{wfwTK|b~X z@#Gl%S9Q2_`4d{Czzu9-c^~6fWZ*sQ$XgSMep6j2#?;BwWHEOrMzq`+t=*nK)L)_- z%u;Jyj=de}j&AWq-DLp$YR3olrM-=v@!!9|&u>nk?4T!W2;&=pY5HHrgJ-z$kpPTq zJ;7JQj}Fnn*`a2QCkkY3l6J3C6L!_3N$NKBvYs3iiLvwk9=!%+i*O{fE(W9Ups?V0 zu_U#C^g}A;mj0J!&G*WvLjo9{FGXrm*j>D0iuZ-Jz}XZ{%L)cc3UbwYMOQyc*}B1 zgwEqD>4zRTHlGH>Xx;JS_O-jA%=j>|o{a>hMA?*bv5BOcDOj+f427y*4}o|f3f$;= zMlo+$@O5&ZN5E1~4+);JR6|f&JGn=s5URwNH}NfnNE+x!NqzeXTPoVHMmewKB%LIgb&GAwTMh2vDrZ`EHQfloJ<$|BX*A$cd=(*^asK5_`)zclc@js3#u!{xISJH@SS9X z>=0!1_((%kMzU!XFu&*T!~Yl+@1qbYTci*eThD#aA?Z|n0+|n_xuwPw^O)E=06&o; zwUb&nOjpDY{WXoE-Pw$Gt^uRhn0rWfQrkFmqfHMDRSyUm^yy}fpC}EUaiky{|@97H0Ul@Yn%S*7S%U>pT;2^3o75ojN zF0iLhtmi+p^3+%bl`xfDn(Syx6=CqSa;5jID6i54>OYnz`3)LVB5>su5>f;-kri6A;>H8lAVl+zQZd7@(|BWy#kr zXRT?&Xv1EI(^@+m70mejA!B@xt32&KpkmUPDI4KGMu#?hAAkXP3U(zH@{dz8w-980 z`INh+Zt9iS5tHg1TcW=9pxF}yRU?s;RE!j_FxA>vwekx#h^q^@7uCvGISocJXncL1DKVAXMyj$Txy&6!r z7|JJk(X74GY>!e0wTmGC5my%axgOmz$=g$A3(YHN=slS4mQl#=G*DjM0H?gzwcs-7ol z2s;a*=Boxt{Di+t%@Ad44e!w)ZyLQrkhA+WHNS~u?U#kS+ z>*woBc$B6B^kYOBXz6l_=Gx2UAVe6`w+g0^ufjl}hW2oUvtvP{swdot!dNMrK1v|o zY>3_WKTYeY$S#+Q=pvi0^T|#$*lL~ z_gNIdr*A{_Yj6tEMN7m27xc;~){@&75S8cb_lENF-X+p^=s ze^g!S#j=&Skac4C2hOo(n>|dmWhJrEZ#-l#Og&zkcHuyo zqFm6+YqFb88>troKA}pXACvJwJhkb$5xv^%Q@Y!*T)P;J@8{^dWf-7-J zi$pZuO^`Mp^b$e-us_DrtCRA{@pX9$(zN5`v?NhD*h6 zG+#WiO;m+KUucIG^=^8kZnY+ew_89|Qf?UDjZ056exaB^w;M&`8H%eHL%)%!xII89 zoW1QTY`egiHG7(VPFf2lZa}rvg_rwtvF)>b>&2MMZhVMRZEQ(7@e!ucyfO=jOer0% z449=&5eE=!6qnZXYAs{0GLago7UAqPi+6q&{X+Z0Z134Q)M9a}aeEbzsgs?_JdOPp z=vJHsNLIMjLfpE~aV7cix7MNBzX7_usKY_(sWP)mO0V3~Q6rz;E=ExahWfzg+${_> z8}xkouE`Wzw@49dy2+et^?Mb(Sh1)I6|x@gVm@S-+Nrxa$bb5;@Uv^%~LU$?pViVNl($&*%;=<*m<#q2!<}OSI5|pDsK* z{zQTx`Wg*!xG`~j_fNdOylr5;#h3!Y9Mz=#wg!sT5YIK-nx_s=r_hqqd={^v-I$}<(_Vu=i_mN`HrRQwimNqJ zfzsFC%IoxZ6IT-iLE#V7VB+)JoSsKl+XOUXSashhGh`Rc_@-cwuEEc(O)%9hFln!O z&g(vg^d{=ycDon73%b1s33qNDe<+y_svr!aa?P>VebYetOzRriq$59w5R&){kD?Ic z7!`#u>0|5c_{bS|#95S~*PL*5ZLsrm-E`(DO|#Y8BV5q6%8DurcBZTvg>EiL)LE$M zeAhz+>aMxKQFU~OATs%}I2SnWI4SH;yDhy@!OS{J_$K)v@-AG=o07k z%I$aTvE))(e^o8WfH7b_Dbfnh-ocfpr``Th?XU<1!Vfq0Gwq1Ba*Br22e|Yzzm`qa zuZ!qi=WAPkcIDGcCjpgI(FInvF=2rQS(>?yxMgCA`)SsWSiW!SVMjxAC@fs?)2k<+ zD1FeM3Ph5QhO!TgY5O%>p~mxP#X#wS6|#s>2_nLETS3<6EanY^WDKE>q@lep^sPqu zxZ5HqZl8zSFd)P{v7RmJtTJ%ZlXmE`h9sLUV>qvR>({@1f~czfR4UTqq}90*;^K*N z=wQ)Ud?JUduGi3xeQ+g8B|52AM1TR)$%6w!Bz`=#f-winv5&{FwmAEwYOSo@4%A^k# zz3w?o7nC685qfVz(Ne_F<*^>44)S`nnDf6{LzOP;$)Sr+u8||JmF?<7D;omJ?cD#w zTbDDL8=m~s@DZ4xTa?~EncgChC~n`1$_IQc$#Yz+!Wy4^mlSpP5@O)iYXm)%0Ku@< zpfW$Lu#|J2r*(tkg)4&PzMLGHE~tT`jQ0$>;s?V~vV^h^Z5A9Fjh_=gzYa@(O-e(q zxzC!ub^@09A>nhup|4^?4Z==k^j9?i`qMauK)3uMSn@%bnC%qkK zXB6WDAwT_G8O0Tt`z}2Hh8d-0np%F(0hu}>#Ol=k>0C?ljVu)yD!3M&Ky$+&Pi=42 zJKCr^zEW@$?ar5+((!U*bxNR2|J^oiect@V{NpjK1=D)+UBkc)yR>y=iY8ynJydCCM_D)clr5j9w zaB>);F%tiLdNE(}a_tGjS-Gr}(T%ZC2~GlLJT zlB8G)-QsW>&m0fBCJs`cr8^h~irla?9W2>2j*l7C?OB4o^^DgB>?1kjy!uv^6)qQo zGgS`_W=}s~@b30-zA<*TV=n|!%q>7IQbvk}>CsTzVEcI02tx|B>q@eW*<0`UkrUgK zuP(SY8Qt3YGwM4Y@@*?@Pm{WW!L#_ogMM=8IgYtB_WOPb+W6L^P`&fr=@(ouUZPyI z@~VpA4b}tqYT?e9TY~~lVc;Xy!wq&f_G^GSCYTQwNL6e6oFpH3sNbCIVyl3C4yy$; zzl}H__jsbs|26wosWa-_ZIub&nu~dN5}j{YJaKrDVEBsCc*gq0vw9Bw^woOD9RHo% z7|P>EAAIa5amB1ALygm)jWQ_yYzM!mmFADGp7niNZB4jiblo59FzqQ3p1bt5bk8NU2Ma_vbwKqNL)n!d#|{5^5;pN;_K)T8=UC) zWlhasb|3+8&Uwi9iXrxh@h~iD$eHc66O9Mr$S5vaT-G#_aFasjWY#H_zetUX;j#sbFBr^}5esv0= zs-cRf-V`ZasVgdkbiA%D#NuP)b6?i3oP)i%a~^pIOv3&$bJPKXV39|MOW3-8!LfWs-SX*-7tnxFzp9*k@_E_+op!`j!%VE zJh|33O4!1GbMtA4a?L;Pf?;=wJvtSRW*qk@g@5ErBF%mFd>NpL5n)!hD{DkG5GUFs zVdM*Oy%G85d?GVH!$*7jlC(Br*pM+2^H-G3AiEK#DxaeT$*Aw(&)~eSQ%7W7&#mjY zI{c;*iIOS<+C<=4r2Qj~-Q`Di>c%MGSte@l0y7P0V`9YN^(&(82 zPE_=8$Wef~lUzP8w@7k^ycth(-`#giiX^joUXE6kwF93Y|K4=DO^@b69gsCd`UB- zIk-)%htn#(eUNBW?DpJ{ub_}tCr3ae(21VQn>#ds-X|csAl?=im*5Bz82Ao>^zH4x z)?OOF7PI|40C@Mn?oPcZ?JNy4D@wCjqwzx`_GNhZcNa?Wd= z)YYH;@^u8A1`_r?sK6|iZ9V6BNJClF)HU1_?av=xO9?RlPo@H|qJ73CmfhFe9AczD zuy=FK#0wslbjtJR#v) zOf5)0t?*`*huo!!1-U`zN*^aA%F9JswJnzaiW0>AI&mYSk_)r;r`XZgf`sxiQg37~ z;`IM{qpDd>F>vi7my#7-czLu=`3l`V+B!>y$=o0A<6Za`#hopeaovM%P5I=Z8yE4o z#x<9kXH~}x?)b2HbK+!8YT-l1w5o6<^=rVvZ2HOqV?yjTT%8|U@r4Y=n2+U`QbKS3 zp)=y9@K|5#5DPS|P)ukO8uTaf*=*#ZDWguQ@F*7+!M?!(9(5gIz$K?BdBa4(95@Y` zhldnXSaYDWA5$75ld_%H&$1InG$PKJvGYh9#@~; z^}NspS3G&r^X>Nm7LU#jbyzQL>7x0-fZCH$uQ-Q*=B$tOcn77CuJSIJ>&Bi57%>lJ zY#M#leUZm7;1M%qioNYLj2y_+CwKrlq8^d%j~Q$^*U|6UZcDL3AnS>2+uyT z#O_tNEjv4`m9~rL@LqBu5URSjy^b}~KDcbl@7 zo+B_IB}Cg+eNOFpl>Re>z#ygogXm?p|GPJ?XGYjQ_ZKWwh@IX0xt+t%dgi+l1Dbwm zT*s0^c4}CmlFI{&!KDkep&zSP=0=U!Iahgl6URLxuKCpL^MWql9q2}1PRamiEtr0L zRp>Bg{E*OuBHDTIzTeAr-_uuskw?2uga?G>8tIhfOk zB@C@9Ik_OsA`6;aIQu}f&t>IxEg=-=(H;7`tBAEzy4%z^qgK@YX&Pghlom9+?NnRM z_iC1)>BzHJ*7{1lmPXBlZSbuEn(Gj-2Ddb7{>kCDNX z!zh2&=#i{L5Uter*<}d{iK8C2Jq0_s5Jl?G5ag-w{d=ypYw>0uWm$~k?}j^XVRDiX zvPEu|AA4DDv5z(WPBSxw`a`Rq3^s?c<}>I(bmb(S*=)PPrj$a6O*~YZrK34*i<50W z()Qy;77ZS3*NVn^QEzHDTUd^0TlB9bXNTpDlh0)jW2oK+^T>4RZZ7lv-%^1cpzd;3sV7031)BzY1dD*8M-WvbJxOBR zST`i<3VUyj?BuO8-(!ir_M2CWUjH8Ge#>iKsll~b4M2nAl^`97FbK=5vya_mk6xP_XGmYFbud{Dw+>o76DEwu z_VvzUrZsiyDEP1xSy1;159(GxXOj$I7 zUwz#^O2eTO5TE);S?e|udrwh2_8OZN=xMVDjr$#f&Hzn8}zf!HH=MEP5?H7=h@oQX(pDuv(9VU7yZK20hzig&v?!2v`%B4ar3Ou z!v{VNyh~2bCF-XdLTyEtxDq%(vjF#cPH&rW=)U`V#6TK2AFjdX?eK5}8b8-CdOMJ1 zfB|`RK(^{9mRsZV8&lr5IaSVVB4aXolkQP0g-AaKeubqcn^djmc0R!<*Hko^*HHgP z=Hl0+%~oA27mF>O#j!n)l4>g=#Ys4+=we;H9R@SIUun5#_aWB7)s4hkhbgIC6F2s0(|a)XD?hSk2c(Cjx@F|4_tu z*3YQ3_cEy(PBexF1ZRCa;r1)|)4c$52l6IU^xYB5S9cvuC$0(2$U&$7IcvTq=qe_E zRm^>FSC94@>`2CkYWu6aM4JP#y)NW>;k6ofbm#+~6;TuKmwu6#>;fhf{UQ`j5z<_R z%S$gB%6GIG`HjqPq2ROGUH@d2sd~ zKD_d@uBXgWCaQVA-HEs`UDSr1Aw@8og zr-i2U%QnAWA2Uqqu77IG>(2kL31>l89`O?7=5g7X7POF`FvpjJ$4f0rC;_%Gv|b)T_BeG3-3Qi@GFhwx(~YAXT5d5azzo_YA#n^=qru zj!E_Nw|T9|pHZGb7gy)hpb_)U&BuR>J4u{ps`jnE?dP)^T=jSnK1IK4>snmn#d|PQ zJsX`778;MzyU(i&7asM3MU6b@iu+fh-urqtS!z-JDa-72WrD2P+;UR!_ce{On`?== z==)!^V?yp1As?y9nhZu<+;TCO9OW1Wi#^dygA``uK1|dFzhi??dG}@oW8#Ow7$Ds* zzVFSbrt+V+ySZlqw*+sm0prpec>fCXR{pQikyKSU3vB)Fr4g_%EZS<}f7abgoJCt^ zxk#~0!`Dq^N&f;3(S}QM-|<(5!B<&>smX#eTJXrKt@;G_UzE8}n+!U^zT!U5chx@2 zDoOP)VYTt&9!hrZ%DfxyQ)V$ZQpelj!P@FzGOznv2HsdI|5SAAkKw<*)8p^fgtBI( zD183bAeH6&b-3jLZkUIwQhpeGcw&l@#}u9wBjn6KMOmS|&bN!CuD84w!+9=4@Sqm6 zm&Xl`V%CL4lLrczftj?1Tlqh>>S>n4FXoOSK`~SSBmlY zHTBDUt1wyb$fQ3{je3uNgu-B?a;?}1?rvd$7+fp%$sjhwf0j95_deoe|s{vRX^T6 z4$=jh6GGU``G!J1R)FqBd}P?7cc5#Uco!>2^xm%Ax9&z` z-`jychr*Xi$hv8ZYYwVgg6efLBkj6uZY;a%OjLWHGc1=&Pq60r$Vd z9tz3x&JyI3w*LP)eVQ9Jxia99+rIbb6YIC!FO$VPBkhxF>^Jfm_M&*o!}rDj&<7o_ zqRPGPUHb#>xnPSW{vi*oO;YEVP{VAe#mzc0_lujQ3%lzd?SBT?j(V1n#)yzJhbDZd za{sl&F+F~w3}605NiBWq?)Psj0LAOE4%})<$ZYsibkACK65?7h(#c0tsxANH*2ZHy z-vnZ7<~PG5J^U1C^7;0hywAv=0!F91(MD-p>K9jkeiU{88z^*)L^IlU=Wvg-m!jBK z=E~#c(z?H%zHY4{C2poQ(!PT#o^0)Qv;>HwysbPVMrdV0X1x{@%0J{uvHC?BW~lY5 zL_qjDsTZs(Yc33K=@9mdP2GvX)F9Wq_CX#*N*P?Z9;u?7I(*aY3ux_4%FDaK86OX* zNk&JtginLu~v^nQXXqv42?|_w$#z zqs@g9_$ZRe5I<9Yf z-Le>&Cu&mPmK{f(;^n4vDEMxX4&-AL8o$QjBIK@Z&`j3#EnKDL>J*;{FfrJw19#dr zmL`r}|LA>MX>aEXq~K{oAF?JqP1yj+a~M!tNb8g_qex+1mR+kQpRET@54b6DfV|=5 z;HA0uGM0lkkTWCgZK43Sg!K#WO$)uC^U!XPa=!S0!v8XpR38W4;&;fE{;~CS`(8Sa zG_f&a4L+&KQfBmjONb{B1=vP2hbyAqZaHuI^6>(_`i1Ke@<~jU5;kc7rR{Pmx(NxrzNn!O%ZgW} zEs0oValJ(DdrbX}X**o9+>>ssFvejGI7iy6_Y}%s?|<>@TE9GBcjqIxuS}L0F3yCX zKY6NB=#V6S*=+LZhBb$0iYKRmz+nLBJd5Jnj&408LYs41_V-AT`}MvbhCZQ_96$9J zKYh#ZJKWl>H3vnXUoVN%Sz1hK7X!oAG(=mw?T%Dld~tf`E9zOg`H39`s|nJugt7%X zOI!`zUd(BGJIjn@6CVg2X7wzSofqK?4y=710I=Dq67N&2L6LZYl1&mjT9;G5-+I-9ex_r_3)l zRnEiT>{-Cb>7#H@!&!DLm$z};f}S9m|FJVg{8h`Ncg-i2+f^14)~%+xN6XpgwI=mQ zQ6GD2hU$sFPeXkottqw01Nm2rEufpO23<`T$?|p`DSa)OrKsW|DNN) zF2HZ7>rz{v1H10nkJSMr`$>-^NhEErYf;vvB7XJ~eIXg=%~7j1o+S|(00>hvopuCV zn1O>{KYT(vZEjTLGtxeTYFwGCze9g@V2`u4k+@#0`QIze6E(Sn& z|IHb1S}Ze_(gl*Z`d@;}0|?GS>G`1njGWB1_(w?J9=#JtL4^-#hU1ALfi@6+a*Iu zY%0M?MrKCOMQj+g=BX~W0M|BP9APYVff{{! z`R}fXH<#^phHRx_=>XL(q3^BRKGM zJ|G=B1@s+jy{2VW=5+GA3yMLnm48e!i7qz5>6P#3nQnz->Y{3t-9{ z>EZ?W8e%$bik94FQ$)YI@kqks^C5P%56}W%et+#KofQ$LOvG=m144+8%BY}m+GhV9 z)lqj*Hb!bM2p?A(k$p)ccMVt(($9j%mYS&)k5;XsoAC`iW`~pDcjCByr{Me29d{FJ zFGS>%@s7qnUv;{63S=R^q+bPjelN*rPqKYw1QOo3GupMPV04LJys(wEfBxhF>A+k5 zNA4vTx{PrhTi8Ua!F_oH_0{vbJ5yuGX_=Zw&b%-|*V3-dWx@ELwXa0{&Z|pw-FkU+ zvH&7SbjHu`qTzYW03@Gxr7q$0Ebd4wBEI?!&0G>&uE4qf41U^YPbcI#0xfzC1iG_O z+^y1&0`R}1a;`ki1d40G{piH|Bke+Ls?#IrI#8VrcHtFsm@ZwHngM)38M{KBO@9(o zPj9A=FB`!!{~kqCY2r0tah85{d z{8K^LFJE(_#`epR_Dr9^+=7|>B-FQlQg&*v-5>15KiztJlM}?s0h5b{Sm+4mNqJ?) zzbM0XyY3vt!kLGAzqy2M&v8@rhE%GS$A8;U%{N}&6)dGqSS|T{#=Km!rVo!u+Ppv> z6#?la2m^cht>4UlbJwypUHRNyq&w+gUR;ahkTIG~JHW7x+&;6nb9PK|fK7GFr8i~H zM6gs0+7&_AJ-!Ku7$&mQW{^x7T^=Tvm(ld;c_+pO3R`#dsw^;lqb*r3iPLN~A3m=U zoXuslZH09K{Docd!IubvJ};EE>*2x4a}yzSW_ozBN3=J7EasB7`RRG)X4B1X}+G~$d8 zCP+I0Y|*8~WTI!EZQJr$Y?uNfoq@>kH@)0y#G~aVi9?bTk z6Idv!0ocJV>P~q1)%Nqtvu~l@UPK~gtau(Uu-(J5<^P2<9E@1AuAk*kC;(-$&0<@G zDSxEfaKr-@KCj> zS#)cQDgqlbZ|dVc%|stpUG|DSQdHFqfY(P6$rHG*v^(HLV6C~d?f0@H6Zw}eu(x$-C{D=M2F;k;-75lP50fh_bH%vqj?IKg6 zB<7GJ_N9?J;NIS?a)8+Zq#KqK$Bv)x-7#|4M2FC*@&)iU9Tgl>C_k^!MsB+Ajoj5z zb`KqJaW8T^sx<9IXMF_6O1NSdtLP3_!R4GgMpMNSD1NT5-&jMWpqUYbodXFa5r{Tp z!DZt;pxTR%gyGs#dl#uV6(mWkh+=?EwY2uK%cB30>KM5HMwy-5)a0#ZZophyQPN-rv)(xvxAIuZh+R4JiI zqz4cP5W>6BpZ@RMd*{ym@_u`napIi1&n{~{>silPr%2r#yL7EB=5`%}#^{r9dGw*6 zm+=TLWltF16|vXxHbatlgHVUE`7SJ>&HqzIBm@>!8K^k5cOCcWl~qf{r&@R-6|ZM? zY({Oj)j^=axHH*0@$BK{?BMe?SNb;av&Z9Bk51~DtQYErvSd-s$VRwYK96`c5`IGX zdY2;MC&erq;k&aKs_PT!Fcz1bb0K-`W`zX#L=Bbauhtf5*E;*sjf<{H_3S?FPk1i6 zJE(L++P+aV9+*wlk_({~64zh9xXBG>zY3iGMjGnaZ96QOaCN+G!qCl~+R}tlx)sHQ z_kM0g-bQ$FH9J70`Ag3r;`xHr2~meMNJrqfFuOKx2AgX{o-e6$O-ZDY;gL&`WUt_n z9NjW#%YK(%TiOxo8{9mBUkhln#V?4+(X|b6Hy@RcQ|PuW2r4M->cmSwh@laV=<%St zYACy4_3W8zCgTwb6QK^_B=~MgnAk=d5=d?NURGy^uT8JJcrD?)vUu}ZD_L&L%d$rw zLN@S&b9kt=RNbqNr>^kLyofQQQQxmjVJSJ#PXq7h5QuWJV7fY35cFim9dCmV(2$aH zc^ZH|fbWK9E@&>Gs5zR}7m0lX=)eknh z$?+Y^Tz`r6sLorTlBQFe{MT7fSBc7%rVAUAU@?JqoUk)rE4V%HHfQ>Dk(pp$!*2Q< zUQR!-jHe1FklzaG3NhUzr=u5liaR0PUzMCLuNfbl?rjrmU|pM~l`}rLRzG}@-`k#u z>MCr@8Px8-DX6ffM;-W8cE8?V`9*rVsXlIgBm^~LV3sY^nIh)W{JS4&( zA4HW|ulx)>`}7^-p%!Va3C$f3q~d*;IxVO8>N8RO5WBnePP1&c_xE>vhGvH9RdYNM zsgTydEHrj5nzUBLgiz>Hy}JL~;Ka*HA3h7~hXpw`JxHu;JN|s_=u=qE@lL(N%OCi% zANarkr}zq??apwPiptqXN^Xu^hMzUk9L=H1_iSrk>yj&nsBe;!d+wEq6JsK3Use z(K?y7J-mF9s~2k?rkxF~oxT1Vu||&FbbbAC^tPHV)?p~bIWKNz zo4?qF-AVs8^3Iq@nY`+%&P-88UbCD~RY3qn2tGpE?aKKNm@(0?O*)TVUkq%=@LAof z5tI8o2nkg7O~=9Wd<^(QdN&)Xlk#5Nj*qr!S*(G|Mo(lLrS})VGz09a(ap*32nP$2 zfR~3us=|Z=mre*oL>JZ24-lo?V$3p1Ss?&mgal7Cl#PZ$2755Fhi%itF2tM^E(r zrUH0wQYI{hX4Db}lUZmnQoT1V?mX!|a7Ir}DzI}iLPklkl9Dy}zA#FZD|S>OYsWXc z7l<~nCAvL}TVmfa##)x>?0!7|4vBbmFgzaq(N6(!8}$xuMOjvB;H*bw5<=ScqNgJ; zoCZ5p$uE>?^wJF8L@RSCG4g-_W*wr=kw{A=F3)^&I3}JLX&OfphqL;BnwWh>B3);P&`{nVZ1gxYMk_;V2UPB{=a+%SN)}k?*y(L)BMz*{ipO@Hjy%+{q9e z_YzL?{`H{-wF0F@6F-S%iB<$n2(`t{b0g|2eS|*qs@k<<`v1d6WQ23e+Cd_Tl*x2> zeF&4aP)evYWTQ0HGcVc>(y_H8Ef?uy9(M9&&kQ{^&o<*apFZ&zhdVboH|^2^+!iZ( zDh6zQQ(&f>LxwveBN6G-%FJ1eJJIjfT^N;bmJ^buosr_!fSHzB9IvyP-Q< zv7xOVzGF_It6^E$P;;m+Ec)T8AaSl_*3OI6Px#dwG4y2WD-qPNknx@ORQ-7R`V*$enAbDsdx=z zXn0j2kU=SMnkUjn@tksqIMg>UI>j+)Pt{tXk?MsE$_9rU82A$O&+aeggme47D7JPJ z!E1fnX!euV#)L)6XG1rCI6jGhBdE#R!mXk zi*Su50|K-V4!71h8Jd43NpGI*U-Uf@_RJoy?Pd=i(Vobm}hu&bfX8f}|D&r-QI*}C#8PO5(gRmt- z7o+D^@6AYB;%VX#ZpgA&aCsnD2zwi9i`f~Y8IN2sUUVflT0n0KqhwLC=&A0<>RAeq zQ95NG?oi^bijW7P!&jw`F3fh@bBS@L6Cx&4N~%wLxMOho3-;>GV~h&2nBiOFNE zkn6m8c+2>Ub}bu3DLuY+^9s(%N;#kVp2ZJHR@e{h&gMB3GwKoA!msA-T@LZpn3J{Z zY$sFeRG#_%7oTvqT^+g{IKFFYy^(7%T~v-JWx`kY(_*2n$~e#sPlMXgJNuM)T-Ml& zc1s&QCW5%m5V9%GlZi&+kq&F8t=VZt!jrDSS@UxOYkywt1W`#qZYPO4VofVB1794x zo_*xSw$NAq?PaS(LCG>yp!P~bhwQ$?l&!$652GYMkd4mniG}Agz#GP26&r)p+6pF# zYRT#x!z2Pe+%VC$u4dGzPAguiVyf(utry?ex{zsLUbqA&6Mg{^HDq{as#<*Or8dyN zUztcTPy3-?TxM*Go+@hS(TmaxLtaKtEj9Gek%}Ktw?)_*j>)6T<82lG22RN#&HCMn zR+)OO$(`lS0X(F)SJajL#}cp7eM@9K_O^2TXu4CMrDsDvFzccf;Vco-EWog*hjNlG zY=2J4wozm-ZWuN;T~O(DECF{kP?;U@c}M!^olOvAUx=Dsw)*!5(cfDJ5Rm@!seesB z%a#ALjpco}J54_0(}A@&4dkF^^y%O5jkD1K1b_Yy6vt%+HYf}&)-r0KifcQqW{-^xsJA&vLygzBZ%&Pm2w3MwAg=3 z^jthJtOLVRtIdu7^?omh(6vv0^Q-@UqhFbg{3W*j{z}}V{C|`I|KGDMPBfPPuNL6% z-T(al@44UqpL@hFaKLks2s0veV%4zV8>0D&Lu_tvBWvNx3!wV5s8~Pn^sfl{<3Jg- z1j6S8L0daxWRD5gItOQa;2@v~pz!@7TQB_k2rrVhGi>2`u-qGh`MmeY0%P)EYQV9Q z{&p&C3wK$>zq4MXU?W+h@g(1CADoi{aDg@$KSP*Hz9fhC?!1nAcC9)WyW>hQ#Q8HO zWZjAQNq1g|6J}>$N{lKUeJWmQh_;0U0l!DQwaFQo>qCr_SWB2d7Tt->r&}n#Z$TQ`WJ)+TtL6{+& z$XZFPL$;ph_rpk%kr8)nZL%MSCSZ3em@8?=iG+G!J}ra_tJ&tv{h{>Tpty+@!SgwE zx#$pK2>vC?>O_jLA2T@XhdnqFOW5K&os#NAk>JwDz?q-D(hWN)NADa2ke(dYi~Qa% zNuJQ2_w;B17O*CF5;q8I7Va3FWy9f|V{BXg8kWnA56!GT1G1AejvU#2;+Exn!=5~t z(CxY#4C(@PQ3ptClp{SV55{F@#r*E9FPUJ~pj(?< z55QI>q78CBc{Bv=@f%WhpekB5+BS(^8s`nMHZ#4V(xXs#mIpKT08vb8@rD~0=7Dt{ z+w?khVCb?Odg{Au)8H+wh!IiJa=39iL3SHRoO&<%5&4G&k*`@-Y#RxNRKjL$XhPnj*2_Pvsb=X0e2?D-0 zej`vB!#0d{Yu#Y`$9)i@tNj;aDu+yOd!C7`$lqc|Pxbi8-4@bg2uFU341h9R)T}2y zImEyM%vfI;SZ~mw&})`n`Ay@`PXZ4FK~hSA{a@~%#~w&)-hF#BGL7XJzwb+*Uir>K zCBv+yiT)?Y7cP6^lx%4KWjRaBjEQKt@(u&;4iYq#u=ka zN~kxAg@^GOLBz1fBSPUFvM4|%BO7(OWh^ZVi|s6;X=;85iWwX7V#rZf(H3eXJ^dvH z);lbLFDn8`mx_zwqGFcK{06Sqs*A9W7EH-7xP{^O&`o(Xp6=7fZ4tvCa}X3Fin@`O zGq+j%&BBkD8D5>nHNB4_iZcVxTA;E6c>Zi8-ynLf=Gn^tmqj+B|1=zrjgcT%_v}@J z-$GB7(6av%(%98skYUB}cUQWUj@DG0z1JU4RPlCowRw5_`N?KU zIX|3k@+vi}jp5aPRcWJI=JeS-tOC3o5}xrO+6r6pu}mwHn*HZU3Dbin&(}dmdo_>0 zg!A3ayk}~vsisECD)q$r^n8oP^G zy}LTLv(>}7gFo6l{iknafBBU6W%X-cXf4nZqDRzDEbe*R*f87 zssK;D{=Ht`(h@&`6E?4lV-Pf|v{f%-Pb!*YA{hJebM6)ru}927KsM^xoSaPO|Ldnp zB8TGR*)~P+Y=xRBd=d`Bzpr3q)ZOH@ip6x$nASaE7Bns|sHz&hGjI&|LpBP_`pYDV zS3liKw4Kd#8LM&7KfTWW#`N;gYhvfgn%4~t>GSgsKY#u_UgylZI$CBskSQqyS~F5& zJoX5WPpAk6AI3%NIS*_Q?v*clpAMJ(VD9OY?5%(Q+T`UF6C3L=)o=}TLEz!?yHfM| z1aNA#=d#{tsj2?=*X$U16nTV&9}>|`kP=oWIqg!>y7$Jo!qRhfG~RoC@=LLy;At=3 zzfb$)$B!oyRS~sLV^xp=mRBMVKC5$%OrC@CW9S2q^1_d|A45_wuPaytZ`HX>XIi%U ze*uRMwlzyS%!}tTFfil=`5d91(wH=P3apNf9KHhRHT8El5}ZTj-91}5(nBLLS;|n@zoM}-B5#{bE0tR zc9*Z9`^acG5TD37??h62RlwJ?Z4b78wCqhfwHz#EtxYjENmbHgpF=lBpLFMvd>HCr z|K?T*1=nMi^SnaN87KyN{KHO=v5`>~=pf6WT|-78lY3t;8!@}Rf^OjdJLwqyv+{e5 z<46pcCmus8oe7MHptK6&)Fa}hNLe?8ojin~{ZH;4pz$uW8q+V@1&%~ArZddu#pIU+;+U7gxbQtW}bC0bth4EQ{WFWb)x-dSL@uK8lD z_hv&vSftCZjhPONmYHWLhbTdp5H{fuJthD8 zPM4EKvN+&cHkf)l-K?%+_Q>o>qE6j~4rtbDAezDk@@`&Gk%X*#AP#?g1pWCsa=6*Y z6|C}=z*%6DP`hJR;0)O(6MD8H%fSHfnd!%9+CtF8_2=mtT)w@hB_=zIdZ9}FT0M~i z>mub&zS^I%g-X%*;Mt>tXA7!KJEH;#l1by0_=o@>jp3^`UYN`K_1c z8^vQ`m+c?}n00TW{$^>{nd~HP_3Mv^@=F$U9k5@nC{QP8Xa#O{l%GB#*gw zRpR>77h0siJS{dXB-p}32~CBuq^6A?yt<0J7y8p8%LiNu6M!1jqJmiM3ImH@?u(&Q zzj9I1@xt-pZolIv(ca-gZNKe4ekaNsrKAZ01$Fk2QJQp34X1Ksjyx1t?61u}oBr%Q zvORCqVtrRWymM@d2_zM3dC|@dks?W_CMlW&)~y@ZWZim~=@lYY5L)*?!4Q9wab z7J0;G#CI5*Q3gWSXJN`c905YW5?745d#{aunreh*T(c$nUhhiNJfRQoi!f;99bwtH zee)&_uoMqFqiAb@oww`}5ENYak!A$}W==9<>>a8@n)GX3KO&gOg1x5V{re~`)#q-% zzNmkFqZS{b;aKbZUCPpTjubMO`#|6KSQOdVzv6Z2Z%CS?aL2-=uyD!_D&W0#jIp=9>@7f`RS|HXfL3Sy<^s2(Q zqN=LoG)w>J<*)A|g{{j-5Kmh*Ci%&#&A;R&Q^|UzGLWQD<|%)k+LrDjom$ zG(+6}y3?r8O(j8LcwR`ft9c5zGSPLV{0mG>;{LxL150eN3D+|OZbRYt*H_#=?BrZ? zUr1OIveE45ZI>`ZlOI{H?^9u6;UAs!x^^fyi~NZlaGlNyTb)4nez*sewA^cTG~0X9 z;n&Zktzi? zfadd#C3P-0WKS8c9KLrB*b&=AMX7hO!G_MWZ8=AqusomHAnC_G%x=%1A~h14bm^@WO!0HJ7J`#9X0HLHy4?AvKS3);hTi6r;{ptg+|60$)z;F$MxcK}mb8@|{ z>v(0DfPT?4!w{2mY(PXJ=1Sa4D4umTJEyG`fQFB@J-}nU&M{tDf19ZE@N=p_AXwxo z&9hx#ten5UpbSzZ%YM;TQB#u&Oy83X>5)%n*ksfy2s)Cd>bOR7oft6@ zzk-C#YOLFzQJB^<^mZrnYBzur+9##&bkvt=uFY3x)WYi{CzOM%07y+2vmwfni|{W0 zP+vbAoOS9}g>YW=P#3)!>Re*TD53AUasD8ycRD%X$eT#w3d7dJl@f)$aa)_Y3S~g* z_J02ag(Lh4D~Om2#ZGMtc54C3;XBfD8;>3}?ekiZB&!4q&gZ4}z1DOooDHXD4m%jc_&`w0cCu6hE?>)t>;RHIeKT@;F zEK4Ro+LD9iK>|f1&rPmlgx?h{{XqQ->zjf0Q|@}T3pV1Z_wrGK(&6LV32Yhr*JRMA zhA>?*zz^?4I{#dhXr7|0x8&lUG^dWA({cL}T&gAjF#vBj)hBqC1{dJQ#<~ zwpwPh0}gx&7|xax{E>$*h}k-d%YJAi?RBP-(6sjFd;u^!|4o&A5EftW6|)HwAz8i+Z@1 zp*RF#4ZN;O^C)0wg!+#z{iSGctz1(6!M=A#8kD6tXlH6WuL-J5$Upt^RvkKS6G^(h zaTdpw9A+orReM|p%39oxXJG(GDAAZcGq^oYqhZ(g^V`T?2>)sMiD6BCH9AlT!t@~y zyJho|mW+5d*6xz&bx=bhOM@!@0t16Pa0u#jEMbGHi~*ZD9|85l=BC0!+?Sl;MHHOZ z)(V!`ootzF$wOzbFS;7hx!PbjpL**`RO@pyYrH~~(ztmY?tvfzDYsgxag!UR%81VK z;V3h!rJM{hT`6AcyjK01q(<6lrbX61FUXtcHEk$>jvh!^^s@+(U`I&fx_&Zn$C;r+ zz2!r9FMtvpT(Q+iIl;Bh_|*ZSx0&iUv(;}rF3^9clN50qZv5(^w2Q?(Ln&gH>(z0N z928s5L^ruX#I=YJAB^LXAZn}X9RFzh^d?b!Xh^#0ZOO!E4dBumu;62v^_s7jDdIz2 z)ubVJtkYmcaODA^%rLvneb9&0N?26yO>P42+i}(a8mTl-5=g6GKl9Iv%H4A-phm1+ zKrQ}S2h(st+v-qnae-OUUjocs>Pg8jh&P%a?a+a!s9*7?$RC7^v7VP8-45?^80&mP zqBG9Lc;4{ZVJ9*>dzMovTa`vpVmM5~@mti^LQkadDE^9JvXVnNUw#)H_&y4l}dX!-gywhe> z!s&c zhXZB{tNW{(2agw|1)Iguh`I?``yWTat-c4_J`kkelJp~c9$@w1`&2B_>D;;m@F^WEMK|g&EQJ+@-R46!E%J-!v{0@=gdlh zGWI!MMngp^@G{eLMbz2Qj?k=>o`7FJKN%W77_lp3?}%d-=PNCo)r6;2v)o~qK_h`U zm_t-ZXtAnaq~B~<<0Y^MX$y(UN{_#iA{zy(S>pahduMnPzFodwgk$Pk0Qiok*&h@P zGt_zvpySYz6+}MQGaE_@&*@tLtOeES&RSr{O3ItA{``z)>?~c;n7=x|$)C8t{$1MX zrAsoyg$r)GBgW(pmM3FE)!tl@MIRDa>2fG9rQ22+E%TWI1J^hoBN#5_Md;G!A>+VX ze5If!lv&opZuIIj&+1g8c$$TW=1>tHZd}c#V}@+34%BbMDkh(DEz*EQIWN?{F475W-0+9p%# zp_>9g$VL7fPrad{k{sy7z_Zfn5Q6Dsa#0x0W3t|rQ$?n&wcEX?a50H!YB5_7;UZCc z$aQZa)=@`?+W3JNgRuE+eU$A~gS!nyB49tO^xNKV1Z(LPHny}VlsByuml`^1O4+oN zB*?A2&%djjU^DW{D%rr{6bV`Q{dj4r+eDZr=0EW$?aN17Yxn#-{$lZe@aC2l{|84O zW-%c@41FH<)@X%PSvf#q7H*$NRkC#4kvVDWzQb6BA$~v1e5`1~TBkI&3k?<=xpR{Y zB-VJA!133s-oC!MWNne?#ZI4>xrzbLc`YrNs=G=y2_>&T+uq)8`1vOGvgTp&bvM+F z+X-idTy|(X9QkAT@+sE z4P$USgM(%P<(E!FG0;!clb&q(#sAWlbd4rL=esOEx2xFL*qAiVm9kuWcx#*jo0`s@ zO4knvCjlX$xJ#*-r?4ddl^`|NjAX7rqsBoa`q8hSUu=v6AtR?|rFdqMP+IN04kg-x zH887y1u3%-S1G7ez0$ISq|(ZD)P+AZzmN@8#4vC={&woqa)$|GOqRYj_qGjaQ2Mmb zQrcyuNi+ngJn;u;`&KnokghZ7k6oaMlD%wC_%@lG|4#aOhha9Xkt~;?xsDG~E$gNG z_#^C91x|P5HJkQlQSGv&`T3WG{8m64JWWX@ACQ03n0Er*GQN{*2r=5LLu&ZQV zGKlnj()2JOZ7QElm#zRqYSRC*qWk@WF=@@i_|cEH>N@hmg{!usyBpJ4HXVA~%Unw^ zML<0=zpBhKd`eEU1fm=2$qVoPr|1gkEcIcHU-r7D(`S~Ga=nm^mB|N8vgkL>Jl8(S zN>g6URp3ZF=pPt3XCBxn$H!YgNbGOOsX-4N{eebDJ~#+*zEjK?NEyPgrA|pnc~Km4 z=hob}Z*|V!D_@<<>-QGo!p9d}KLB(`Y!@i!(gg#w3z>6FKkh-E!Vqv#l~VMkWA(09 zWYl+-9}->5g|)ZeJltJnQWFvYfz(C7+|kwu)b0432F+#WTIiP=r)bdg=Ma~l40(f8 zxLbGuVivfj%VkFQrqi$4g7K{7BQvC;8wuB&A%B!Nel3LL)m_DZo@y5Eu-o!I((&+4@EV}c3*)y)>L_K`M%{z>l(F0d~s2ikLsR#(z~o?b(F__Zqe;r=G4$Ijzt zfj-PH-(Ngmn5qRm<-P;zhlWuV#-El<~gX zJ3DWqcnzSLu(^&%^_h7g)3}i4zvVD`X@xB9zdYpm;>cY{gFti*H3m zfQa>69_D@H7A}jPmkSYy76j{}2>2P$*kS;(&R#rk!mK>pnU%jlb5ZP)(TK~Xb$hLE zJJL#i)0gk~L#M??F_so8mTRd#)!|fol%W;@(3%WJ;j7p$B$fA?qRbPw#*UE^X_wN!bY7fk~eqaF{ zjh${6kjOiTHLnf7$`ws>e{goFe`v@1uNGjDaKNfnBI_V_`AGT^{^op~g@%Q|mi_N1 zIDjG}T16#7yk4ZGsc+7-K9y3F3W+Y*oAdL0{S$mr(}J8~4u6zBkYLpQ&~yntRTlm&S8rN;XDjg@>mOt}Rf_ z^9V2JwQ@m?&Fjs9Q$uz(o1FJq1g)muS<`P%W?-jRH!Lyg>ktmRl7|5e=W^fm`~o?U z%sQYl9g5$srpf$*>%zE_OT2|~Udx8<-H10l#NlxHi^C>S;JY70j)8mLz_`Am+CSfa zpe^fdk>Zl?hp@6Z@xJlo1pgP|^wOTTO;2RgX`<@U1@~{7@OE($h37(AvfdhTAM+2K zvR~}qE?p8uZdwgkb^KUbBCB=#OseMyx4&PV0lV0?N0eA`P{N9~;J9}$NN7=!j>d*E_~iqb2nJ;l;0%=x@+x+9$v@Z7sm_Fj7LZ4YEE9q!7=1qUUguR)$UwX0!~6Fr(EN`r+e$7w>TtA+#cVSebSJ62ATWYLK|T)I z7m;~UBr*nsS>7E9W_rU^X7I|~iD&`py0tLjfY)!Xdwr*9ecjblAW#P_=fyBsU&54T zv|0`x`4F{Hdy;X8RUc#|{!wuAKvQMtT(r00$egWK6Oe+FdF-oSxA6Wpcy6$u5OH8l zkUCFhubT|B%P-0%Sb!jtmsfzP?-k*FvMKO1i$Nbp5JWmaB?_@N*bM^-Jdo360Osjt z?%0*tX2YJ%2SNFOq@!k+yLCaQ;o_jx$qKQ&&&H|M2d+Zo`A$76%DsTm=qdhzwJy0CWlmq> zi@6sMW1t&%#Iw>49{&+*Twk#DV84-KlBrM`bmaY}Y=*NGu%2(;yve`YUj^)oz)n2? zeOKn+8-hLnp1`8O_lyaHii{z!GYm{jD~~p_za;g0vL^Ap?o4uX(!9!Kp{GaJ-}N+}Rhn$KZ^i~< z{x*u=MxgCMr#c|)!s#R~*8{2`f9?l_@saJXu0CSGoW(q<*n4rH9j)B2mjDCVQWL&l z$+QEiXmO7}D-1bV8)lER1tnPAZ|m%gxj{yO-E+@|fwd*|Wc+-MgJzxWhldab5+3DF zcRUzr&R$`o0CjlTI~p3Ww+8ZRsW>BI$gT4_UST^-(&lwfY=AH9>)?fhul0No`Ed}tfGabt z=DWh|BmTuW@G;{K6W!PLsFDGdom`kSw~iT8l4JJM!5lWP3tQ0jpYOSQGn=;r%#ZfzJ%TdaWa|2<-cS6wlnFMr8Y;pa!Bw6_h;9|PqPL!0=#=?3OXyTKZReJ zKVxWViA?4VV@m6`!CUHbM19A-Oiir}JlM{^TLF=OwL-@~W6zh^qi90Y`5SjZwto>21AQGwVLq47ylaxZ z5x>g(HE{G6q%WCQMfkiF{T#zSg}8> zP{pBlVbaxb&P@m0MGd^mC>_>D_2C~R?ck`zrnxDA8>!%&02-B2t=knqNCH$Id^Bmq zDVNXiPo(q_JB^l7tS+-h0~T0cPcK=ag(>~N@L_ql@T24YsbYg%INOD2Chk(U9k3nN zpNObA;{5NfSMJXQh(7jxol%%toOlD2Wy+EkH8SK}Lguha;F>blL0m(atHqHn@8eR+ zSh0l2pqIb}exn+U?yNv3_o&Xt-|1c5?0e6YOCN1j!IKEWjp$R8F$hZ?%y}YS{Stlz zpjalXs;a8;))LrurbYY4{1iySU#8?A0eR_;@Qs_J zoafK;uCQb8p(1sJ5cLG9E}`US0g#mLglrvv$!GKN4nWUb9G5>hege9Hhx%STcZJ+D zVB{&}Qm!*>S;fr2l2OL;*6q}L{LqqenYsu9+Tl*R?+4~JkMBPzh6tI}M9t67FWL}u zBwWhAB(MVpYqP7!@wSE#_zV^8Gkw)JBMYj13!5!F)IetfBG{F(`Web%)O`}1c(|LR zST!9H-5Fp!)xcg7+QLj(DCC}yU?^NPVj}gu$uGTaED$ExrZg~be_v-8kL_V1p-tjW zhq_#F^Me;zWtx6Y8 z30zaZ!uu+MimA$``#2XTlFN1nL7ju7L?X@bb$xF9?< z{jD`5fWibv6)nGN3DU6nEBr@cA$H1{29Ua>&|(@ccl+sYAM~FV=RM)(GhWM_2(&iZ z#sW3Q<%-FNkUKeYl8?Vq;zl;yF;_LCBMJ12?nDP}RlIp~`3ZhTvbrI)(3fmh3eElU z<;y!+jK^Qifh4G;g3P}`WdvFs@x-`pfnv)zxmalp zWWX7tJAgc?B@6Ee+3J0=HLL0rI0QVbI54Pylz9S3Qya~!khrEvJxPJt1!>@2 zVvB_xf>pbh>$9SIlh(e6YO{PN!nM7Ym#Dssf)PsveUcq3d42S!M4SVqx?9+QTmXv+ z8Jm6(2GCz`KvOPUxNwW^8<4BCGnV7k&X%W_0*ReB>U}Yxxb600-<0n{e9dFp0c|(1 zI)LuMT5v0k{-xghybs(bRLYPTI4_WD%2=&{aiEh29qf7l)8e}`0C$l{%$SII(v#9X z4UacCDis7NU~tFzOsDc_q4&UHMASN9wdWlt8nC=>YP(?|#M}n58n>-3CiM%+t`on2 zz=y>7&{bUid=1xS7|-&KZ%+dIsZ;~-lIrxdZCwT${k6>x^@Puemv|zl)$}fp0G=j8 z-bVza**zq}=1Lfze6JIJ{8bcSn2X|qUdU-;T+tyGWOI8#a`WPKJB?dO2aDCy%_*Wy zB=eFXUC2gd!g?dlJRm;;6k>j5rC25$)2B};ymvLU8Cbs;8D;KB#j}C053%O1tz7`B zE`Q<9Y}$i#0}A-!H&D&&6O~+&SoJ&E`Ec^s{79Mv)0|POTb<20; z{vHOqbzbk1c-BgG7FAril9KbJ#A)7fvc`elW3b2&Oq|mT);REDOkQ;)34Pb6{Avte ztF81BT^lEtl^x>aq?GTd%yrwd{y8cI?FkGaMp#h9N~wM!eEbQ@{1J&iKb! zPM9Ibug5e!UXl7#Hg>L2KkcD8>!MGqQk8XwInFRRl)COI;?uP3v#x%58#hK?qBkBCRUV(Z>fLcPf*6OI9)|H zCcsAsvo@rwhi`A-E3i8cNAd;~P&*R&|9!kuLm#Ys@M-aO*?n%**2)4v&Wj+2fhsO3 zkGafun)%&Zo1m{bh`a`BABhXPBahh{>JPE6gw4~XoLQJ2c;4?-%+djf9Q)XSI1G}O2eianIDb1C0;^yErC|1nK z$^y`Aotl?84n?`e+`DLCYfqI9Du(pl#4x1)4TtoJ0A!=Rh873VN>BqGQL(1BSl5VU#xUnU8o4pDUyj?%tX{^NK8({vDn9-xA>gZSSo}R4 zSJ#U9(PO=$CzJjwW?&$U7#3mGdiLaq>_TLL`|K7S+jsV0KMD)?o6{WnFIvgxW|;9N z=BxKKs&v&GmOYhwU_s}$&EcCB;U(%)3eWEx{OETZT)lT;Ti|eP5FY=oFhMt)xE}VC zUZfp&sMoMN)zo$00fVu?E?+RY-|u99OBA~=j(V=YTDR%h6X?Pbto2TfI$QEg zn>dFEM<%{59^-nWq~N0#$~AuaV}`Q_vh|F`tD5ClWMlKZC-oooZ^9EtLw$0{9tq)b z6_c=8C(wgca}4&2!q|+Ku%_a%`n5?{LrC^@E+OV&#=?*N33|lpAYXgyuQX)?ajjMt1cwp8=ok zyY{iCy}a%uDZ_cd5hPQct*y3E1)3@CGwOGXfRf-;Z1vMIAas*wlD=zTOv7;k$cOvr zbrop$lpL>v`1D7TI>+45Yj{5$jP|Y6Ik@_IQ`>F5{^ua10dkzA%KiH(0wN+y(fqk* znkL(bu>MmzSUyG%Sc-xrkidbQZ72SC;sF}u^aKZ$j-Ct^(c#c{N_mQv3wctES`$Bi zt~&BIt%jUC!P%pXTOQ(O8%;ofLc*iE0W?Rat-EqiqZtJBa5kD&tI_uXWeM5iAS^op zxTP07N7YQ#YU}6mwOty5*p>7WU^~{w&Mks)l;FuY zsWSeZZq2x%hYSTo*VaixV%Vu4w{S+Ag`(Ppe+r{ZPgNfBey@Nwlr#%f zq6?3*8e6Yrv@@iQJLYVdZv(A>!y+xs$Kt3=&J851jK*H0=4ZJ&mP ze)}U_OL~08yJ|ehHJ@vioY&P& z*hz5RJPP<>O8FT%6uBuyK`wapA!G#lS)d1<7m^D^;}G5i5ZZtxeZjj$4O_?d(H0^f z=E{?3Y+!%^8m>xzuOcLIAz-w+LBbaaq&wkKgYEn#T?rXw{HRuUz}0aC{rXZ_k|K{_RSNFup&RAz=SUp@{4;D#?yiC(#S^|KZmm~fBgJEPZtBK+~rEMje> zltfP#B9#)_^%bz>7@)?AFr4|^02mfNo$TJWAOTqe0+9TF{hZKpBqpro~lhuwK+_iy@eBj!!@bulC~Y4 zFKucLO+7mXXv$NKz}2f&Uzty7M1_OJ6ctUYm+&-O6XCa?oo{>&umw4=7VLbiO5!_B zM4qy8X_pqW+tZ8)3C*afdW^HDV>-Z`Ce5Guwvh6h&v=E`NnPU1hhIxrZh;bd`q zE`k?-LW7etQf}4`q|;6v9&nIb^&S;w{va}AN^H9kw$I|G2p=x<(w5NNYrlNat6te# z6=6#L4wqJ8lq`U36oIFQ-pzQzD@A(d@Y-JVS&l!iWE9*_mC;j&)X~9w^X5-2I}p&_ z^(U$a)DK7|a+S766Xr=*`{$PT<#xWA^dau{+h|0Cgx2j8OGQ&K`ue(xU279zq-xZJ zd73;IJCie7$#4pOEmse0fUf7RKMqJ;ej5wAgQGAfItr1Gb`ah9?^56Hfs;FEl-c$g zu0sHiu7E{7wPDclCdiYlUMtO)49z$W3++M!6(j~uOj8-?_mweo0>sTBCr7g#SF=k= z+r|qBC1!LfOaeh_NO~WrnN6z+ zb8()$skk~;fd=6z(4)eYX~o0jGbKX1n*sJW1iIeE&5)A-lhtQ&rXL@Yo?v^F+ml^# ziG#<$&2C=a+BG98HUeaJ3^|8yeqTx&BQWKi(e$#uV!Zo|s0zrQMhe-`8Hf?3#Z#+G z6*gP*T?#l%$KBi(37?HAPX?Wh>(w7rSm)sgZ{oae{>R_Md^Rj{{Z~Hl-LGgyawWE^Tz`1ABD80c{xJ|!CTwQqys?4>Tex>Ic&?>~S#6B~$1Z2R$TLghP9 z;|`EAS=emJAoc#L%IQ{kbeD< zmPtTn8GQWKL%UL0G#LtAVZU7mSa^{Z%uOA+Wu9y(vpfXo13}a-1>UzXGkNsP5PRU1 z`EY`HF`JbdL7>-`4}veZKHfAjAdZORs0K?9h;zjO!&2EfbO3MYoc9G+MiLYyP44HO z3VMLL!yR$0$L?1;2(nv1yktXBfyx`CqVHq=xRl*Yl1@^93;PM)DlxWjCp|veAN+Yu zTU+}8t$e~f%0YtiLw&xbU1|bF2615JUu+YO)jAoMw`^l5vw^V9nSlw07l}>Z z8#7asJ27Y>3vKoF%BI(Z>M7+-3&L%ViX*)NV|$syrmyNgw!GEbuew%|AFTvY#Fh?= zVRD4pYFO&`%OcnDC9LHaLB7_u$3N{dS~M(ffq1*{K}{qz6$RoR?b4F(`+nCYs}V|J zR6M_3vo264l-GUPS+ME5ElQH_wzp8eDr8(_b^5ih(m>63dawpb4Ba6~6PG|!^H=y< zg5cyUKxVigjL*`0`>8#mf0Bck%wt*Lce^6?9A!<>A8nL7O5%H3f_dIO}x5^iI&UC%`T zGY=%>uew@zNiu+5Y+O2SNC(wm^}QV zu;l@K!4Wz6f~ci&B7V~tpx-;^qN!ZF8pJWTq<~A>opB~hepxYQmD?AFG$YTiv zS@M69<^!!Y0VWKdI`4srJ(vNOraPy5U7(ULLm(VDGseip)@~|~wIe9(a}B7M+JnRs za6%7(E9?1d{9#ts5L4ptCsxnetnD4%+SZTRTU)JzVirbT<9_hCO$%xLmJ|kG&$r9 z>LcD@+>NL0HEL-egLGXVe!9LCc~P)X!dJ|alc&t+uO$n`~idN6NtqCh$RZ@sEE-p(8~a^|0~Fft^*i;X2@k; zKr>J(<4PJBaLrnysw$zm==HT=AP<0#1h=GS$sci=HzCZ-B6+gOKy=-z@&KLN1$^o0Y#bQ@{8oa^_ZPfp&S zj<)y2@xU&lnSykqwQtX7Gor%j)MJ1x3;}wZj>5A(v+-K*I-%YQ)wLlc_xoIyQtj*1 z^zFDDwK$iWlE;Rx8@MOu?FH(J{q*Or=6T#cz>B&z@i7&sBg;Hknm_}_ zVXyw9s$xS8S3e~K2WmbS`1g3}{mQBb*%Wgi~nQwK;YB32g8 z@86oV>m+xzi9jz0@&>x>!}eY2() zYzhwUh~kI*7B7Q7KQTQGpf?+Uz5)GNArNc<1m!Bgx&d*%^Kgs=aWDIs zGZ7I8Y<=zE5MgvV>#va?TYS>8h`7z5s~5a_l>!!?9-za%QsZX>hVH(@hhLPI-Un58 z#U&R<$62AD67(f`7DLZd*5CIr|LMYdckr$4N3Qb4#l`V-yMR8+`}bfO*bfc`s1C@F zK|I}@qwjmEAwpk7I{u5h<=kOpWI4=fzZw=EupP}2E~fN|;lg{+9z0NnSK#wayg=SUrL>p!S?UE(=9zAV8kafL4TB}MyqV1tivZ{_`5*Z- z$h>R(2H(+oUtY!qxYyn2IFJl6%)UI7F=GTLx$~KADwvn-20TzF}Vp#%%}zhN1ct@P%9Km8=xs#s;9t_z$OJ8O2)rH$vW?rHa;S3UmDB|Z8M_{ zrXZ^$B9ePJN~4#Z;B!>-G3OHjxtj>Kw&!ZU3Wl%64<$R313ltg(=#VsC}S@R z^iIO~A@r5`nKVcsj*#Q_Cw07ClfLH_Sn)5Mp@s0(p zC9!>NHNDFDOyk;s+pQh5=|+y?>XvJ+y6bNq)&4=-Sv*a|2f#4x6O|j0qvEsRhNDSA zu+U`qf(l#UmN%RM3Z4CvcdxmZ5nT?1YdM+TKmN?Nb)NL*GBd3sx_YtWK2W&4=vC$?CxX3T^^f3LRMt`MCegVz1Xx@^)%(2b^1Zci<=Ri} zw>n6Py1K)(JX0mhFbYJ#A%hp%N?Wpd&m(FHLYzMxO#BSy6=JdnZ%vQ*Fjp2_L~Se% z?ceVS3h#$5|E=x~M>z`+K4|3_d7ch=EyW*3tr+pE zxkuuIF@W{^7EZmL_m2_KpLoJ(5V&=|_q2=po->%QQ(3hSJE%83lE|D$)olvE0lF&C zC})+sd!XhTaea|`a;Epvu;w#)xyco5nSoI0CtyMY!-Kb2CN>3#;IG2C?e9s?Zon|X zq>jc;inb??7x#jv#IPH5c9HU^M=vyGYN27d*hcL+tJl&;kUCX`^xaBNRH7(mMdYsE z#GBY4{3WPKV#}7J(=9~v9#7BeYo5b^QI80^;z?$VxslUO6E(2vFH6-YhL4e9>kf9H3>f1UEp#LFZB4qMVf%7<_aQ!Ft6cXeJ z14Bc%A7DX#yoFwo%{Fq>AiY640&w(J!|q~y`y61z8y1%tD)qnIK)KaV&^%(@1oo5& zZ{kM_=-nKgIzi+LsO3g>TO$%nwzhm#bvl^~6R`5KF#phM<2z2>W}kr>){D)7AN*N$6rq z{QT@xFr9%RqVnTb0^=M6=e!#C{$;&(&t!~Ayr@zgB4weH5}!Hp=iURK;KNnsdD%5n zF0VXPsIkM;=vnHZ(%7Qb4fVG$pHoEWXq(j+jHQ>DAn zs-Fe#-{HAmm*1-O4|o3SJ_kKFV&5D0WUX|7<7D>WBc@;)A$3qN%@Jui*9c=A)_qx> zd;)$QRu6o#nKh8)6z~~}`SQI;&fuetN{vYWcE5XAcj-IuMD%XhYhPFqVFyGo9Xzd&?VVy^NcZ6%kgXc zzK4SJ=@py4qZR0<*w3I&8oYDBzE;$61+WfLm-vv1+x=l@AC1U~=|*nl;#Mfp)e58Q zro=q}hVi9MJhsjrlwf{GkbtxtLI8XCg^xNx@*|5HjMXMJ{{%sRQwvY^G;zGgC@^>VQ z!56#gaIv!v@on*d+l$=m=i3b*J|Mlex1aD=?1=LAFJ%EZ@-})-N5N^lM-xd#-+61w zlQpxm*pK`EiLD&HtmvJS?SQcYW88eQnW@P+FeyAQo*hd_dqu#a$n81}PvlHtH6Po{ zg_48=L z%HO{BseSZFl|+;b^TzXdhSh;4=onk!$>(-7Fi=I79EyN39CVfL5yaO#o;=kbwTmi> zZi_kwV~FUoF6CS6R<)kI)}Bbb%*@m=RjRE=F}H1&i`oTaNb8zD#9EK4I}Fh{=ne-T~0~{?Fn+b#-06U=-km)m!2NwGO(9cfd3)KN7-U_t5jeZPYhQ4@hOhmT(wvL-vLclday zO})OUk_*L2`uXJ;izqJowzNyh=9zW>@v{Q#-LtKrrZ~ENZ`20qSnrYwX37SYJqV-j zpzfG1+fMinmOz7-mH7)H`)qvqJtJS&f@5)M3v}O$1CNF!F|29J`_VN{l;* z6U~-tXBl0*SaR?E?354{$b|TV6fIJHMA4iikp7t>6X(sDClg)($2!SN%wUJtV(HdF zk+R|4#{G&=n{Q{R;cPadr4#DQW`!BzTibMyh}`~mKO4cPEytfd8$EgT8N6JD1K>mS zLXuD&_-Q`wpv(OV+vQyr=ULTicb<;6hmNI7acOz8ce%sCis%JlII|wUBY|LA0c=m* z*n!E(sI-Xfy2H^95e@yu{gPbrb$-rg_6skrSwz0Br)Y?*%FHNh}_$KxnJkJx$>!{pJK^5pf!I*kTrHyhhiJYkA>`r!&J$yB% z?7hwV1UW9K%K53#liynw7ro!zDpQ}*eF?_km#jd-Pui&;YjZZun(s^!O`PbJ-#DAbk^PGfwEbN<(~0j1YQX3w6e-k1 zLwmw-Rx%DFLe;C`=v^bB=pN<3OAg-Y(yW)g*4dTa8d3MpdaRXyXeQII5^){c>q_$1 z3xCdE(ha_`t{IMj+^5O8pXne|1CGFNc2wF(P}_=n11;fwjq&b?`uT6@S4PFf6mr(P z6;c^RJNNS~M>(sUukM>`I`z*6{o<}hIK5Q-B_2){-rl2Hr|#?<9(LHQ>vMB8G3_wD z=@T7?a~l3UG5PLAlW!8XaeBv(#GMG|5K+#2bWWlIUyJHwy3<#x+zH2)02F?#?2fH; zV}mZ$ihH-Z6~?U*U>}Fc{x3)-J!25?Mw`#r9KWHq<^W*w_mPU z$`-8~82h=`h@`-OU8L*7#$Fw=a<>23hDN=0tNn5J%c7Rqrq08qTn@)b;xu;~dQ}Ix zs?F6J#I;1*-3qU)6K&=OlG8*D=3*qc69+TYS$(x@Eh85!BCpW~03SwPwfJ)I3r&jw z@uCrkp{1l{K5aVO$z0Yy=gM%*mr9}BZi*M+JS-Xpt-M57l7w20;cE0-`o725aW=Wm zets2-Q9uUlZWs3s-ZJ%U*O4c79hE4Nu@TFHi{Y4hfobWUr53bhin@}=^Xg-=8rMF-IE_ZY0q16qfEbUJ)` z+B6(Gmr%qJG89p7J>yQ3lY1w--?q({!>vKlGg_PirHokv)wu;b;F3-r>J%&zjS*zMX7wVhF{eO%liP0Yv-5TZ0>mOlj<{RB!wl>;3 zbH|O)hPzrfjkx5z1wG>WIPS3u8y?mxmXZq*I!rBhcvr+dzf|MOATlGz{**)Jfly7V z?A9pEp%~*%sBb1D!7Acj46|q7w0NNVdpIn58%S0nF6zcl+cD)l4e zYMTdia@U!9f$*i_BX3D00Yy+ok1I0Wa`gQySK{>(T^1+6+KcRbziR&LwQ{`)a4gdipt z>`OGlwVz+CIY>VoUb9e!os)x|bGgKM{&4Ji?O&R_te@`>{?$EBf=yIn>$T!29y>-1Uf{1xH8!}c z+A4bjvS8kKFncPZ^SajA7R?D?{AYUcXx1$DHY>n?Hk1(HBYUFxXM%zut5p^Yrhsh5 zcaUh>DkJXW@O6cEkj??-IL7-w<3%IRsJJ$k1~yGL>0;g<@Lxjp<{r57hCcED!2c=f zBU=iiO>pttH@ns9Beq-HkKf`)7FU{vk`NG0>4Dn>th3<#jY;U_@x*^+(+YHQm1mUaCrV*2MX+dF-?~wY%H(j-Tb7DZVI*@N{9jkn z;yuoc(e%M^bsYN5`f(^(?a`ln__SX-y^4n`-fIyyExnqe+0s~`)rf2E^(UyfuD*X}YAN&9+)yOIM8zT)$pqV!0wSue`q|~w z8z|GYg2Gof)aXmB{?0tk#TZt?i+Fox*-~ zUB>b$*Dh#51=D7qT@Y9?3?Xdr!4)P;HT6$a5skw%f~>mf4()TKIjdjo5i$Yd0V(z& zAErDkG;+&g0iwb+DK+}$cwwc#)|o4O@ct&i)yy=IPmZZd ziiZocZmCIDUzIZ>Y#zNb?uy%@8r5o>uO4(J%;=3OG9=sK*s#F_oVl%WDNGWu6_4|OhaM((sk-W0V zxbW&5Eeq=K(uIB<>9yNYQf0Kzx3;mMAUoUUIc3Xbf*MckBwAA>G_DIZmgdPpH-}a%Nmh%by zZQ3J#ZL^GT<^ZT6y6s#YTnp1>IiVQN{`vFKTaV%j&N^TRdnE^pHbxI%nf?}J@`1HJ zH90OqBZ&`ePQ<5aqc|$ONvA5JNY;27EyX_KQ!BguCQO1>zp-0zYGJ|u*qDbD)I+TC z@5)W-o-$UFiNI!P)@#*G`O#!-ZTsf%{ z^}A{QuvX3<+%-KiNnBa%J+T)%=(JpmDTW6q2Oaz@Q%h+d_0IXNB|Guo$lkqM3|6C5 zTPCwY5k($^gB^2u552Nljimbs63UYH+s7wqv)5^rqp0l0zJ^yfwJuyRSYO=eQIQ{m z&tlAeU0$l+ACX@_-buu4wCV2Ms+V7-vVB$Wp31Gl%ssE?RXNjHH@O49KgVXuA^vrv zUtT7A&B&hW7A3Z-1TFP+DZBofHPw3_8vQ<^gTz2}T|7^&Gi?2L-@O}2|K5#)!PWM} zlR0(4pUP0g!+1WPj#2wKqQZ0I0(?UwidI;JZ> z{p#`W?6012URe20lT9*-Sn)%&&Aefr}D#-^Xg1?(H8S>u(`MU=hAOCMP z`PDurCq!TTPw(CMW-8C6;rADQzb2jgFIxWp>4fW+hr5Gu>ggl#eBfbo%Kl{e@vAZa E2c04EN&o-= literal 274539 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�t$H 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ⅇL&`fNZV?C!ijU(MaXVcIAH^eBquLvb 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 b-6sO>3%DCB_v}Q6oV0oVa>T8|H9X~iTPI5V From d7153b96c89357118d39a847ae4d1ff4f984c59c Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Fri, 25 Oct 2024 21:45:43 +0100 Subject: [PATCH 28/57] Revert old guessers and tables removal (#4752) * Add back MDAnalysis.topology's guessers and tables modules * Adds deprecation warnings for these two modules --------- Co-authored-by: Oliver Beckstein --- package/CHANGELOG | 4 + package/MDAnalysis/topology/guessers.py | 542 ++++++++++++++++++ package/MDAnalysis/topology/tables.py | 52 ++ .../documentation_pages/topology/guessers.rst | 2 + .../documentation_pages/topology/tables.rst | 1 + .../documentation_pages/topology_modules.rst | 2 + .../MDAnalysisTests/topology/test_guessers.py | 212 +++++++ .../MDAnalysisTests/topology/test_tables.py | 35 ++ 8 files changed, 850 insertions(+) create mode 100644 package/MDAnalysis/topology/guessers.py create mode 100644 package/MDAnalysis/topology/tables.py create mode 100644 package/doc/sphinx/source/documentation_pages/topology/guessers.rst create mode 100644 package/doc/sphinx/source/documentation_pages/topology/tables.rst create mode 100644 testsuite/MDAnalysisTests/topology/test_guessers.py create mode 100644 testsuite/MDAnalysisTests/topology/test_tables.py diff --git a/package/CHANGELOG b/package/CHANGELOG index 4b1c091df69..d9a73a464f0 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -114,6 +114,10 @@ Changes numpy.testing.assert_allclose #4438) Deprecations + * MDAnalysis.topology.guessers is deprecated in favour of the new + Guessers API and will be removed in version 3.0 (PR #4752) + * MDAnalysis.topology.tables is deprecated in favour of + MDAnalysis.guesser.tables and will be removed in version 3.0 (PR #4752) * Element guessing in the ITPParser is deprecated and will be removed in version 3.0 (Issue #4698) * Unknown masses are set to 0.0 for current version, this will be depracated diff --git a/package/MDAnalysis/topology/guessers.py b/package/MDAnalysis/topology/guessers.py new file mode 100644 index 00000000000..7d81f239617 --- /dev/null +++ b/package/MDAnalysis/topology/guessers.py @@ -0,0 +1,542 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# 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 +# +""" +Guessing unknown Topology information --- :mod:`MDAnalysis.topology.guessers` +============================================================================= + +.. deprecated:: 2.8.0 + The :mod:`MDAnalysis.topology.guessers` module will be removed in release 3.0.0. + It is deprecated in favor of the new Guessers API. See + :mod:`MDAnalysis.guesser.default_guesser` for more details. + +In general `guess_atom_X` returns the guessed value for a single value, +while `guess_Xs` will work on an array of many atoms. + + +Example uses of guessers +------------------------ + +Guessing elements from atom names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Currently, it is possible to guess elements from atom names using +:func:`guess_atom_element` (or the synonymous :func:`guess_atom_type`). This can +be done in the following manner:: + + import MDAnalysis as mda + from MDAnalysis.topology.guessers import guess_atom_element + from MDAnalysisTests.datafiles import PRM7 + + u = mda.Universe(PRM7) + + print(u.atoms.names[1]) # returns the atom name H1 + + element = guess_atom_element(u.atoms.names[1]) + + print(element) # returns element H + +In the above example, we take an atom named H1 and use +:func:`guess_atom_element` to guess the element hydrogen (i.e. H). It is +important to note that element guessing is not always accurate. Indeed in cases +where the atom type is not recognised, we may end up with the wrong element. +For example:: + + import MDAnalysis as mda + from MDAnalysis.topology.guessers import guess_atom_element + from MDAnalysisTests.datafiles import PRM19SBOPC + + u = mda.Universe(PRM19SBOPC) + + print(u.atoms.names[-1]) # returns the atom name EPW + + element = guess_atom_element(u.atoms.names[-1]) + + print(element) # returns element P + +Here we find that virtual site atom 'EPW' was given the element P, which +would not be an expected result. We therefore always recommend that users +carefully check the outcomes of any guessers. + +In some cases, one may want to guess elements for an entire universe and add +this guess as a topology attribute. This can be done using :func:`guess_types` +in the following manner:: + + import MDAnalysis as mda + from MDAnalysis.topology.guessers import guess_types + from MDAnalysisTests.datafiles import PRM7 + + u = mda.Universe(PRM7) + + guessed_elements = guess_types(u.atoms.names) + + u.add_TopologyAttr('elements', guessed_elements) + + print(u.atoms.elements) # returns an array of guessed elements + +More information on adding topology attributes can found in the `user guide`_. + + +.. Links + +.. _user guide: https://www.mdanalysis.org/UserGuide/examples/constructing_universe.html#Adding-topology-attributes + +""" +import numpy as np +import warnings +import re + +from ..lib import distances +from MDAnalysis.guesser import tables + + +wmsg = ( + "Deprecated in version 2.8.0\n" + "MDAnalysis.topology.guessers is deprecated in favour of " + "the new Guessers API and will be removed in MDAnalysis version 3.0.0. " + "See MDAnalysis.guesser.default_guesser for more details." +) + + +warnings.warn(wmsg, category=DeprecationWarning) + + +def guess_masses(atom_types): + """Guess the mass of many atoms based upon their type + + Parameters + ---------- + atom_types + Type of each atom + + Returns + ------- + atom_masses : np.ndarray dtype float64 + """ + validate_atom_types(atom_types) + masses = np.array([get_atom_mass(atom_t) for atom_t in atom_types], dtype=np.float64) + return masses + + +def validate_atom_types(atom_types): + """Vaildates the atom types based on whether they are available in our tables + + Parameters + ---------- + atom_types + Type of each atom + + Returns + ------- + None + + .. versionchanged:: 0.20.0 + Try uppercase atom type name as well + """ + for atom_type in np.unique(atom_types): + try: + tables.masses[atom_type] + except KeyError: + try: + tables.masses[atom_type.upper()] + except KeyError: + warnings.warn("Failed to guess the mass for the following atom types: {}".format(atom_type)) + + +def guess_types(atom_names): + """Guess the atom type of many atoms based on atom name + + Parameters + ---------- + atom_names + Name of each atom + + Returns + ------- + atom_types : np.ndarray dtype object + """ + return np.array([guess_atom_element(name) for name in atom_names], dtype=object) + + +def guess_atom_type(atomname): + """Guess atom type from the name. + + At the moment, this function simply returns the element, as + guessed by :func:`guess_atom_element`. + + + See Also + -------- + :func:`guess_atom_element` + :mod:`MDAnalysis.topology.tables` + + + """ + return guess_atom_element(atomname) + + +NUMBERS = re.compile(r'[0-9]') # match numbers +SYMBOLS = re.compile(r'[*+-]') # match *, +, - + +def guess_atom_element(atomname): + """Guess the element of the atom from the name. + + Looks in dict to see if element is found, otherwise it uses the first + character in the atomname. The table comes from CHARMM and AMBER atom + types, where the first character is not sufficient to determine the atom + type. Some GROMOS ions have also been added. + + .. Warning: The translation table is incomplete. This will probably result + in some mistakes, but it still better than nothing! + + See Also + -------- + :func:`guess_atom_type` + :mod:`MDAnalysis.topology.tables` + """ + if atomname == '': + return '' + try: + return tables.atomelements[atomname.upper()] + except KeyError: + # strip symbols + no_symbols = re.sub(SYMBOLS, '', atomname) + + # split name by numbers + no_numbers = re.split(NUMBERS, no_symbols) + no_numbers = list(filter(None, no_numbers)) #remove '' + # if no_numbers is not empty, use the first element of no_numbers + name = no_numbers[0].upper() if no_numbers else '' + + # just in case + if name in tables.atomelements: + return tables.atomelements[name] + + while name: + if name in tables.elements: + return name + if name[:-1] in tables.elements: + return name[:-1] + if name[1:] in tables.elements: + return name[1:] + if len(name) <= 2: + return name[0] + name = name[:-1] # probably element is on left not right + + # if it's numbers + return no_symbols + + +def guess_bonds(atoms, coords, box=None, **kwargs): + r"""Guess if bonds exist between two atoms based on their distance. + + Bond between two atoms is created, if the two atoms are within + + .. math:: + + d < f \cdot (R_1 + R_2) + + of each other, where :math:`R_1` and :math:`R_2` are the VdW radii + of the atoms and :math:`f` is an ad-hoc *fudge_factor*. This is + the `same algorithm that VMD uses`_. + + Parameters + ---------- + atoms : AtomGroup + atoms for which bonds should be guessed + coords : array + coordinates of the atoms (i.e., `AtomGroup.positions)`) + fudge_factor : float, optional + The factor by which atoms must overlap eachother to be considered a + bond. Larger values will increase the number of bonds found. [0.55] + vdwradii : dict, optional + To supply custom vdwradii for atoms in the algorithm. Must be a dict + of format {type:radii}. The default table of van der Waals radii is + hard-coded as :data:`MDAnalysis.topology.tables.vdwradii`. Any user + defined vdwradii passed as an argument will supercede the table + values. [``None``] + lower_bound : float, optional + The minimum bond length. All bonds found shorter than this length will + be ignored. This is useful for parsing PDB with altloc records where + atoms with altloc A and B maybe very close together and there should be + no chemical bond between them. [0.1] + box : array_like, optional + Bonds are found using a distance search, if unit cell information is + given, periodic boundary conditions will be considered in the distance + search. [``None``] + + Returns + ------- + list + List of tuples suitable for use in Universe topology building. + + Warnings + -------- + No check is done after the bonds are guessed to see if Lewis + structure is correct. This is wrong and will burn somebody. + + Raises + ------ + :exc:`ValueError` if inputs are malformed or `vdwradii` data is missing. + + + .. _`same algorithm that VMD uses`: + http://www.ks.uiuc.edu/Research/vmd/vmd-1.9.1/ug/node26.html + + .. versionadded:: 0.7.7 + .. versionchanged:: 0.9.0 + Updated method internally to use more :mod:`numpy`, should work + faster. Should also use less memory, previously scaled as + :math:`O(n^2)`. *vdwradii* argument now augments table list + rather than replacing entirely. + """ + # why not just use atom.positions? + if len(atoms) != len(coords): + raise ValueError("'atoms' and 'coord' must be the same length") + + fudge_factor = kwargs.get('fudge_factor', 0.55) + + vdwradii = tables.vdwradii.copy() # so I don't permanently change it + user_vdwradii = kwargs.get('vdwradii', None) + if user_vdwradii: # this should make algo use their values over defaults + vdwradii.update(user_vdwradii) + + # Try using types, then elements + atomtypes = atoms.types + + # check that all types have a defined vdw + if not all(val in vdwradii for val in set(atomtypes)): + raise ValueError(("vdw radii for types: " + + ", ".join([t for t in set(atomtypes) if + not t in vdwradii]) + + ". These can be defined manually using the" + + " keyword 'vdwradii'")) + + lower_bound = kwargs.get('lower_bound', 0.1) + + if box is not None: + box = np.asarray(box) + + # to speed up checking, calculate what the largest possible bond + # atom that would warrant attention. + # then use this to quickly mask distance results later + max_vdw = max([vdwradii[t] for t in atomtypes]) + + bonds = [] + + pairs, dist = distances.self_capped_distance(coords, + max_cutoff=2.0*max_vdw, + min_cutoff=lower_bound, + box=box) + for idx, (i, j) in enumerate(pairs): + d = (vdwradii[atomtypes[i]] + vdwradii[atomtypes[j]])*fudge_factor + if (dist[idx] < d): + bonds.append((atoms[i].index, atoms[j].index)) + return tuple(bonds) + + +def guess_angles(bonds): + """Given a list of Bonds, find all angles that exist between atoms. + + Works by assuming that if atoms 1 & 2 are bonded, and 2 & 3 are bonded, + then (1,2,3) must be an angle. + + Returns + ------- + list of tuples + List of tuples defining the angles. + Suitable for use in u._topology + + + See Also + -------- + :meth:`guess_bonds` + + + .. versionadded 0.9.0 + """ + angles_found = set() + + for b in bonds: + for atom in b: + other_a = b.partner(atom) # who's my friend currently in Bond + for other_b in atom.bonds: + if other_b != b: # if not the same bond I start as + third_a = other_b.partner(atom) + desc = tuple([other_a.index, atom.index, third_a.index]) + if desc[0] > desc[-1]: # first index always less than last + desc = desc[::-1] + angles_found.add(desc) + + return tuple(angles_found) + + +def guess_dihedrals(angles): + """Given a list of Angles, find all dihedrals that exist between atoms. + + Works by assuming that if (1,2,3) is an angle, and 3 & 4 are bonded, + then (1,2,3,4) must be a dihedral. + + Returns + ------- + list of tuples + List of tuples defining the dihedrals. + Suitable for use in u._topology + + .. versionadded 0.9.0 + """ + dihedrals_found = set() + + for b in angles: + a_tup = tuple([a.index for a in b]) # angle as tuple of numbers + # if searching with b[0], want tuple of (b[2], b[1], b[0], +new) + # search the first and last atom of each angle + for atom, prefix in zip([b.atoms[0], b.atoms[-1]], + [a_tup[::-1], a_tup]): + for other_b in atom.bonds: + if not other_b.partner(atom) in b: + third_a = other_b.partner(atom) + desc = prefix + (third_a.index,) + if desc[0] > desc[-1]: + desc = desc[::-1] + dihedrals_found.add(desc) + + return tuple(dihedrals_found) + + +def guess_improper_dihedrals(angles): + """Given a list of Angles, find all improper dihedrals that exist between + atoms. + + Works by assuming that if (1,2,3) is an angle, and 2 & 4 are bonded, + then (2, 1, 3, 4) must be an improper dihedral. + ie the improper dihedral is the angle between the planes formed by + (1, 2, 3) and (1, 3, 4) + + Returns + ------- + List of tuples defining the improper dihedrals. + Suitable for use in u._topology + + .. versionadded 0.9.0 + """ + dihedrals_found = set() + + for b in angles: + atom = b[1] # select middle atom in angle + # start of improper tuple + a_tup = tuple([b[a].index for a in [1, 2, 0]]) + # if searching with b[1], want tuple of (b[1], b[2], b[0], +new) + # search the first and last atom of each angle + for other_b in atom.bonds: + other_atom = other_b.partner(atom) + # if this atom isn't in the angle I started with + if not other_atom in b: + desc = a_tup + (other_atom.index,) + if desc[0] > desc[-1]: + desc = desc[::-1] + dihedrals_found.add(desc) + + return tuple(dihedrals_found) + + +def get_atom_mass(element): + """Return the atomic mass in u for *element*. + + Masses are looked up in :data:`MDAnalysis.topology.tables.masses`. + + .. Warning:: Unknown masses are set to 0.0 + + .. versionchanged:: 0.20.0 + Try uppercase atom type name as well + """ + try: + return tables.masses[element] + except KeyError: + try: + return tables.masses[element.upper()] + except KeyError: + return 0.0 + + +def guess_atom_mass(atomname): + """Guess a mass based on the atom name. + + :func:`guess_atom_element` is used to determine the kind of atom. + + .. warning:: Anything not recognized is simply set to 0; if you rely on the + masses you might want to double check. + """ + return get_atom_mass(guess_atom_element(atomname)) + + +def guess_atom_charge(atomname): + """Guess atom charge from the name. + + .. Warning:: Not implemented; simply returns 0. + """ + # TODO: do something slightly smarter, at least use name/element + return 0.0 + + +def guess_aromaticities(atomgroup): + """Guess aromaticity of atoms using RDKit + + Parameters + ---------- + atomgroup : mda.core.groups.AtomGroup + Atoms for which the aromaticity will be guessed + + Returns + ------- + aromaticities : numpy.ndarray + Array of boolean values for the aromaticity of each atom + + + .. versionadded:: 2.0.0 + """ + mol = atomgroup.convert_to("RDKIT") + return np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) + + +def guess_gasteiger_charges(atomgroup): + """Guess Gasteiger partial charges using RDKit + + Parameters + ---------- + atomgroup : mda.core.groups.AtomGroup + Atoms for which the charges will be guessed + + Returns + ------- + charges : numpy.ndarray + Array of float values representing the charge of each atom + + + .. versionadded:: 2.0.0 + """ + mol = atomgroup.convert_to("RDKIT") + from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges + ComputeGasteigerCharges(mol, throwOnParamFailure=True) + return np.array([atom.GetDoubleProp("_GasteigerCharge") + for atom in mol.GetAtoms()], + dtype=np.float32) diff --git a/package/MDAnalysis/topology/tables.py b/package/MDAnalysis/topology/tables.py new file mode 100644 index 00000000000..6f88368ace7 --- /dev/null +++ b/package/MDAnalysis/topology/tables.py @@ -0,0 +1,52 @@ +""" +MDAnalysis topology tables +========================== + +.. deprecated:: 2.8.0 + The :mod:`MDAnalysis.topology.tables` module has been moved to + :mod:`MDAnalysis.guesser.tables`. This import point will + be removed in release 3.0.0. + +The module contains static lookup tables for atom typing etc. The +tables are dictionaries that are indexed by the element. + +.. autodata:: atomelements +.. autodata:: masses +.. autodata:: vdwradii + +The original raw data are stored as multi-line strings that are +translated into dictionaries with :func:`kv2dict`. In the future, +these tables might be moved into external data files; see +:func:`kv2dict` for explanation of the file format. + +.. autofunction:: kv2dict + +The raw tables are stored in the strings + +.. autodata:: TABLE_ATOMELEMENTS +.. autodata:: TABLE_MASSES +.. autodata:: TABLE_VDWRADII +""" + +import warnings +from MDAnalysis.guesser.tables import ( + kv2dict, + TABLE_ATOMELEMENTS, + atomelements, + elements, + TABLE_MASSES, + masses, + TABLE_VDWRADII, + vdwradii, + Z2SYMB, + SYMB2Z, + SYBYL2SYMB, +) + +wmsg = ( + "Deprecated in version 2.8.0\n" + "MDAnalysis.topology.tables has been moved to " + "MDAnalysis.guesser.tables. This import point " + "will be removed in MDAnalysis version 3.0.0" +) +warnings.warn(wmsg, category=DeprecationWarning) diff --git a/package/doc/sphinx/source/documentation_pages/topology/guessers.rst b/package/doc/sphinx/source/documentation_pages/topology/guessers.rst new file mode 100644 index 00000000000..e6449f5ddc8 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/topology/guessers.rst @@ -0,0 +1,2 @@ +.. automodule:: MDAnalysis.topology.guessers + :members: diff --git a/package/doc/sphinx/source/documentation_pages/topology/tables.rst b/package/doc/sphinx/source/documentation_pages/topology/tables.rst new file mode 100644 index 00000000000..f4d579ec9c8 --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/topology/tables.rst @@ -0,0 +1 @@ +.. automodule:: MDAnalysis.topology.tables diff --git a/package/doc/sphinx/source/documentation_pages/topology_modules.rst b/package/doc/sphinx/source/documentation_pages/topology_modules.rst index 01f3ab32e27..e1f818adabf 100644 --- a/package/doc/sphinx/source/documentation_pages/topology_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/topology_modules.rst @@ -60,3 +60,5 @@ the topology readers. topology/base topology/core topology/tpr_util + topology/guessers + topology/tables diff --git a/testsuite/MDAnalysisTests/topology/test_guessers.py b/testsuite/MDAnalysisTests/topology/test_guessers.py new file mode 100644 index 00000000000..7ab62b56eed --- /dev/null +++ b/testsuite/MDAnalysisTests/topology/test_guessers.py @@ -0,0 +1,212 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# 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 +# +from importlib import reload +import pytest +from numpy.testing import assert_equal +import numpy as np + +import MDAnalysis as mda +from MDAnalysis.topology import guessers +from MDAnalysis.core.topologyattrs import Angles + +from MDAnalysisTests import make_Universe +from MDAnalysisTests.core.test_fragments import make_starshape +import MDAnalysis.tests.datafiles as datafiles + +from MDAnalysisTests.util import import_not_available + + +try: + from rdkit import Chem + from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges +except ImportError: + pass + +requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), + reason="requires RDKit") + + +def test_moved_to_guessers_warning(): + wmsg = "deprecated in favour of the new Guessers API" + with pytest.warns(DeprecationWarning, match=wmsg): + reload(guessers) + + +class TestGuessMasses(object): + def test_guess_masses(self): + out = guessers.guess_masses(['C', 'C', 'H']) + + assert isinstance(out, np.ndarray) + assert_equal(out, np.array([12.011, 12.011, 1.008])) + + def test_guess_masses_warn(self): + with pytest.warns(UserWarning): + guessers.guess_masses(['X']) + + def test_guess_masses_miss(self): + out = guessers.guess_masses(['X', 'Z']) + assert_equal(out, np.array([0.0, 0.0])) + + @pytest.mark.parametrize('element, value', (('H', 1.008), ('XYZ', 0.0), )) + def test_get_atom_mass(self, element, value): + assert guessers.get_atom_mass(element) == value + + def test_guess_atom_mass(self): + assert guessers.guess_atom_mass('1H') == 1.008 + + +class TestGuessTypes(object): + # guess_types + # guess_atom_type + # guess_atom_element + def test_guess_types(self): + out = guessers.guess_types(['MG2+', 'C12']) + + assert isinstance(out, np.ndarray) + assert_equal(out, np.array(['MG', 'C'], dtype=object)) + + def test_guess_atom_element(self): + assert guessers.guess_atom_element('MG2+') == 'MG' + + def test_guess_atom_element_empty(self): + assert guessers.guess_atom_element('') == '' + + def test_guess_atom_element_singledigit(self): + assert guessers.guess_atom_element('1') == '1' + + def test_guess_atom_element_1H(self): + assert guessers.guess_atom_element('1H') == 'H' + assert guessers.guess_atom_element('2H') == 'H' + + @pytest.mark.parametrize('name, element', ( + ('AO5*', 'O'), + ('F-', 'F'), + ('HB1', 'H'), + ('OC2', 'O'), + ('1he2', 'H'), + ('3hg2', 'H'), + ('OH-', 'O'), + ('HO', 'H'), + ('he', 'H'), + ('zn', 'ZN'), + ('Ca2+', 'CA'), + ('CA', 'C'), + ('N0A', 'N'), + ('C0U', 'C'), + ('C0S', 'C'), + ('Na+', 'NA'), + ('Cu2+', 'CU') + )) + def test_guess_element_from_name(self, name, element): + assert guessers.guess_atom_element(name) == element + + +def test_guess_charge(): + # this always returns 0.0 + assert guessers.guess_atom_charge('this') == 0.0 + + +def test_guess_bonds_Error(): + u = make_Universe(trajectory=True) + with pytest.raises(ValueError): + guessers.guess_bonds(u.atoms[:4], u.atoms.positions[:5]) + + +def test_guess_impropers(): + u = make_starshape() + + ag = u.atoms[:5] + + u.add_TopologyAttr(Angles(guessers.guess_angles(ag.bonds))) + + vals = guessers.guess_improper_dihedrals(ag.angles) + assert_equal(len(vals), 12) + + +def bond_sort(arr): + # sort from low to high, also within a tuple + # e.g. ([5, 4], [0, 1], [0, 3]) -> ([0, 1], [0, 3], [4, 5]) + out = [] + for (i, j) in arr: + if i > j: + i, j = j, i + out.append((i, j)) + return sorted(out) + +def test_guess_bonds_water(): + u = mda.Universe(datafiles.two_water_gro) + bonds = bond_sort(guessers.guess_bonds(u.atoms, u.atoms.positions, u.dimensions)) + assert_equal(bonds, ((0, 1), + (0, 2), + (3, 4), + (3, 5))) + +def test_guess_bonds_adk(): + u = mda.Universe(datafiles.PSF, datafiles.DCD) + u.atoms.types = guessers.guess_types(u.atoms.names) + bonds = bond_sort(guessers.guess_bonds(u.atoms, u.atoms.positions)) + assert_equal(np.sort(u.bonds.indices, axis=0), + np.sort(bonds, axis=0)) + +def test_guess_bonds_peptide(): + u = mda.Universe(datafiles.PSF_NAMD, datafiles.PDB_NAMD) + u.atoms.types = guessers.guess_types(u.atoms.names) + bonds = bond_sort(guessers.guess_bonds(u.atoms, u.atoms.positions)) + assert_equal(np.sort(u.bonds.indices, axis=0), + np.sort(bonds, axis=0)) + + +@pytest.mark.parametrize("smi", [ + "c1ccccc1", + "C1=CC=CC=C1", + "CCO", + "c1ccccc1Cc1ccccc1", + "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", +]) +@requires_rdkit +def test_guess_aromaticities(smi): + mol = Chem.MolFromSmiles(smi) + mol = Chem.AddHs(mol) + expected = np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) + u = mda.Universe(mol) + values = guessers.guess_aromaticities(u.atoms) + assert_equal(values, expected) + + +@pytest.mark.parametrize("smi", [ + "c1ccccc1", + "C1=CC=CC=C1", + "CCO", + "c1ccccc1Cc1ccccc1", + "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", +]) +@requires_rdkit +def test_guess_gasteiger_charges(smi): + mol = Chem.MolFromSmiles(smi) + mol = Chem.AddHs(mol) + ComputeGasteigerCharges(mol, throwOnParamFailure=True) + expected = np.array([atom.GetDoubleProp("_GasteigerCharge") + for atom in mol.GetAtoms()], dtype=np.float32) + u = mda.Universe(mol) + values = guessers.guess_gasteiger_charges(u.atoms) + assert_equal(values, expected) diff --git a/testsuite/MDAnalysisTests/topology/test_tables.py b/testsuite/MDAnalysisTests/topology/test_tables.py new file mode 100644 index 00000000000..37246ad1864 --- /dev/null +++ b/testsuite/MDAnalysisTests/topology/test_tables.py @@ -0,0 +1,35 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2024 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the Lesser GNU Public Licence, v2.1 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 +# +import pytest + +from importlib import reload +import pytest + +from MDAnalysis.topology import tables + + +def test_moved_to_guessers_warning(): + wmsg = "has been moved to MDAnalysis.guesser.tables" + with pytest.warns(DeprecationWarning, match=wmsg): + reload(tables) + From 52ff77bd50bcb091edd407bb4bc64c2ce6fa0b9c Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Sat, 26 Oct 2024 22:46:22 +1100 Subject: [PATCH 29/57] Deprecate guess_bonds and bond guessing kwargs in Universe (#4757) * deprecate bond guessing kwargs in Universe --------- Co-authored-by: Oliver Beckstein --- package/CHANGELOG | 8 ++++ package/MDAnalysis/core/universe.py | 47 +++++++++++++++++++ .../MDAnalysisTests/guesser/test_base.py | 8 ++++ 3 files changed, 63 insertions(+) diff --git a/package/CHANGELOG b/package/CHANGELOG index d9a73a464f0..eaa76f46a96 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -114,6 +114,14 @@ Changes numpy.testing.assert_allclose #4438) Deprecations + * The `guess_bonds`, `vdwradii`, `fudge_factor`, and `lower_bound` kwargs + are deprecated for bond guessing during Universe creation. Instead, + pass `("bonds", "angles", "dihedrals")` into `to_guess` or `force_guess` + during Universe creation, and the associated `vdwradii`, `fudge_factor`, + and `lower_bound` kwargs into `Guesser` creation. Alternatively, if + `vdwradii`, `fudge_factor`, and `lower_bound` are passed into + `Universe.guess_TopologyAttrs`, they will override the previous values + of those kwargs. (Issue #4756, PR #4757) * MDAnalysis.topology.guessers is deprecated in favour of the new Guessers API and will be removed in version 3.0 (PR #4752) * MDAnalysis.topology.tables is deprecated in favour of diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index dcc8c634aab..09e323f5b41 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -235,9 +235,24 @@ class Universe(object): Once Universe has been loaded, attempt to guess the connectivity between atoms. This will populate the .bonds, .angles, and .dihedrals attributes of the Universe. + + .. deprecated:: 2.8.0 + This keyword is deprecated and will be removed in MDAnalysis 3.0. + Please pass ("bonds", "angles", "dihedrals") into + `to_guess` or `force_guess` instead to guess bonds, angles, + and dihedrals respectively. + vdwradii: dict, ``None``, default ``None`` For use with *guess_bonds*. Supply a dict giving a vdwradii for each atom type which are used in guessing bonds. + + .. deprecated:: 2.8.0 + This keyword is deprecated and will be removed in MDAnalysis 3.0. + Please pass it into Guesser creation (:mod:`~MDAnalysis.guesser`), + or to :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` + method instead. If passed into `guess_TopologyAttrs`, it will + override the values set during Guesser creation. + context: str or :mod:`Guesser`, default ``'default'`` Type of the Guesser to be used in guessing TopologyAttrs to_guess: list[str] (optional, default ``['types', 'masses']``) @@ -260,8 +275,25 @@ class Universe(object): fudge_factor: float, default [0.55] For use with *guess_bonds*. Supply the factor by which atoms must overlap each other to be considered a bond. + + .. deprecated:: 2.8.0 + This keyword is deprecated and will be removed in MDAnalysis 3.0. + Please pass it into Guesser creation (:mod:`~MDAnalysis.guesser`), + or to :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` + method instead. If passed into `guess_TopologyAttrs`, it will + override the values set during Guesser creation. + lower_bound: float, default [0.1] For use with *guess_bonds*. Supply the minimum bond length. + + .. deprecated:: 2.8.0 + This keyword is deprecated and will be removed in MDAnalysis 3.0. + Please pass it into Guesser creation (:mod:`~MDAnalysis.guesser`), + or to :meth:`~MDAnalysis.core.universe.Universe.guess_TopologyAttrs` + method instead. If passed into `guess_TopologyAttrs`, it will + override the values set during Guesser creation. + + transformations: function or list, ``None``, default ``None`` Provide a list of transformations that you wish to apply to the trajectory upon reading. Transformations can be found in @@ -407,6 +439,21 @@ def __init__(self, topology=None, *coordinates, all_coordinates=False, self._trajectory.add_transformations(*transformations) if guess_bonds: + warnings.warn( + "Bond guessing through the `guess_bonds` keyword is deprecated" + " and will be removed in MDAnalysis 3.0. " + "Instead, pass 'bonds', 'angles', and 'dihedrals' to " + "the `to_guess` keyword in Universe for guessing these " + "if they are not present, or `force_guess` if they are " + "and you wish to replace these bonds with guessed values. " + "The kwargs `fudge_factor`, `vdwradii`, and `lower_bound` " + "are also deprecated and will be removed in MDAnalysis 3.0, " + "where they should be passed into the Context for guessing on " + "Universe instantiation. If using guess_TopologyAttrs, " + "pass these kwargs to the method instead, as they will override " + "the previous Context values.", + DeprecationWarning + ) force_guess = list(force_guess) + ['bonds', 'angles', 'dihedrals'] self.guess_TopologyAttrs( diff --git a/testsuite/MDAnalysisTests/guesser/test_base.py b/testsuite/MDAnalysisTests/guesser/test_base.py index 4cca0de24da..fe645a7c3ca 100644 --- a/testsuite/MDAnalysisTests/guesser/test_base.py +++ b/testsuite/MDAnalysisTests/guesser/test_base.py @@ -102,6 +102,14 @@ def test_partial_guess_attr_with_unknown_no_value_label(self): assert_equal(u.atoms.types, ['', '', '', '']) +def test_Universe_guess_bonds_deprecated(): + with pytest.warns( + DeprecationWarning, + match='`guess_bonds` keyword is deprecated' + ): + u = mda.Universe(datafiles.PDB_full, guess_bonds=True) + + @pytest.mark.parametrize( "universe_input", [datafiles.DCD, datafiles.XTC, np.random.rand(3, 3), datafiles.PDB] From 961cbd5df42571d17aac365fa11894f34f0879a9 Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Sat, 26 Oct 2024 22:47:32 +1100 Subject: [PATCH 30/57] Switch guessers to catching and raising NoDataErrors (#4755) * Switch guessers to catching and raising NoDataErrors --------- Co-authored-by: Yuxuan Zhuang --- package/MDAnalysis/guesser/default_guesser.py | 17 +++++++++-------- .../guesser/test_default_guesser.py | 7 ++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/package/MDAnalysis/guesser/default_guesser.py b/package/MDAnalysis/guesser/default_guesser.py index 87da87e12cf..ee4ede1d7d0 100644 --- a/package/MDAnalysis/guesser/default_guesser.py +++ b/package/MDAnalysis/guesser/default_guesser.py @@ -109,6 +109,7 @@ import re +from ..exceptions import NoDataError from ..lib import distances from . import tables @@ -218,18 +219,18 @@ def guess_masses(self, atom_types=None, indices_to_guess=None): if atom_types is None: try: atom_types = self._universe.atoms.elements - except AttributeError: + except NoDataError: try: atom_types = self._universe.atoms.types - except AttributeError: + except NoDataError: try: atom_types = self.guess_types( atom_types=self._universe.atoms.names) - except ValueError: - raise ValueError( + except NoDataError: + raise NoDataError( "there is no reference attributes" " (elements, types, or names)" - " in this universe to guess mass from") + " in this universe to guess mass from") from None if indices_to_guess is not None: atom_types = atom_types[indices_to_guess] @@ -291,10 +292,10 @@ def guess_types(self, atom_types=None, indices_to_guess=None): if atom_types is None: try: atom_types = self._universe.atoms.names - except AttributeError: - raise ValueError( + except NoDataError: + raise NoDataError( "there is no reference attributes in this universe " - "to guess types from") + "to guess types from") from None if indices_to_guess is not None: atom_types = atom_types[indices_to_guess] diff --git a/testsuite/MDAnalysisTests/guesser/test_default_guesser.py b/testsuite/MDAnalysisTests/guesser/test_default_guesser.py index 8eb55b69529..fe8e012c8c4 100644 --- a/testsuite/MDAnalysisTests/guesser/test_default_guesser.py +++ b/testsuite/MDAnalysisTests/guesser/test_default_guesser.py @@ -26,7 +26,8 @@ from numpy.testing import assert_equal, assert_allclose import numpy as np -from MDAnalysis.core.topologyattrs import Angles, Atomtypes, Atomnames, Masses +from MDAnalysis.core.topologyattrs import Angles, Atomtypes, Atomnames +from MDAnalysis.exceptions import NoDataError from MDAnalysis.guesser.default_guesser import DefaultGuesser from MDAnalysis.core.topology import Topology from MDAnalysisTests import make_Universe @@ -82,7 +83,7 @@ def test_guess_atom_mass(self, default_guesser): def test_guess_masses_with_no_reference_elements(self): u = mda.Universe.empty(3) - with pytest.raises(ValueError, + with pytest.raises(NoDataError, match=('there is no reference attributes ')): u.guess_TopologyAttrs('default', ['masses']) @@ -150,7 +151,7 @@ def test_guess_charge(default_guesser): def test_guess_bonds_Error(): u = make_Universe(trajectory=True) msg = "This Universe does not contain name information" - with pytest.raises(ValueError, match=msg): + with pytest.raises(NoDataError, match=msg): u.guess_TopologyAttrs(to_guess=['bonds']) From 800b4b2d88765f8af65ba9115ebd7554f75fd041 Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Sat, 26 Oct 2024 23:26:29 +1100 Subject: [PATCH 31/57] Fix bond deletion (#4763) * Fix issue where duplicate bonds were not being adequately deleted. --------- Co-authored-by: Irfan Alibay --- package/CHANGELOG | 2 ++ package/MDAnalysis/core/topologyattrs.py | 3 ++- testsuite/MDAnalysisTests/core/test_universe.py | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index eaa76f46a96..892e47d3854 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,8 @@ The rules for this file: * 2.8.0 Fixes + * Fixes bug where deleting connections by index would only delete + one of multiple, if multiple are present (Issue #4762, PR #4763) * Changes error to warning on Universe creation if guessing fails due to missing information (Issue #4750, PR #4754) * Adds guessed attributes documentation back to each parser page diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 5e2621dc63d..e5cf003b5ba 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -3146,7 +3146,8 @@ def _delete_bonds(self, values): '{attrname} with atom indices:' '{indices}').format(attrname=self.attrname, indices=indices)) - idx = [self.values.index(v) for v in to_check] + # allow multiple matches + idx = [i for i, x in enumerate(self.values) if x in to_check] for i in sorted(idx, reverse=True): del self.values[i] diff --git a/testsuite/MDAnalysisTests/core/test_universe.py b/testsuite/MDAnalysisTests/core/test_universe.py index 3e41a38a967..4f0806728e4 100644 --- a/testsuite/MDAnalysisTests/core/test_universe.py +++ b/testsuite/MDAnalysisTests/core/test_universe.py @@ -48,6 +48,7 @@ two_water_gro, two_water_gro_nonames, TRZ, TRZ_psf, PDB, MMTF, CONECT, + PDB_conect ) import MDAnalysis as mda @@ -1247,6 +1248,16 @@ def test_delete_bonds_refresh_fragments(self, universe): universe.delete_bonds([universe.atoms[[2, 3]]]) assert len(universe.atoms.fragments) == n_fragments + 1 + @pytest.mark.parametrize("filename, n_bonds", [ + (CONECT, 72), + (PDB_conect, 8) + ]) + def test_delete_all_bonds(self, filename, n_bonds): + u = mda.Universe(filename) + assert len(u.bonds) == n_bonds + u.delete_bonds(u.bonds) + assert len(u.bonds) == 0 + @pytest.mark.parametrize( 'attr,values', existing_atom_indices ) From e776f124c18c41f8990b487e8557d3ad82fe7d1f Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sun, 27 Oct 2024 01:06:58 +0000 Subject: [PATCH 32/57] Expose `MDAnalysis.topology.guessers` and `MDAnalysis.guesser.tables` under `MDAnalysis.topology.core` (#4766) * Enable direct import via MDAnalysis.topology * Switch deprecated guesser methods to individual deprecate calls --- package/MDAnalysis/topology/core.py | 8 +++ package/MDAnalysis/topology/guessers.py | 24 +++++-- .../MDAnalysisTests/topology/test_guessers.py | 64 +++++++++++++++++-- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/package/MDAnalysis/topology/core.py b/package/MDAnalysis/topology/core.py index 3ed1c7a3461..b5d73183018 100644 --- a/package/MDAnalysis/topology/core.py +++ b/package/MDAnalysis/topology/core.py @@ -38,4 +38,12 @@ from ..core._get_readers import get_parser_for from ..lib.util import cached +# Deprecated local imports +from MDAnalysis.guesser import tables +from .guessers import ( + guess_atom_element, guess_atom_type, + get_atom_mass, guess_atom_mass, guess_atom_charge, + guess_bonds, guess_angles, guess_dihedrals, guess_improper_dihedrals, +) + #tumbleweed diff --git a/package/MDAnalysis/topology/guessers.py b/package/MDAnalysis/topology/guessers.py index 7d81f239617..a4661c871c0 100644 --- a/package/MDAnalysis/topology/guessers.py +++ b/package/MDAnalysis/topology/guessers.py @@ -107,19 +107,17 @@ from ..lib import distances from MDAnalysis.guesser import tables +from MDAnalysis.lib.util import deprecate -wmsg = ( - "Deprecated in version 2.8.0\n" +deprecation_msg = ( "MDAnalysis.topology.guessers is deprecated in favour of " - "the new Guessers API and will be removed in MDAnalysis version 3.0.0. " + "the new Guessers API. " "See MDAnalysis.guesser.default_guesser for more details." ) -warnings.warn(wmsg, category=DeprecationWarning) - - +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_masses(atom_types): """Guess the mass of many atoms based upon their type @@ -137,6 +135,7 @@ def guess_masses(atom_types): return masses +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def validate_atom_types(atom_types): """Vaildates the atom types based on whether they are available in our tables @@ -162,6 +161,7 @@ def validate_atom_types(atom_types): warnings.warn("Failed to guess the mass for the following atom types: {}".format(atom_type)) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_types(atom_names): """Guess the atom type of many atoms based on atom name @@ -177,6 +177,7 @@ def guess_types(atom_names): return np.array([guess_atom_element(name) for name in atom_names], dtype=object) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_atom_type(atomname): """Guess atom type from the name. @@ -197,6 +198,8 @@ def guess_atom_type(atomname): NUMBERS = re.compile(r'[0-9]') # match numbers SYMBOLS = re.compile(r'[*+-]') # match *, +, - + +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_atom_element(atomname): """Guess the element of the atom from the name. @@ -246,6 +249,7 @@ def guess_atom_element(atomname): return no_symbols +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_bonds(atoms, coords, box=None, **kwargs): r"""Guess if bonds exist between two atoms based on their distance. @@ -354,6 +358,7 @@ def guess_bonds(atoms, coords, box=None, **kwargs): return tuple(bonds) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_angles(bonds): """Given a list of Bonds, find all angles that exist between atoms. @@ -390,6 +395,7 @@ def guess_angles(bonds): return tuple(angles_found) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_dihedrals(angles): """Given a list of Angles, find all dihedrals that exist between atoms. @@ -423,6 +429,7 @@ def guess_dihedrals(angles): return tuple(dihedrals_found) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_improper_dihedrals(angles): """Given a list of Angles, find all improper dihedrals that exist between atoms. @@ -459,6 +466,7 @@ def guess_improper_dihedrals(angles): return tuple(dihedrals_found) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def get_atom_mass(element): """Return the atomic mass in u for *element*. @@ -478,6 +486,7 @@ def get_atom_mass(element): return 0.0 +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_atom_mass(atomname): """Guess a mass based on the atom name. @@ -489,6 +498,7 @@ def guess_atom_mass(atomname): return get_atom_mass(guess_atom_element(atomname)) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_atom_charge(atomname): """Guess atom charge from the name. @@ -498,6 +508,7 @@ def guess_atom_charge(atomname): return 0.0 +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_aromaticities(atomgroup): """Guess aromaticity of atoms using RDKit @@ -518,6 +529,7 @@ def guess_aromaticities(atomgroup): return np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) +@deprecate(release="2.8.0", remove="3.0.0", message=deprecation_msg) def guess_gasteiger_charges(atomgroup): """Guess Gasteiger partial charges using RDKit diff --git a/testsuite/MDAnalysisTests/topology/test_guessers.py b/testsuite/MDAnalysisTests/topology/test_guessers.py index 7ab62b56eed..939d147d34b 100644 --- a/testsuite/MDAnalysisTests/topology/test_guessers.py +++ b/testsuite/MDAnalysisTests/topology/test_guessers.py @@ -46,12 +46,6 @@ reason="requires RDKit") -def test_moved_to_guessers_warning(): - wmsg = "deprecated in favour of the new Guessers API" - with pytest.warns(DeprecationWarning, match=wmsg): - reload(guessers) - - class TestGuessMasses(object): def test_guess_masses(self): out = guessers.guess_masses(['C', 'C', 'H']) @@ -60,7 +54,7 @@ def test_guess_masses(self): assert_equal(out, np.array([12.011, 12.011, 1.008])) def test_guess_masses_warn(self): - with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match='Failed to guess the mass'): guessers.guess_masses(['X']) def test_guess_masses_miss(self): @@ -210,3 +204,59 @@ def test_guess_gasteiger_charges(smi): u = mda.Universe(mol) values = guessers.guess_gasteiger_charges(u.atoms) assert_equal(values, expected) + + +class TestDeprecationWarning: + wmsg = ( + "MDAnalysis.topology.guessers is deprecated in favour of " + "the new Guessers API. " + "See MDAnalysis.guesser.default_guesser for more details." + ) + + @pytest.mark.parametrize('func, arg', [ + [guessers.guess_masses, ['C']], + [guessers.validate_atom_types, ['C']], + [guessers.guess_types, ['CA']], + [guessers.guess_atom_type, 'CA'], + [guessers.guess_atom_element, 'CA'], + [guessers.get_atom_mass, 'C'], + [guessers.guess_atom_mass, 'CA'], + [guessers.guess_atom_charge, 'CA'], + ]) + def test_mass_type_elements_deprecations(self, func, arg): + with pytest.warns(DeprecationWarning, match=self.wmsg): + func(arg) + + def test_bonds_deprecations(self): + u = mda.Universe(datafiles.two_water_gro) + + with pytest.warns(DeprecationWarning, match=self.wmsg): + guessers.guess_bonds(u.atoms, u.atoms.positions) + + def test_angles_dihedral_deprecations(self): + u = make_starshape() + ag = u.atoms[:5] + + with pytest.warns(DeprecationWarning, match=self.wmsg): + angles = guessers.guess_angles(ag.bonds) + + # add angles back to the Universe + u.add_TopologyAttr(Angles(angles)) + + with pytest.warns(DeprecationWarning, match=self.wmsg): + guessers.guess_dihedrals(ag.angles) + + with pytest.warns(DeprecationWarning, match=self.wmsg): + guessers.guess_improper_dihedrals(ag.angles) + + @requires_rdkit + def test_rdkit_guessers_deprecations(self): + mol = Chem.MolFromSmiles('c1ccccc1') + mol = Chem.AddHs(mol) + u = mda.Universe(mol) + + with pytest.warns(DeprecationWarning, match=self.wmsg): + guessers.guess_aromaticities(u.atoms) + + with pytest.warns(DeprecationWarning, match=self.wmsg): + guessers.guess_gasteiger_charges(u.atoms) From c9a377889af42b084f936bf7eaf2d6183e063b16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:38:39 +0000 Subject: [PATCH 33/57] Bump the github-actions group with 2 updates (#4776) Bumps the github-actions group with 2 updates: [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) and [mamba-org/setup-micromamba](https://github.com/mamba-org/setup-micromamba). Updates `pypa/gh-action-pypi-publish` from 1.10.2 to 1.11.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.2...v1.11.0) Updates `mamba-org/setup-micromamba` from 1 to 2 - [Release notes](https://github.com/mamba-org/setup-micromamba/releases) - [Commits](https://github.com/mamba-org/setup-micromamba/compare/v1...v2) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: mamba-org/setup-micromamba dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yaml | 10 +++++----- .github/workflows/gh-ci-cron.yaml | 6 +++--- .github/workflows/gh-ci.yaml | 4 ++-- .github/workflows/linters.yaml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5db1a4f5c16..19e32c978a3 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -142,7 +142,7 @@ jobs: mv dist/MDAnalysisTests-* testsuite/dist - name: upload_source_and_wheels - uses: pypa/gh-action-pypi-publish@v1.10.2 + uses: pypa/gh-action-pypi-publish@v1.11.0 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.10.2 + uses: pypa/gh-action-pypi-publish@v1.11.0 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.10.2 + uses: pypa/gh-action-pypi-publish@v1.11.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.10.2 + uses: pypa/gh-action-pypi-publish@v1.11.0 with: packages_dir: testsuite/dist @@ -256,7 +256,7 @@ jobs: - uses: actions/checkout@v4 - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-name: mda create-args: >- diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 072d1cecb51..34aadb7c941 100644 --- a/.github/workflows/gh-ci-cron.yaml +++ b/.github/workflows/gh-ci-cron.yaml @@ -104,7 +104,7 @@ jobs: os-type: "ubuntu" - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-name: mda create-args: >- @@ -157,7 +157,7 @@ jobs: os-type: ${{ matrix.os }} - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-name: mda create-args: >- @@ -249,7 +249,7 @@ jobs: os-type: ${{ matrix.os }} - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-name: mda create-args: >- diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index c07535e2f78..d389356450e 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -72,7 +72,7 @@ jobs: os-type: ${{ matrix.os }} - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-name: mda create-args: >- @@ -150,7 +150,7 @@ jobs: - uses: actions/checkout@v4 - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-name: mda create-args: >- diff --git a/.github/workflows/linters.yaml b/.github/workflows/linters.yaml index ebc6225036c..f8b24dcd091 100644 --- a/.github/workflows/linters.yaml +++ b/.github/workflows/linters.yaml @@ -76,7 +76,7 @@ jobs: - uses: actions/checkout@v4 - name: setup_micromamba - uses: mamba-org/setup-micromamba@v1 + uses: mamba-org/setup-micromamba@v2 with: environment-name: mda create-args: >- From e6bc0961ceb6677a67f166b01b98afea5420832a Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Tue, 12 Nov 2024 04:01:06 +1100 Subject: [PATCH 34/57] Allow bonds etc to be additively guessed when present (#4761) Allow bonds to be additively guessed (fixes #4759) --------- Co-authored-by: Irfan Alibay --- package/CHANGELOG | 3 + package/MDAnalysis/core/topologyattrs.py | 1 - package/MDAnalysis/core/universe.py | 67 ++++++--- package/MDAnalysis/guesser/base.py | 34 ++++- .../MDAnalysisTests/guesser/test_base.py | 138 ++++++++++++++++++ 5 files changed, 214 insertions(+), 29 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 892e47d3854..d6ee851f712 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,9 @@ The rules for this file: * 2.8.0 Fixes + * Allows bond/angle/dihedral connectivity to be guessed additively with + `to_guess`, and as a replacement of existing values with `force_guess`. + Also updates cached bond attributes when updating bonds. (Issue #4759, PR #4761) * Fixes bug where deleting connections by index would only delete one of multiple, if multiple are present (Issue #4762, PR #4763) * Changes error to warning on Universe creation if guessing fails diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index e5cf003b5ba..ef5897268c9 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -3117,7 +3117,6 @@ def _add_bonds(self, values, types=None, guessed=True, order=None): guessed = itertools.cycle((guessed,)) if order is None: order = itertools.cycle((None,)) - existing = set(self.values) for v, t, g, o in zip(values, types, guessed, order): if v not in existing: diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index 09e323f5b41..a2bc60c25f9 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -84,7 +84,10 @@ Atom, Residue, Segment, AtomGroup, ResidueGroup, SegmentGroup) from .topology import Topology -from .topologyattrs import AtomAttr, ResidueAttr, SegmentAttr, BFACTOR_WARNING +from .topologyattrs import ( + AtomAttr, ResidueAttr, SegmentAttr, + BFACTOR_WARNING, _Connection +) from .topologyobjects import TopologyObject from ..guesser.base import get_guesser @@ -454,7 +457,10 @@ def __init__(self, topology=None, *coordinates, all_coordinates=False, "the previous Context values.", DeprecationWarning ) - force_guess = list(force_guess) + ['bonds', 'angles', 'dihedrals'] + # Original behaviour is to add additionally guessed bond info + # this is achieved by adding to the `to_guess` list (unliked `force_guess` + # which replaces existing bonds). + to_guess = list(to_guess) + ['bonds', 'angles', 'dihedrals'] self.guess_TopologyAttrs( context, to_guess, force_guess, error_if_missing=False @@ -1180,7 +1186,6 @@ def _add_topology_objects(self, object_type, values, types=None, guessed=False, self.add_TopologyAttr(object_type, []) attr = getattr(self._topology, object_type) - attr._add_bonds(indices, types=types, guessed=guessed, order=order) def add_bonds(self, values, types=None, guessed=False, order=None): @@ -1231,6 +1236,16 @@ def add_bonds(self, values, types=None, guessed=False, order=None): """ self._add_topology_objects('bonds', values, types=types, guessed=guessed, order=order) + self._invalidate_bond_related_caches() + + def _invalidate_bond_related_caches(self): + """ + Invalidate caches related to bonds and fragments. + + This should be called whenever the Universe's bonds are modified. + + .. versionadded: 2.8.0 + """ # Invalidate bond-related caches self._cache.pop('fragments', None) self._cache['_valid'].pop('fragments', None) @@ -1307,7 +1322,7 @@ def _delete_topology_objects(self, object_type, values): Parameters ---------- object_type : {'bonds', 'angles', 'dihedrals', 'impropers'} - The type of TopologyObject to add. + The type of TopologyObject to delete. values : iterable of tuples, AtomGroups, or TopologyObjects; or TopologyGroup An iterable of: tuples of atom indices, or AtomGroups, or TopologyObjects. @@ -1330,7 +1345,6 @@ def _delete_topology_objects(self, object_type, values): attr = getattr(self._topology, object_type) except AttributeError: raise ValueError('There are no {} to delete'.format(object_type)) - attr._delete_bonds(indices) def delete_bonds(self, values): @@ -1371,10 +1385,7 @@ def delete_bonds(self, values): .. versionadded:: 1.0.0 """ self._delete_topology_objects('bonds', values) - # Invalidate bond-related caches - self._cache.pop('fragments', None) - self._cache['_valid'].pop('fragments', None) - self._cache['_valid'].pop('fragindices', None) + self._invalidate_bond_related_caches() def delete_angles(self, values): """Delete Angles from this Universe. @@ -1613,7 +1624,12 @@ def guess_TopologyAttrs( # in the same order that the user provided total_guess = list(dict.fromkeys(total_guess)) - objects = ['bonds', 'angles', 'dihedrals', 'impropers'] + # Set of all Connectivity related attribute names + # used to special case attribute replacement after calling the guesser + objects = set( + topattr.attrname for topattr in _TOPOLOGY_ATTRS.values() + if issubclass(topattr, _Connection) + ) # Checking if the universe is empty to avoid errors # from guesser methods @@ -1640,23 +1656,32 @@ def guess_TopologyAttrs( fg = attr in force_guess try: values = guesser.guess_attr(attr, fg) - except ValueError as e: + except NoDataError as e: if error_if_missing or fg: raise e else: warnings.warn(str(e)) continue - if values is not None: - if attr in objects: - self._add_topology_objects( - attr, values, guessed=True) - else: - guessed_attr = _TOPOLOGY_ATTRS[attr](values, True) - self.add_TopologyAttr(guessed_attr) - logger.info( - f'attribute {attr} has been guessed' - ' successfully.') + # None indicates no additional guessing was done + if values is None: + continue + if attr in objects: + # delete existing connections if they exist + if fg and hasattr(self.atoms, attr): + group = getattr(self.atoms, attr) + self._delete_topology_objects(attr, group) + # this method appends any new bonds in values to existing bonds + self._add_topology_objects( + attr, values, guessed=True) + if attr == "bonds": + self._invalidate_bond_related_caches() + else: + guessed_attr = _TOPOLOGY_ATTRS[attr](values, True) + self.add_TopologyAttr(guessed_attr) + logger.info( + f'attribute {attr} has been guessed' + ' successfully.') else: raise ValueError(f'{context} guesser can not guess the' diff --git a/package/MDAnalysis/guesser/base.py b/package/MDAnalysis/guesser/base.py index aab0723aaa6..0fd7a7e18ea 100644 --- a/package/MDAnalysis/guesser/base.py +++ b/package/MDAnalysis/guesser/base.py @@ -36,9 +36,9 @@ .. autofunction:: get_guesser """ -from .. import _GUESSERS +from .. import _GUESSERS, _TOPOLOGY_ATTRS +from ..core.topologyattrs import _Connection import numpy as np -from .. import _TOPOLOGY_ATTRS import logging from typing import Dict import copy @@ -136,21 +136,41 @@ def guess_attr(self, attr_to_guess, force_guess=False): NDArray of guessed values """ + try: + top_attr = _TOPOLOGY_ATTRS[attr_to_guess] + except KeyError: + raise KeyError( + f"{attr_to_guess} is not a recognized MDAnalysis " + "topology attribute" + ) + # make attribute to guess plural + attr_to_guess = top_attr.attrname + + try: + guesser_method = self._guesser_methods[attr_to_guess] + except KeyError: + raise ValueError(f'{type(self).__name__} cannot guess this ' + f'attribute: {attr_to_guess}') + + # Connection attributes should be just returned as they are always + # appended to the Universe. ``force_guess`` handling should happen + # at Universe level. + if issubclass(top_attr, _Connection): + return guesser_method() # check if the topology already has the attribute to partially guess it if hasattr(self._universe.atoms, attr_to_guess) and not force_guess: attr_values = np.array( getattr(self._universe.atoms, attr_to_guess, None)) - top_attr = _TOPOLOGY_ATTRS[attr_to_guess] - empty_values = top_attr.are_values_missing(attr_values) if True in empty_values: # pass to the guesser_method boolean mask to only guess the # empty values - attr_values[empty_values] = self._guesser_methods[attr_to_guess]( - indices_to_guess=empty_values) + attr_values[empty_values] = guesser_method( + indices_to_guess=empty_values + ) return attr_values else: @@ -159,7 +179,7 @@ def guess_attr(self, attr_to_guess, force_guess=False): f'not guess any new values for {attr_to_guess} attribute') return None else: - return np.array(self._guesser_methods[attr_to_guess]()) + return np.array(guesser_method()) def get_guesser(context, u=None, **kwargs): diff --git a/testsuite/MDAnalysisTests/guesser/test_base.py b/testsuite/MDAnalysisTests/guesser/test_base.py index fe645a7c3ca..c44fdc3c591 100644 --- a/testsuite/MDAnalysisTests/guesser/test_base.py +++ b/testsuite/MDAnalysisTests/guesser/test_base.py @@ -27,8 +27,11 @@ from MDAnalysis.core.topology import Topology from MDAnalysis.core.topologyattrs import Masses, Atomnames, Atomtypes import MDAnalysis.tests.datafiles as datafiles +from MDAnalysis.exceptions import NoDataError from numpy.testing import assert_allclose, assert_equal +from MDAnalysis import _TOPOLOGY_ATTRS, _GUESSERS + class TestBaseGuesser(): @@ -101,6 +104,141 @@ def test_partial_guess_attr_with_unknown_no_value_label(self): u = mda.Universe(top, to_guess=['types']) assert_equal(u.atoms.types, ['', '', '', '']) + def test_guess_topology_objects_existing_read(self): + u = mda.Universe(datafiles.CONECT) + assert len(u.atoms.bonds) == 72 + assert list(u.bonds[0].indices) == [623, 630] + + # delete some bonds + u.delete_bonds(u.atoms.bonds[:10]) + assert len(u.atoms.bonds) == 62 + # first bond has changed + assert list(u.bonds[0].indices) == [1545, 1552] + # count number of (1545, 1552) bonds + ag = u.atoms[[1545, 1552]] + bonds = ag.bonds.atomgroup_intersection(ag, strict=True) + assert len(bonds) == 1 + assert not bonds[0].is_guessed + + all_indices = [tuple(x.indices) for x in u.bonds] + assert (623, 630) not in all_indices + + # test guessing new bonds doesn't remove old ones + u.guess_TopologyAttrs("default", to_guess=["bonds"]) + assert len(u.atoms.bonds) == 1922 + old_bonds = ag.bonds.atomgroup_intersection(ag, strict=True) + assert len(old_bonds) == 1 + # test guessing new bonds doesn't duplicate old ones + assert not old_bonds[0].is_guessed + + new_ag = u.atoms[[623, 630]] + new_bonds = new_ag.bonds.atomgroup_intersection(new_ag, strict=True) + assert len(new_bonds) == 1 + assert new_bonds[0].is_guessed + + def test_guess_topology_objects_existing_in_universe(self): + u = mda.Universe(datafiles.CONECT, to_guess=["bonds"]) + assert len(u.atoms.bonds) == 1922 + assert list(u.bonds[0].indices) == [0, 1] + + # delete some bonds + u.delete_bonds(u.atoms.bonds[:100]) + assert len(u.atoms.bonds) == 1822 + assert list(u.bonds[0].indices) == [94, 99] + + all_indices = [tuple(x.indices) for x in u.bonds] + assert (0, 1) not in all_indices + + # guess old bonds back + u.guess_TopologyAttrs("default", to_guess=["bonds"]) + assert len(u.atoms.bonds) == 1922 + # check TopologyGroup contains new (old) bonds + assert list(u.bonds[0].indices) == [0, 1] + + def test_guess_topology_objects_force(self): + u = mda.Universe(datafiles.CONECT, force_guess=["bonds"]) + assert len(u.atoms.bonds) == 1922 + + with pytest.raises(NoDataError): + u.atoms.angles + + def test_guess_topology_objects_out_of_order_init(self): + u = mda.Universe( + datafiles.PDB_small, + to_guess=["dihedrals", "angles", "bonds"], + guess_bonds=False + ) + assert len(u.atoms.angles) == 6123 + assert len(u.atoms.dihedrals) == 8921 + + def test_guess_topology_objects_out_of_order_guess(self): + u = mda.Universe(datafiles.PDB_small) + with pytest.raises(NoDataError): + u.atoms.angles + + u.guess_TopologyAttrs( + "default", + to_guess=["dihedrals", "angles", "bonds"] + ) + assert len(u.atoms.angles) == 6123 + assert len(u.atoms.dihedrals) == 8921 + + def test_force_guess_overwrites_existing_bonds(self): + u = mda.Universe(datafiles.CONECT) + assert len(u.atoms.bonds) == 72 + + # This low radius should find no bonds + vdw = dict.fromkeys(set(u.atoms.types), 0.1) + u.guess_TopologyAttrs("default", to_guess=["bonds"], vdwradii=vdw) + assert len(u.atoms.bonds) == 72 + + # Now force guess bonds + u.guess_TopologyAttrs("default", force_guess=["bonds"], vdwradii=vdw) + assert len(u.atoms.bonds) == 0 + + def test_guessing_angles_respects_bond_kwargs(self): + u = mda.Universe(datafiles.PDB) + assert not hasattr(u.atoms, "angles") + + # This low radius should find no angles + vdw = dict.fromkeys(set(u.atoms.types), 0.01) + + u.guess_TopologyAttrs("default", to_guess=["angles"], vdwradii=vdw) + assert len(u.atoms.angles) == 0 + + # set higher radii for lots of angles! + vdw = dict.fromkeys(set(u.atoms.types), 1) + u.guess_TopologyAttrs("default", force_guess=["angles"], vdwradii=vdw) + assert len(u.atoms.angles) == 89466 + + def test_guessing_dihedrals_respects_bond_kwargs(self): + u = mda.Universe(datafiles.CONECT) + assert len(u.atoms.bonds) == 72 + + u.guess_TopologyAttrs("default", to_guess=["dihedrals"]) + assert len(u.atoms.dihedrals) == 3548 + assert not hasattr(u.atoms, "angles") + + def test_guess_invalid_attribute(self): + default_guesser = get_guesser("default") + err = "not a recognized MDAnalysis topology attribute" + with pytest.raises(KeyError, match=err): + default_guesser.guess_attr('not_an_attribute') + + def test_guess_unsupported_attribute(self): + default_guesser = get_guesser("default") + err = "cannot guess this attribute" + with pytest.raises(ValueError, match=err): + default_guesser.guess_attr('tempfactors') + + def test_guess_singular(self): + default_guesser = get_guesser("default") + u = mda.Universe(datafiles.PDB, to_guess=[]) + assert not hasattr(u.atoms, "masses") + + default_guesser._universe = u + masses = default_guesser.guess_attr('mass') + def test_Universe_guess_bonds_deprecated(): with pytest.warns( From 5eeedd65198a3491cd68fa26a60e42a9f685fcf5 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Mon, 11 Nov 2024 18:00:59 +0000 Subject: [PATCH 35/57] Finalize v2.8.0 release (#4780) --- package/CHANGELOG | 2 +- package/MDAnalysis/version.py | 2 +- package/setup.py | 2 +- testsuite/MDAnalysisTests/__init__.py | 2 +- testsuite/pyproject.toml | 3 ++- testsuite/setup.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index d6ee851f712..83dbbc4d0c3 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,7 +14,7 @@ The rules for this file: ------------------------------------------------------------------------------- -??/??/?? IAlibay, HeetVekariya, marinegor, lilyminium, RMeli, +11/11/24 IAlibay, HeetVekariya, marinegor, lilyminium, RMeli, ljwoods2, aditya292002, pstaerk, PicoCentauri, BFedder, tyler.je.reddy, SampurnaM, leonwehrhan, kainszs, orionarcher, yuxuanzhuang, PythonFZ, laksh-krishna-sharma, orbeckst, MattTDavies, diff --git a/package/MDAnalysis/version.py b/package/MDAnalysis/version.py index 4f165486593..a8213cc90e6 100644 --- a/package/MDAnalysis/version.py +++ b/package/MDAnalysis/version.py @@ -67,4 +67,4 @@ # e.g. with lib.log #: Release of MDAnalysis as a string, using `semantic versioning`_. -__version__ = "2.8.0-dev0" # NOTE: keep in sync with RELEASE in setup.py +__version__ = "2.8.0" # NOTE: keep in sync with RELEASE in setup.py diff --git a/package/setup.py b/package/setup.py index 0fc4bef4e2a..a65641f93b2 100755 --- a/package/setup.py +++ b/package/setup.py @@ -58,7 +58,7 @@ from subprocess import getoutput # NOTE: keep in sync with MDAnalysis.__version__ in version.py -RELEASE = "2.8.0-dev0" +RELEASE = "2.8.0" is_release = 'dev' not in RELEASE diff --git a/testsuite/MDAnalysisTests/__init__.py b/testsuite/MDAnalysisTests/__init__.py index 942033e167f..3924a570855 100644 --- a/testsuite/MDAnalysisTests/__init__.py +++ b/testsuite/MDAnalysisTests/__init__.py @@ -97,7 +97,7 @@ logger = logging.getLogger("MDAnalysisTests.__init__") # keep in sync with RELEASE in setup.py -__version__ = "2.8.0-dev0" +__version__ = "2.8.0" # Do NOT import MDAnalysis at this level. Tests should do it themselves. diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index a757271db9d..c81800660f1 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: C", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Bio-Informatics", @@ -154,6 +155,6 @@ filterwarnings = [ [tool.black] line-length = 79 -target-version = ['py310', 'py311', 'py312'] +target-version = ['py310', 'py311', 'py312', 'py313'] extend-exclude = '.' required-version = '24' diff --git a/testsuite/setup.py b/testsuite/setup.py index 887546fd385..58f314e7b40 100755 --- a/testsuite/setup.py +++ b/testsuite/setup.py @@ -74,7 +74,7 @@ def run(self): if __name__ == '__main__': # this must be in-sync with MDAnalysis - RELEASE = "2.8.0-dev0" + RELEASE = "2.8.0" setup( version=RELEASE, From b254921612468c1e7c564379003d7ca62e42e04e Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Mon, 11 Nov 2024 19:18:34 +0000 Subject: [PATCH 36/57] revert to artifact upload/download to v3 for 2.8.0 release (#4784) --- .github/workflows/deploy.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 19e32c978a3..dd0570bc464 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -63,7 +63,7 @@ jobs: if: | (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/package')) || (github.event_name == 'release' && github.event.action == 'published') - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: path: wheelhouse/*.whl retention-days: 7 @@ -88,7 +88,7 @@ jobs: if: | (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/package')) || (github.event_name == 'release' && github.event.action == 'published') - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: path: package/dist/*.tar.gz retention-days: 7 @@ -113,7 +113,7 @@ jobs: if: | (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/package')) || (github.event_name == 'release' && github.event.action == 'published') - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: path: testsuite/dist/*.tar.gz retention-days: 7 @@ -131,7 +131,7 @@ jobs: runs-on: ubuntu-latest needs: [build_wheels, build_sdist, build_sdist_tests] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: name: artifact path: dist @@ -160,7 +160,7 @@ jobs: runs-on: ubuntu-latest needs: [build_wheels, build_sdist, build_sdist_tests] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: name: artifact path: dist @@ -190,7 +190,7 @@ jobs: runs-on: ubuntu-latest needs: [build_wheels, build_sdist, build_sdist_tests] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: name: artifact path: dist @@ -216,7 +216,7 @@ jobs: runs-on: ubuntu-latest needs: [build_wheels, build_sdist, build_sdist_tests] steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v3 with: name: artifact path: dist From 4e42f7a85b5dd9b1f2624231dba7cc6450611cd9 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:35:02 +0100 Subject: [PATCH 37/57] Addition of `isInstance` test of `BackendSerial` (#4773) * Update test_base.py Addition of test for serial backend instance * Update test_base.py pep fix * Update test_base.py Adjusted test_instance_serial_backend to test through pytest raise ValueError --- testsuite/MDAnalysisTests/analysis/test_base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 68b86fc9439..ab7748a20c7 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -286,6 +286,19 @@ def test_parallelizable_transformations(): with pytest.raises(ValueError): FrameAnalysis(u.trajectory).run(backend='multiprocessing') + +def test_instance_serial_backend(u): + # test that isinstance is checked and the correct ValueError raise appears + msg = 'Can not display progressbar with non-serial backend' + with pytest.raises(ValueError, match=msg): + FrameAnalysis(u.trajectory).run( + backend=backends.BackendMultiprocessing(n_workers=2), + verbose=True, + progressbar_kwargs={"leave": True}, + unsupported_backend=True + ) + + def test_frame_bool_fail(client_FrameAnalysis): u = mda.Universe(TPR, XTC) # dt = 100 an = FrameAnalysis(u.trajectory) From 0a91d2cc1825f2bc759d138b3593e5d44306fa16 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Fri, 22 Nov 2024 15:42:24 +0000 Subject: [PATCH 38/57] change license to LGPL switch (#4794) * change license from GPLv2+ to LGPLv2.1+ * license change * package is temporarily under LGPLv3+ until we remove apache licensed dependencies: Update LICENSE files (LGPLv3+), include a copy of GPLv3 license text as referenced by LGPLv3 * Update all code references of GPLv2+ to LGPLv2.1+ * see https://www.mdanalysis.org/2023/09/22/licensing-update/#detailing-our-proposed-relicensing-process for details (we are now at step 3, where we package under LGPLv3+ until we remove our apache dependencies) * specific changes * Update main LICENSE * update the other two LICENSE files * Updating license headers for MDAnalysisTests * Update docs, README, authors, pyproject.toml, setup.py * Analysis files * Through to lib * The rest * update CHANGELOG --------- Co-authored-by: Oliver Beckstein Co-authored-by: Rocco Meli --- LICENSE | 537 ++---- README.rst | 8 +- package/AUTHORS | 6 +- package/CHANGELOG | 2 + package/LICENSE | 1532 ++++++++++++++--- package/MDAnalysis/__init__.py | 2 +- package/MDAnalysis/analysis/__init__.py | 2 +- package/MDAnalysis/analysis/align.py | 6 +- .../MDAnalysis/analysis/atomicdistances.py | 4 +- package/MDAnalysis/analysis/base.py | 2 +- package/MDAnalysis/analysis/bat.py | 4 +- package/MDAnalysis/analysis/contacts.py | 2 +- package/MDAnalysis/analysis/data/filenames.py | 2 +- package/MDAnalysis/analysis/density.py | 4 +- package/MDAnalysis/analysis/dielectric.py | 4 +- package/MDAnalysis/analysis/diffusionmap.py | 4 +- package/MDAnalysis/analysis/dihedrals.py | 4 +- package/MDAnalysis/analysis/distances.py | 2 +- .../MDAnalysis/analysis/encore/__init__.py | 2 +- .../MDAnalysis/analysis/encore/bootstrap.py | 2 +- .../encore/clustering/ClusterCollection.py | 2 +- .../encore/clustering/ClusteringMethod.py | 2 +- .../analysis/encore/clustering/__init__.py | 2 +- .../encore/clustering/affinityprop.pyx | 2 +- .../analysis/encore/clustering/cluster.py | 2 +- .../analysis/encore/clustering/include/ap.h | 2 +- .../analysis/encore/clustering/src/ap.c | 2 +- .../analysis/encore/confdistmatrix.py | 2 +- .../MDAnalysis/analysis/encore/covariance.py | 2 +- package/MDAnalysis/analysis/encore/cutils.pyx | 2 +- .../DimensionalityReductionMethod.py | 2 +- .../dimensionality_reduction/__init__.py | 2 +- .../dimensionality_reduction/include/spe.h | 2 +- .../reduce_dimensionality.py | 2 +- .../encore/dimensionality_reduction/src/spe.c | 2 +- .../stochasticproxembed.pyx | 2 +- .../MDAnalysis/analysis/encore/similarity.py | 2 +- package/MDAnalysis/analysis/encore/utils.py | 2 +- package/MDAnalysis/analysis/gnm.py | 4 +- .../MDAnalysis/analysis/hbonds/__init__.py | 2 +- .../analysis/hbonds/hbond_autocorrel.py | 4 +- package/MDAnalysis/analysis/helix_analysis.py | 4 +- package/MDAnalysis/analysis/hole2/__init__.py | 4 +- .../analysis/hydrogenbonds/__init__.py | 2 +- .../analysis/hydrogenbonds/hbond_analysis.py | 4 +- .../hydrogenbonds/hbond_autocorrel.py | 4 +- .../hydrogenbonds/wbridge_analysis.py | 4 +- package/MDAnalysis/analysis/leaflet.py | 2 +- .../MDAnalysis/analysis/legacy/__init__.py | 2 +- package/MDAnalysis/analysis/legacy/x3dna.py | 4 +- package/MDAnalysis/analysis/lineardensity.py | 2 +- package/MDAnalysis/analysis/msd.py | 4 +- package/MDAnalysis/analysis/nucleicacids.py | 2 +- package/MDAnalysis/analysis/nuclinfo.py | 4 +- package/MDAnalysis/analysis/pca.py | 4 +- package/MDAnalysis/analysis/polymer.py | 4 +- package/MDAnalysis/analysis/psa.py | 4 +- package/MDAnalysis/analysis/rdf.py | 2 +- package/MDAnalysis/analysis/rms.py | 4 +- package/MDAnalysis/analysis/waterdynamics.py | 4 +- package/MDAnalysis/auxiliary/EDR.py | 2 +- package/MDAnalysis/auxiliary/XVG.py | 2 +- package/MDAnalysis/auxiliary/__init__.py | 2 +- package/MDAnalysis/auxiliary/base.py | 2 +- package/MDAnalysis/auxiliary/core.py | 2 +- package/MDAnalysis/converters/OpenMM.py | 2 +- package/MDAnalysis/converters/OpenMMParser.py | 2 +- package/MDAnalysis/converters/ParmEd.py | 2 +- package/MDAnalysis/converters/ParmEdParser.py | 2 +- package/MDAnalysis/converters/RDKit.py | 2 +- package/MDAnalysis/converters/RDKitParser.py | 2 +- package/MDAnalysis/converters/__init__.py | 2 +- package/MDAnalysis/converters/base.py | 2 +- package/MDAnalysis/coordinates/CRD.py | 2 +- package/MDAnalysis/coordinates/DCD.py | 2 +- package/MDAnalysis/coordinates/DLPoly.py | 2 +- package/MDAnalysis/coordinates/DMS.py | 2 +- package/MDAnalysis/coordinates/FHIAIMS.py | 2 +- package/MDAnalysis/coordinates/GMS.py | 2 +- package/MDAnalysis/coordinates/GRO.py | 2 +- package/MDAnalysis/coordinates/GSD.py | 2 +- package/MDAnalysis/coordinates/H5MD.py | 2 +- package/MDAnalysis/coordinates/INPCRD.py | 2 +- package/MDAnalysis/coordinates/LAMMPS.py | 2 +- package/MDAnalysis/coordinates/MMTF.py | 2 +- package/MDAnalysis/coordinates/MOL2.py | 2 +- package/MDAnalysis/coordinates/NAMDBIN.py | 2 +- package/MDAnalysis/coordinates/PDB.py | 2 +- package/MDAnalysis/coordinates/PDBQT.py | 2 +- package/MDAnalysis/coordinates/PQR.py | 2 +- package/MDAnalysis/coordinates/ParmEd.py | 2 +- package/MDAnalysis/coordinates/TNG.py | 2 +- package/MDAnalysis/coordinates/TRC.py | 2 +- package/MDAnalysis/coordinates/TRJ.py | 2 +- package/MDAnalysis/coordinates/TRR.py | 2 +- package/MDAnalysis/coordinates/TRZ.py | 2 +- package/MDAnalysis/coordinates/TXYZ.py | 2 +- package/MDAnalysis/coordinates/XDR.py | 2 +- package/MDAnalysis/coordinates/XTC.py | 2 +- package/MDAnalysis/coordinates/XYZ.py | 2 +- package/MDAnalysis/coordinates/__init__.py | 2 +- package/MDAnalysis/coordinates/base.py | 2 +- package/MDAnalysis/coordinates/chain.py | 2 +- package/MDAnalysis/coordinates/chemfiles.py | 2 +- package/MDAnalysis/coordinates/core.py | 2 +- package/MDAnalysis/coordinates/memory.py | 4 +- package/MDAnalysis/coordinates/null.py | 2 +- package/MDAnalysis/coordinates/timestep.pyx | 2 +- package/MDAnalysis/core/__init__.py | 2 +- package/MDAnalysis/core/_get_readers.py | 2 +- package/MDAnalysis/core/accessors.py | 2 +- package/MDAnalysis/core/groups.py | 2 +- package/MDAnalysis/core/selection.py | 2 +- package/MDAnalysis/core/topology.py | 2 +- package/MDAnalysis/core/topologyattrs.py | 2 +- package/MDAnalysis/core/topologyobjects.py | 2 +- package/MDAnalysis/core/universe.py | 2 +- package/MDAnalysis/exceptions.py | 2 +- package/MDAnalysis/guesser/tables.py | 2 +- package/MDAnalysis/lib/NeighborSearch.py | 2 +- package/MDAnalysis/lib/__init__.py | 2 +- package/MDAnalysis/lib/_augment.pyx | 2 +- package/MDAnalysis/lib/_cutil.pyx | 2 +- package/MDAnalysis/lib/_distopia.py | 2 +- package/MDAnalysis/lib/c_distances.pyx | 2 +- package/MDAnalysis/lib/c_distances_openmp.pyx | 2 +- package/MDAnalysis/lib/correlations.py | 4 +- package/MDAnalysis/lib/distances.py | 2 +- package/MDAnalysis/lib/formats/__init__.py | 2 +- .../MDAnalysis/lib/formats/cython_util.pxd | 2 +- .../MDAnalysis/lib/formats/cython_util.pyx | 2 +- .../MDAnalysis/lib/formats/include/trr_seek.h | 2 +- .../MDAnalysis/lib/formats/include/xtc_seek.h | 2 +- package/MDAnalysis/lib/formats/libdcd.pxd | 2 +- package/MDAnalysis/lib/formats/libdcd.pyx | 2 +- package/MDAnalysis/lib/formats/libmdaxdr.pxd | 2 +- package/MDAnalysis/lib/formats/libmdaxdr.pyx | 2 +- package/MDAnalysis/lib/formats/src/trr_seek.c | 2 +- package/MDAnalysis/lib/formats/src/xtc_seek.c | 2 +- .../MDAnalysis/lib/include/calc_distances.h | 2 +- package/MDAnalysis/lib/log.py | 2 +- package/MDAnalysis/lib/mdamath.py | 2 +- package/MDAnalysis/lib/picklable_file_io.py | 2 +- package/MDAnalysis/lib/pkdtree.py | 2 +- package/MDAnalysis/lib/util.py | 2 +- package/MDAnalysis/selections/__init__.py | 2 +- package/MDAnalysis/selections/base.py | 2 +- package/MDAnalysis/selections/charmm.py | 2 +- package/MDAnalysis/selections/gromacs.py | 2 +- package/MDAnalysis/selections/jmol.py | 2 +- package/MDAnalysis/selections/pymol.py | 2 +- package/MDAnalysis/selections/vmd.py | 2 +- package/MDAnalysis/tests/__init__.py | 2 +- package/MDAnalysis/tests/datafiles.py | 2 +- package/MDAnalysis/topology/CRDParser.py | 2 +- package/MDAnalysis/topology/DLPolyParser.py | 2 +- package/MDAnalysis/topology/DMSParser.py | 2 +- .../MDAnalysis/topology/ExtendedPDBParser.py | 2 +- package/MDAnalysis/topology/FHIAIMSParser.py | 2 +- package/MDAnalysis/topology/GMSParser.py | 2 +- package/MDAnalysis/topology/GROParser.py | 2 +- package/MDAnalysis/topology/GSDParser.py | 2 +- package/MDAnalysis/topology/HoomdXMLParser.py | 2 +- package/MDAnalysis/topology/ITPParser.py | 2 +- package/MDAnalysis/topology/LAMMPSParser.py | 2 +- package/MDAnalysis/topology/MMTFParser.py | 2 +- package/MDAnalysis/topology/MOL2Parser.py | 2 +- package/MDAnalysis/topology/MinimalParser.py | 2 +- package/MDAnalysis/topology/PDBParser.py | 2 +- package/MDAnalysis/topology/PDBQTParser.py | 2 +- package/MDAnalysis/topology/PQRParser.py | 2 +- package/MDAnalysis/topology/PSFParser.py | 2 +- package/MDAnalysis/topology/ParmEdParser.py | 2 +- package/MDAnalysis/topology/TOPParser.py | 2 +- package/MDAnalysis/topology/TPRParser.py | 2 +- package/MDAnalysis/topology/TXYZParser.py | 2 +- package/MDAnalysis/topology/XYZParser.py | 2 +- package/MDAnalysis/topology/__init__.py | 2 +- package/MDAnalysis/topology/base.py | 2 +- package/MDAnalysis/topology/core.py | 2 +- package/MDAnalysis/topology/guessers.py | 2 +- package/MDAnalysis/topology/tpr/__init__.py | 2 +- package/MDAnalysis/topology/tpr/obj.py | 4 +- package/MDAnalysis/topology/tpr/setting.py | 4 +- package/MDAnalysis/topology/tpr/utils.py | 4 +- .../MDAnalysis/transformations/__init__.py | 2 +- package/MDAnalysis/transformations/base.py | 2 +- .../transformations/boxdimensions.py | 2 +- package/MDAnalysis/transformations/fit.py | 2 +- package/MDAnalysis/transformations/nojump.py | 2 +- .../transformations/positionaveraging.py | 2 +- package/MDAnalysis/transformations/rotate.py | 2 +- .../MDAnalysis/transformations/translate.py | 2 +- package/MDAnalysis/transformations/wrap.py | 2 +- package/MDAnalysis/units.py | 2 +- package/MDAnalysis/version.py | 2 +- package/MDAnalysis/visualization/__init__.py | 2 +- .../MDAnalysis/visualization/streamlines.py | 4 +- .../visualization/streamlines_3D.py | 4 +- package/README | 6 +- .../documentation_pages/analysis/encore.rst | 2 +- .../documentation_pages/analysis/hole2.rst | 2 +- package/doc/sphinx/source/index.rst | 8 +- package/pyproject.toml | 2 +- package/setup.py | 2 +- testsuite/LICENSE | 564 +++--- testsuite/MDAnalysisTests/__init__.py | 2 +- .../MDAnalysisTests/analysis/test_align.py | 2 +- .../analysis/test_atomicdistances.py | 2 +- .../MDAnalysisTests/analysis/test_base.py | 2 +- .../MDAnalysisTests/analysis/test_bat.py | 2 +- .../MDAnalysisTests/analysis/test_contacts.py | 2 +- .../MDAnalysisTests/analysis/test_data.py | 2 +- .../MDAnalysisTests/analysis/test_density.py | 2 +- .../analysis/test_dielectric.py | 2 +- .../analysis/test_diffusionmap.py | 2 +- .../analysis/test_dihedrals.py | 2 +- .../analysis/test_distances.py | 2 +- .../MDAnalysisTests/analysis/test_encore.py | 2 +- .../MDAnalysisTests/analysis/test_gnm.py | 2 +- .../analysis/test_helix_analysis.py | 2 +- .../MDAnalysisTests/analysis/test_hole2.py | 2 +- .../analysis/test_hydrogenbondautocorrel.py | 2 +- .../test_hydrogenbondautocorrel_deprecated.py | 2 +- .../analysis/test_hydrogenbonds_analysis.py | 2 +- .../MDAnalysisTests/analysis/test_leaflet.py | 2 +- .../analysis/test_lineardensity.py | 2 +- .../MDAnalysisTests/analysis/test_msd.py | 2 +- .../analysis/test_nucleicacids.py | 2 +- .../MDAnalysisTests/analysis/test_nuclinfo.py | 2 +- .../MDAnalysisTests/analysis/test_pca.py | 2 +- .../analysis/test_persistencelength.py | 2 +- .../MDAnalysisTests/analysis/test_psa.py | 2 +- .../MDAnalysisTests/analysis/test_rdf.py | 2 +- .../MDAnalysisTests/analysis/test_rdf_s.py | 2 +- .../MDAnalysisTests/analysis/test_rms.py | 2 +- .../analysis/test_waterdynamics.py | 2 +- testsuite/MDAnalysisTests/auxiliary/base.py | 2 +- .../MDAnalysisTests/auxiliary/test_core.py | 2 +- .../MDAnalysisTests/auxiliary/test_edr.py | 2 +- .../MDAnalysisTests/auxiliary/test_xvg.py | 2 +- .../MDAnalysisTests/converters/test_base.py | 2 +- .../MDAnalysisTests/converters/test_openmm.py | 2 +- .../converters/test_openmm_parser.py | 2 +- .../MDAnalysisTests/converters/test_parmed.py | 2 +- .../converters/test_parmed_parser.py | 2 +- .../MDAnalysisTests/converters/test_rdkit.py | 2 +- .../converters/test_rdkit_parser.py | 2 +- testsuite/MDAnalysisTests/coordinates/base.py | 2 +- .../MDAnalysisTests/coordinates/reference.py | 2 +- .../coordinates/test_amber_inpcrd.py | 2 +- .../coordinates/test_chainreader.py | 2 +- .../coordinates/test_chemfiles.py | 2 +- .../coordinates/test_copying.py | 2 +- .../MDAnalysisTests/coordinates/test_crd.py | 2 +- .../MDAnalysisTests/coordinates/test_dcd.py | 2 +- .../coordinates/test_dlpoly.py | 2 +- .../MDAnalysisTests/coordinates/test_dms.py | 2 +- .../coordinates/test_fhiaims.py | 2 +- .../MDAnalysisTests/coordinates/test_gms.py | 2 +- .../MDAnalysisTests/coordinates/test_gro.py | 2 +- .../MDAnalysisTests/coordinates/test_gsd.py | 2 +- .../coordinates/test_lammps.py | 2 +- .../coordinates/test_memory.py | 2 +- .../MDAnalysisTests/coordinates/test_mmtf.py | 2 +- .../MDAnalysisTests/coordinates/test_mol2.py | 2 +- .../coordinates/test_namdbin.py | 2 +- .../coordinates/test_netcdf.py | 2 +- .../MDAnalysisTests/coordinates/test_null.py | 2 +- .../MDAnalysisTests/coordinates/test_pdb.py | 2 +- .../MDAnalysisTests/coordinates/test_pdbqt.py | 2 +- .../MDAnalysisTests/coordinates/test_pqr.py | 2 +- .../coordinates/test_reader_api.py | 2 +- .../coordinates/test_timestep_api.py | 2 +- .../MDAnalysisTests/coordinates/test_tng.py | 2 +- .../MDAnalysisTests/coordinates/test_trc.py | 2 +- .../MDAnalysisTests/coordinates/test_trj.py | 2 +- .../MDAnalysisTests/coordinates/test_trz.py | 2 +- .../MDAnalysisTests/coordinates/test_txyz.py | 2 +- .../coordinates/test_windows.py | 2 +- .../coordinates/test_writer_api.py | 2 +- .../coordinates/test_writer_registration.py | 2 +- .../MDAnalysisTests/coordinates/test_xdr.py | 2 +- .../MDAnalysisTests/coordinates/test_xyz.py | 2 +- .../MDAnalysisTests/core/test_accessors.py | 2 +- .../MDAnalysisTests/core/test_accumulate.py | 2 +- testsuite/MDAnalysisTests/core/test_atom.py | 2 +- .../MDAnalysisTests/core/test_atomgroup.py | 2 +- .../core/test_atomselections.py | 2 +- .../MDAnalysisTests/core/test_copying.py | 2 +- .../MDAnalysisTests/core/test_fragments.py | 2 +- .../core/test_group_traj_access.py | 2 +- testsuite/MDAnalysisTests/core/test_groups.py | 2 +- .../MDAnalysisTests/core/test_index_dtype.py | 2 +- .../MDAnalysisTests/core/test_residue.py | 2 +- .../MDAnalysisTests/core/test_residuegroup.py | 2 +- .../MDAnalysisTests/core/test_segment.py | 2 +- .../MDAnalysisTests/core/test_segmentgroup.py | 2 +- .../core/test_topologyattrs.py | 2 +- .../core/test_topologyobjects.py | 2 +- .../MDAnalysisTests/core/test_universe.py | 2 +- testsuite/MDAnalysisTests/core/test_unwrap.py | 2 +- .../core/test_updating_atomgroup.py | 2 +- testsuite/MDAnalysisTests/core/test_wrap.py | 2 +- testsuite/MDAnalysisTests/core/util.py | 2 +- testsuite/MDAnalysisTests/datafiles.py | 2 +- testsuite/MDAnalysisTests/dummy.py | 2 +- .../MDAnalysisTests/formats/test_libdcd.py | 2 +- .../MDAnalysisTests/formats/test_libmdaxdr.py | 2 +- .../MDAnalysisTests/import/fork_called.py | 2 +- .../MDAnalysisTests/import/test_import.py | 2 +- testsuite/MDAnalysisTests/lib/test_augment.py | 2 +- testsuite/MDAnalysisTests/lib/test_cutil.py | 2 +- .../MDAnalysisTests/lib/test_distances.py | 2 +- testsuite/MDAnalysisTests/lib/test_log.py | 2 +- .../lib/test_neighborsearch.py | 2 +- testsuite/MDAnalysisTests/lib/test_nsgrid.py | 2 +- testsuite/MDAnalysisTests/lib/test_pkdtree.py | 2 +- testsuite/MDAnalysisTests/lib/test_qcprot.py | 2 +- testsuite/MDAnalysisTests/lib/test_util.py | 2 +- .../parallelism/test_multiprocessing.py | 2 +- .../parallelism/test_pickle_transformation.py | 2 +- testsuite/MDAnalysisTests/test_api.py | 2 +- testsuite/MDAnalysisTests/topology/base.py | 2 +- .../MDAnalysisTests/topology/test_altloc.py | 2 +- .../MDAnalysisTests/topology/test_crd.py | 2 +- .../MDAnalysisTests/topology/test_dlpoly.py | 2 +- .../MDAnalysisTests/topology/test_dms.py | 2 +- .../MDAnalysisTests/topology/test_fhiaims.py | 2 +- .../MDAnalysisTests/topology/test_gms.py | 2 +- .../MDAnalysisTests/topology/test_gro.py | 2 +- .../MDAnalysisTests/topology/test_gsd.py | 2 +- .../MDAnalysisTests/topology/test_guessers.py | 2 +- .../MDAnalysisTests/topology/test_hoomdxml.py | 2 +- .../MDAnalysisTests/topology/test_itp.py | 2 +- .../topology/test_lammpsdata.py | 2 +- .../MDAnalysisTests/topology/test_minimal.py | 2 +- .../MDAnalysisTests/topology/test_mol2.py | 2 +- .../MDAnalysisTests/topology/test_pdb.py | 2 +- .../MDAnalysisTests/topology/test_pdbqt.py | 2 +- .../MDAnalysisTests/topology/test_pqr.py | 2 +- .../MDAnalysisTests/topology/test_psf.py | 2 +- .../MDAnalysisTests/topology/test_top.py | 2 +- .../topology/test_topology_str_types.py | 2 +- .../topology/test_tprparser.py | 2 +- .../MDAnalysisTests/topology/test_txyz.py | 2 +- .../MDAnalysisTests/topology/test_xpdb.py | 2 +- .../MDAnalysisTests/topology/test_xyz.py | 2 +- .../transformations/test_base.py | 2 +- .../transformations/test_boxdimensions.py | 2 +- .../transformations/test_fit.py | 2 +- .../transformations/test_rotate.py | 2 +- .../transformations/test_translate.py | 2 +- .../transformations/test_wrap.py | 2 +- testsuite/MDAnalysisTests/util.py | 2 +- .../MDAnalysisTests/utils/test_authors.py | 2 +- .../MDAnalysisTests/utils/test_datafiles.py | 2 +- .../MDAnalysisTests/utils/test_duecredit.py | 2 +- .../MDAnalysisTests/utils/test_failure.py | 2 +- .../MDAnalysisTests/utils/test_imports.py | 2 +- testsuite/MDAnalysisTests/utils/test_log.py | 2 +- testsuite/MDAnalysisTests/utils/test_meta.py | 2 +- .../MDAnalysisTests/utils/test_modelling.py | 2 +- .../MDAnalysisTests/utils/test_persistence.py | 2 +- .../MDAnalysisTests/utils/test_pickleio.py | 2 +- .../MDAnalysisTests/utils/test_qcprot.py | 2 +- .../MDAnalysisTests/utils/test_selections.py | 2 +- .../MDAnalysisTests/utils/test_streamio.py | 2 +- .../utils/test_transformations.py | 2 +- testsuite/MDAnalysisTests/utils/test_units.py | 2 +- .../visualization/test_streamlines.py | 2 +- testsuite/pyproject.toml | 2 +- testsuite/setup.py | 2 +- 373 files changed, 2087 insertions(+), 1366 deletions(-) diff --git a/LICENSE b/LICENSE index 59b77146988..8d0eeadd954 100644 --- a/LICENSE +++ b/LICENSE @@ -2,22 +2,17 @@ Licensing of the MDAnalysis library ========================================================================== -As of MDAnalysis version 2.6.0, the MDAnalyis library is packaged under -the terms of the GNU General Public License version 3 or any later version -(GPLv3+). - -Developer contributions to the library have, unless otherwise stated, been -made under the following conditions: - - From the 31st of July 2023 onwards, all contributions are made under - the terms of the GNU Lesser General Public License v2.1 or any later - version (LGPLv2.1+) - - Before the 31st of July 2023, contributions were made under the GNU - General Public License version 2 or any later version (GPLv2+). +The MDAnalyis library is packaged under the terms of the GNU Lesser +General Public License version 3 or any later version (LGPLv3+). + +Developer contributions to the library are, unless otherwise stated, +made under the GNU Lesser General Public License version 2.1 or any +later version (LGPLv2.1+). The MDAnalysis library also includes external codes provided under licenses -compatible with the terms of the GNU General Public License version 3 or any -later version (GPLv3+). These are outlined under "Licenses of components of -MDAnalysis". +compatible with the terms of the GNU Lesser General Public License version +3 or any later version (LGPLv3+). These are outlined under +"Licenses of components of MDAnalysis". ========================================================================== Licenses of components of MDAnalysis @@ -529,6 +524,175 @@ necessary. Here is a sample; alter the names: That's all there is to it! +========================================================================== + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + ========================================================================== GNU GENERAL PUBLIC LICENSE @@ -1206,351 +1370,6 @@ the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . - -========================================================================== - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. - - ========================================================================== Gromacs xdrfile library for reading XTC/TRR trajectories diff --git a/README.rst b/README.rst index 339829ef056..3759747b18e 100644 --- a/README.rst +++ b/README.rst @@ -78,10 +78,10 @@ described in the `Installation Quick Start`_. **Source code** is hosted in a git repository at https://github.com/MDAnalysis/mdanalysis and is packaged under the -GNU General Public License, version 3 or any later version. Invidiual -source code components are provided under a mixture of GPLv3+ compatible -licenses, including LGPLv2.1+ and GPLv2+. Please see the file LICENSE_ -for more information. +GNU Lesser General Public License, version 3 or any later version (LGPLv3+). +Invidiual source code components are provided under the +GNU Lesser General Public License, version 2.1 or any later version (LGPLv2.1+). +Please see the file LICENSE_ for more information. Contributing diff --git a/package/AUTHORS b/package/AUTHORS index 9728e7ac531..5871ec8f74f 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -249,7 +249,7 @@ Chronological list of authors External code ------------- -External code (under a GPL-compatible licence) was obtained from +External code (under a LGPL-compatible licence) was obtained from various sources. The authors (as far as we know them) are listed here. xdrfile @@ -261,9 +261,9 @@ xdrfile The Gromacs libxdrfile (LGPL-licensed) was used before MDAnalysis version 0.8.0. Between MDAnalysis versions 0.8.0 and 0.13.0 libxdrfile was replaced by - libxdrfile2, our GPLv2 enhanced derivative of libxdrfile. + libxdrfile2, our LGPLv2.1+ enhanced derivative of libxdrfile. Since version 0.14.0 xdr enhanecments were rebased onto Gromacs' - xdrfile 1.1.4 code (now BSD-licensed). Our contributions remain GPLv2 + xdrfile 1.1.4 code (now BSD-licensed). Our contributions remain LGPLv2.1+ and were split into files xtc_seek.c, trr_seek.c, xtc_seek.h, and trr_seek.h, for clarity (xdrfile 1.1.4 code is distributed with minor modifications). Also for clarity we now name the resulting enhanced diff --git a/package/CHANGELOG b/package/CHANGELOG index 83dbbc4d0c3..9a6026971df 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -97,6 +97,8 @@ Enhancements DOI 10.1021/acs.jpcb.7b11988. (Issue #2039, PR #4524) Changes + * Relicense code contributions from GPLv2+ to LGPLv2.1+ + and the package from GPLv3+ to LGPLv3+ (PR #4794) * only use distopia < 0.3.0 due to API changes (Issue #4739) * The `fetch_mmtf` method has been removed as the REST API service for MMTF files has ceased to exist (Issue #4634) diff --git a/package/LICENSE b/package/LICENSE index 8437675a953..8d0eeadd954 100644 --- a/package/LICENSE +++ b/package/LICENSE @@ -1,226 +1,417 @@ ========================================================================== -Licenses of components of MDAnalysis +Licensing of the MDAnalysis library ========================================================================== +The MDAnalyis library is packaged under the terms of the GNU Lesser +General Public License version 3 or any later version (LGPLv3+). + +Developer contributions to the library are, unless otherwise stated, +made under the GNU Lesser General Public License version 2.1 or any +later version (LGPLv2.1+). + +The MDAnalysis library also includes external codes provided under licenses +compatible with the terms of the GNU Lesser General Public License version +3 or any later version (LGPLv3+). These are outlined under +"Licenses of components of MDAnalysis". - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 +========================================================================== +Licenses of components of MDAnalysis +========================================================================== - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. The precise terms and conditions for copying, distribution and -modification follow. +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. - GNU GENERAL PUBLIC LICENSE + GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - a) You must cause the modified files to carry prominent notices + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, +identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of +on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. +entire whole, and thus to each and every part regardless of who wrote +it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or -collective works based on the Program. +collective works based on the Library. -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not compelled to copy the source along with the object code. - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are +distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying -the Program or works based on it. +the Library or works based on it. - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to +You are not responsible for enforcing compliance by third parties with this License. - 7. If, as a consequence of a court judgment or allegation of patent + 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. +refrain entirely from distribution of the Library. -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is +integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that @@ -231,60 +422,902 @@ impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - 8. If the distribution and/or use of the Program is restricted in + 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + +========================================================================== + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + +========================================================================== + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it @@ -292,15 +1325,15 @@ free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least +state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) - This program is free software; you can redistribute it and/or modify + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or + the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, @@ -309,41 +1342,33 @@ the "copyright" line and a pointer to where the full notice is found. GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. ========================================================================== @@ -559,6 +1584,33 @@ PyQCPROT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================================================== + +DSSP module code for protein secondary structure assignment + - analysis/dssp/pydssp_numpy.py + +MIT License + +Copyright (c) 2022 Shintaro Minami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ========================================================================== MDAnalysis logo (see doc/sphinx/source/logos) diff --git a/package/MDAnalysis/__init__.py b/package/MDAnalysis/__init__.py index ca11be4bdf2..69d992afef8 100644 --- a/package/MDAnalysis/__init__.py +++ b/package/MDAnalysis/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index 25450b759b9..056c7899826 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/align.py b/package/MDAnalysis/analysis/align.py index 2bc80042b69..fd7f15a8226 100644 --- a/package/MDAnalysis/analysis/align.py +++ b/package/MDAnalysis/analysis/align.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Author: Oliver Beckstein, Joshua Adelman :Year: 2010--2013 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ The module contains functions to fit a target structure to a reference structure. They use the fast QCP algorithm to calculate the root mean @@ -1669,4 +1669,4 @@ def get_atoms_byres(g, match_mask=np.logical_not(mismatch_mask)): logger.error(errmsg) raise SelectionError(errmsg) - return ag1, ag2 \ No newline at end of file + return ag1, ag2 diff --git a/package/MDAnalysis/analysis/atomicdistances.py b/package/MDAnalysis/analysis/atomicdistances.py index 59638dbaf8c..1860d3285b1 100644 --- a/package/MDAnalysis/analysis/atomicdistances.py +++ b/package/MDAnalysis/analysis/atomicdistances.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -29,7 +29,7 @@ :Author: Xu Hong Chen :Year: 2023 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ This module provides a class to efficiently compute distances between two groups of atoms with an equal number of atoms over a trajectory. diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 930f4fa90c2..f940af58e82 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/bat.py b/package/MDAnalysis/analysis/bat.py index c8a908f9ea5..9c1995f7ccc 100644 --- a/package/MDAnalysis/analysis/bat.py +++ b/package/MDAnalysis/analysis/bat.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -25,7 +25,7 @@ :Author: Soohaeng Yoo Willow and David Minh :Year: 2020 -:Copyright: GNU Public License, v2 or any higher version +:Copyright: Lesser GNU Public License, v2.1 or any higher version .. versionadded:: 2.0.0 diff --git a/package/MDAnalysis/analysis/contacts.py b/package/MDAnalysis/analysis/contacts.py index 7d6804f1a73..7a7e195f09a 100644 --- a/package/MDAnalysis/analysis/contacts.py +++ b/package/MDAnalysis/analysis/contacts.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/data/filenames.py b/package/MDAnalysis/analysis/data/filenames.py index c1149bc4cb7..a747450b86d 100644 --- a/package/MDAnalysis/analysis/data/filenames.py +++ b/package/MDAnalysis/analysis/data/filenames.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index d5c62866768..8f3f0b33647 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -30,7 +30,7 @@ :Author: Oliver Beckstein :Year: 2011 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ The module provides classes and functions to generate and represent volumetric data, in particular densities. diff --git a/package/MDAnalysis/analysis/dielectric.py b/package/MDAnalysis/analysis/dielectric.py index d28bb376448..4f14eb88074 100644 --- a/package/MDAnalysis/analysis/dielectric.py +++ b/package/MDAnalysis/analysis/dielectric.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Authors: Mattia Felice Palermo, Philip Loche :Year: 2022 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ """ import numpy as np diff --git a/package/MDAnalysis/analysis/diffusionmap.py b/package/MDAnalysis/analysis/diffusionmap.py index 1c63357a160..65330196ec2 100644 --- a/package/MDAnalysis/analysis/diffusionmap.py +++ b/package/MDAnalysis/analysis/diffusionmap.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Authors: Eugen Hruska, John Detlefs :Year: 2016 -:Copyright: GNU Public License v2 +:Copyright: Lesser GNU Public License v2.1+ This module contains the non-linear dimension reduction method diffusion map. The eigenvectors of a diffusion matrix represent the 'collective coordinates' diff --git a/package/MDAnalysis/analysis/dihedrals.py b/package/MDAnalysis/analysis/dihedrals.py index 56b95fc42c3..c6a5585f7a0 100644 --- a/package/MDAnalysis/analysis/dihedrals.py +++ b/package/MDAnalysis/analysis/dihedrals.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -25,7 +25,7 @@ :Author: Henry Mull :Year: 2018 -:Copyright: GNU Public License v2 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.19.0 diff --git a/package/MDAnalysis/analysis/distances.py b/package/MDAnalysis/analysis/distances.py index ae81c8941a3..9e81de95688 100644 --- a/package/MDAnalysis/analysis/distances.py +++ b/package/MDAnalysis/analysis/distances.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/__init__.py b/package/MDAnalysis/analysis/encore/__init__.py index 34b70dd28d0..49095ecfd5c 100644 --- a/package/MDAnalysis/analysis/encore/__init__.py +++ b/package/MDAnalysis/analysis/encore/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/bootstrap.py b/package/MDAnalysis/analysis/encore/bootstrap.py index 2d50d486dcb..80761a8fdd2 100644 --- a/package/MDAnalysis/analysis/encore/bootstrap.py +++ b/package/MDAnalysis/analysis/encore/bootstrap.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py b/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py index 87879ba1077..e4b7070dcac 100644 --- a/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py +++ b/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py b/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py index df13aaff570..8071d5eac4a 100644 --- a/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py +++ b/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/clustering/__init__.py b/package/MDAnalysis/analysis/encore/clustering/__init__.py index f9fef60a7b1..33f828ce5f4 100644 --- a/package/MDAnalysis/analysis/encore/clustering/__init__.py +++ b/package/MDAnalysis/analysis/encore/clustering/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/clustering/affinityprop.pyx b/package/MDAnalysis/analysis/encore/clustering/affinityprop.pyx index be3d00dca79..9b168ba2e45 100644 --- a/package/MDAnalysis/analysis/encore/clustering/affinityprop.pyx +++ b/package/MDAnalysis/analysis/encore/clustering/affinityprop.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/clustering/cluster.py b/package/MDAnalysis/analysis/encore/clustering/cluster.py index 0ad713775d6..1c43f2cfd75 100644 --- a/package/MDAnalysis/analysis/encore/clustering/cluster.py +++ b/package/MDAnalysis/analysis/encore/clustering/cluster.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/clustering/include/ap.h b/package/MDAnalysis/analysis/encore/clustering/include/ap.h index a3b1e538cf9..9f09c40557e 100644 --- a/package/MDAnalysis/analysis/encore/clustering/include/ap.h +++ b/package/MDAnalysis/analysis/encore/clustering/include/ap.h @@ -5,7 +5,7 @@ Copyright (c) 2006-2016 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 + Released under the Lesser GNU Public Licence, v2.1 or any higher version Please cite your use of MDAnalysis in published work: diff --git a/package/MDAnalysis/analysis/encore/clustering/src/ap.c b/package/MDAnalysis/analysis/encore/clustering/src/ap.c index 6f42037dce7..53c806f8f99 100644 --- a/package/MDAnalysis/analysis/encore/clustering/src/ap.c +++ b/package/MDAnalysis/analysis/encore/clustering/src/ap.c @@ -5,7 +5,7 @@ Copyright (c) 2006-2016 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 + Released under the Lesser GNU Public Licence, v2.1 or any higher version Please cite your use of MDAnalysis in published work: diff --git a/package/MDAnalysis/analysis/encore/confdistmatrix.py b/package/MDAnalysis/analysis/encore/confdistmatrix.py index 2f3e83b94ff..739d715865f 100644 --- a/package/MDAnalysis/analysis/encore/confdistmatrix.py +++ b/package/MDAnalysis/analysis/encore/confdistmatrix.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/covariance.py b/package/MDAnalysis/analysis/encore/covariance.py index e6768bf698d..5c7b3b363a5 100644 --- a/package/MDAnalysis/analysis/encore/covariance.py +++ b/package/MDAnalysis/analysis/encore/covariance.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/cutils.pyx b/package/MDAnalysis/analysis/encore/cutils.pyx index 08a2ebc7944..031f0a1de5e 100644 --- a/package/MDAnalysis/analysis/encore/cutils.pyx +++ b/package/MDAnalysis/analysis/encore/cutils.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py index cef202843d7..50349960bdd 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py index 99f2a14c999..fefd1b85acd 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/include/spe.h b/package/MDAnalysis/analysis/encore/dimensionality_reduction/include/spe.h index ffb2c4c38f8..10b015bc32b 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/include/spe.h +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/include/spe.h @@ -5,7 +5,7 @@ Copyright (c) 2006-2016 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 + Released under the Lesser GNU Public Licence, v2.1 or any higher version Please cite your use of MDAnalysis in published work: diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py index 1a35548fbf6..281d681203f 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c b/package/MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c index f3ae089a7c2..1589eefece6 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c @@ -5,7 +5,7 @@ Copyright (c) 2006-2016 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 + Released under the Lesser GNU Public Licence, v2.1 or any higher version Please cite your use of MDAnalysis in published work: diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed.pyx b/package/MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed.pyx index a65eb492a05..64569aec2b0 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed.pyx +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/similarity.py b/package/MDAnalysis/analysis/encore/similarity.py index 2f41d233d48..4fe6f0e35a5 100644 --- a/package/MDAnalysis/analysis/encore/similarity.py +++ b/package/MDAnalysis/analysis/encore/similarity.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/encore/utils.py b/package/MDAnalysis/analysis/encore/utils.py index 399eedd320c..13a028f45c4 100644 --- a/package/MDAnalysis/analysis/encore/utils.py +++ b/package/MDAnalysis/analysis/encore/utils.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index 510fb887d01..ee42bc165ef 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -30,7 +30,7 @@ :Author: Benjamin Hall :Year: 2011 -:Copyright: GNU Public License v2 or later +:Copyright: Lesser GNU Public License v2.1 or later Analyse a trajectory using elastic network models, following the approach of diff --git a/package/MDAnalysis/analysis/hbonds/__init__.py b/package/MDAnalysis/analysis/hbonds/__init__.py index 8dc8091e969..b74b96638b4 100644 --- a/package/MDAnalysis/analysis/hbonds/__init__.py +++ b/package/MDAnalysis/analysis/hbonds/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py index a09f655d3d8..a5204236a07 100644 --- a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py +++ b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -25,7 +25,7 @@ :Author: Richard J. Gowers :Year: 2014 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.9.0 diff --git a/package/MDAnalysis/analysis/helix_analysis.py b/package/MDAnalysis/analysis/helix_analysis.py index da57fbc1ab6..9c287fb7508 100644 --- a/package/MDAnalysis/analysis/helix_analysis.py +++ b/package/MDAnalysis/analysis/helix_analysis.py @@ -5,7 +5,7 @@ # Copyright (c) 2006-2020 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Author: Lily Wang :Year: 2020 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 2.0.0 diff --git a/package/MDAnalysis/analysis/hole2/__init__.py b/package/MDAnalysis/analysis/hole2/__init__.py index 8bcb8575781..d09359f0917 100644 --- a/package/MDAnalysis/analysis/hole2/__init__.py +++ b/package/MDAnalysis/analysis/hole2/__init__.py @@ -5,7 +5,7 @@ # Copyright (c) 2006-2020 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Author: Lily Wang :Year: 2020 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 1.0.0 diff --git a/package/MDAnalysis/analysis/hydrogenbonds/__init__.py b/package/MDAnalysis/analysis/hydrogenbonds/__init__.py index 9476d064138..7bb75ea625f 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/__init__.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py b/package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py index 3bf9d5c27a9..95cf2d00246 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Author: Paul Smith :Year: 2019 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 1.0.0 diff --git a/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py b/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py index 39da128719e..51fb1bd19aa 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Author: Richard J. Gowers :Year: 2014 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.9.0 diff --git a/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py b/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py index 71382aa5d22..69f281b4f75 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Author: Zhiyi Wu :Year: 2017-2018 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ :Maintainer: Zhiyi Wu , `@xiki-tempula`_ on GitHub diff --git a/package/MDAnalysis/analysis/leaflet.py b/package/MDAnalysis/analysis/leaflet.py index a40ff34aed0..5ea0d362a90 100644 --- a/package/MDAnalysis/analysis/leaflet.py +++ b/package/MDAnalysis/analysis/leaflet.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/legacy/__init__.py b/package/MDAnalysis/analysis/legacy/__init__.py index 455f50eed78..5a40e52efff 100644 --- a/package/MDAnalysis/analysis/legacy/__init__.py +++ b/package/MDAnalysis/analysis/legacy/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/legacy/x3dna.py b/package/MDAnalysis/analysis/legacy/x3dna.py index 9dc69a46702..46a2f5a8f60 100644 --- a/package/MDAnalysis/analysis/legacy/x3dna.py +++ b/package/MDAnalysis/analysis/legacy/x3dna.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Author: Elizabeth Denning :Year: 2013-2014 -:Copyright: GNU Public License v2 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.8 .. versionchanged:: 0.16.0 diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 08a3728d378..8970d68d8a0 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/msd.py b/package/MDAnalysis/analysis/msd.py index 659b0d71e96..4515ed40983 100644 --- a/package/MDAnalysis/analysis/msd.py +++ b/package/MDAnalysis/analysis/msd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Authors: Hugo MacDermott-Opeskin :Year: 2020 -:Copyright: GNU Public License v2 +:Copyright: Lesser GNU Public License v2.1+ This module implements the calculation of Mean Squared Displacements (MSDs) by the Einstein relation. MSDs can be used to characterize the speed at diff --git a/package/MDAnalysis/analysis/nucleicacids.py b/package/MDAnalysis/analysis/nucleicacids.py index 0eccd039ba4..9bdbe8d1124 100644 --- a/package/MDAnalysis/analysis/nucleicacids.py +++ b/package/MDAnalysis/analysis/nucleicacids.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/nuclinfo.py b/package/MDAnalysis/analysis/nuclinfo.py index 4baa0ba5bf7..0a8a3f6aa48 100644 --- a/package/MDAnalysis/analysis/nuclinfo.py +++ b/package/MDAnalysis/analysis/nuclinfo.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Author: Elizabeth Denning :Year: 2011 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ The module provides functions to analyze nucleic acid structures, in particular diff --git a/package/MDAnalysis/analysis/pca.py b/package/MDAnalysis/analysis/pca.py index d9b88cc8e5d..cbf4cb588c8 100644 --- a/package/MDAnalysis/analysis/pca.py +++ b/package/MDAnalysis/analysis/pca.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Authors: John Detlefs :Year: 2016 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.16.0 diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index d3a6bf29de4..a38cf68daac 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -28,7 +28,7 @@ :Author: Richard J. Gowers :Year: 2015, 2018 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ This module contains various commonly used tools in analysing polymers. """ diff --git a/package/MDAnalysis/analysis/psa.py b/package/MDAnalysis/analysis/psa.py index daaff4296cd..b93ea90c64b 100644 --- a/package/MDAnalysis/analysis/psa.py +++ b/package/MDAnalysis/analysis/psa.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Author: Sean Seyler :Year: 2015 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.10.0 diff --git a/package/MDAnalysis/analysis/rdf.py b/package/MDAnalysis/analysis/rdf.py index 9be624f2a06..891be116ca5 100644 --- a/package/MDAnalysis/analysis/rdf.py +++ b/package/MDAnalysis/analysis/rdf.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index b8dcb97065f..f33d1b761fb 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Author: Oliver Beckstein, David L. Dotson, John Detlefs :Year: 2016 -:Copyright: GNU Public License v2 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.7.7 .. versionchanged:: 0.11.0 diff --git a/package/MDAnalysis/analysis/waterdynamics.py b/package/MDAnalysis/analysis/waterdynamics.py index df6867a3bf3..2c7a1c4bec3 100644 --- a/package/MDAnalysis/analysis/waterdynamics.py +++ b/package/MDAnalysis/analysis/waterdynamics.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Author: Alejandro Bernardin :Year: 2014-2015 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 0.11.0 diff --git a/package/MDAnalysis/auxiliary/EDR.py b/package/MDAnalysis/auxiliary/EDR.py index fe9173b7528..37f4394c24d 100644 --- a/package/MDAnalysis/auxiliary/EDR.py +++ b/package/MDAnalysis/auxiliary/EDR.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/auxiliary/XVG.py b/package/MDAnalysis/auxiliary/XVG.py index 014831f2e4e..c690b414059 100644 --- a/package/MDAnalysis/auxiliary/XVG.py +++ b/package/MDAnalysis/auxiliary/XVG.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/auxiliary/__init__.py b/package/MDAnalysis/auxiliary/__init__.py index dff77786744..5e168003d27 100644 --- a/package/MDAnalysis/auxiliary/__init__.py +++ b/package/MDAnalysis/auxiliary/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/auxiliary/base.py b/package/MDAnalysis/auxiliary/base.py index 3dc1325a636..58f9219c002 100644 --- a/package/MDAnalysis/auxiliary/base.py +++ b/package/MDAnalysis/auxiliary/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/auxiliary/core.py b/package/MDAnalysis/auxiliary/core.py index 6dc3124d57a..e62109e1517 100644 --- a/package/MDAnalysis/auxiliary/core.py +++ b/package/MDAnalysis/auxiliary/core.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/OpenMM.py b/package/MDAnalysis/converters/OpenMM.py index 11ba70498f1..227a99ebe59 100644 --- a/package/MDAnalysis/converters/OpenMM.py +++ b/package/MDAnalysis/converters/OpenMM.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/OpenMMParser.py b/package/MDAnalysis/converters/OpenMMParser.py index b3402c448eb..a8b2866085e 100644 --- a/package/MDAnalysis/converters/OpenMMParser.py +++ b/package/MDAnalysis/converters/OpenMMParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/ParmEd.py b/package/MDAnalysis/converters/ParmEd.py index cc2e7a4cc52..b808f6b1484 100644 --- a/package/MDAnalysis/converters/ParmEd.py +++ b/package/MDAnalysis/converters/ParmEd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/ParmEdParser.py b/package/MDAnalysis/converters/ParmEdParser.py index 86de585fe53..31ed9bee410 100644 --- a/package/MDAnalysis/converters/ParmEdParser.py +++ b/package/MDAnalysis/converters/ParmEdParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/RDKit.py b/package/MDAnalysis/converters/RDKit.py index b6d806df4c0..85f55b7900d 100644 --- a/package/MDAnalysis/converters/RDKit.py +++ b/package/MDAnalysis/converters/RDKit.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/RDKitParser.py b/package/MDAnalysis/converters/RDKitParser.py index 6bca57a43fe..24c730ac061 100644 --- a/package/MDAnalysis/converters/RDKitParser.py +++ b/package/MDAnalysis/converters/RDKitParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/__init__.py b/package/MDAnalysis/converters/__init__.py index bd6286afd0d..11612cfb790 100644 --- a/package/MDAnalysis/converters/__init__.py +++ b/package/MDAnalysis/converters/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/converters/base.py b/package/MDAnalysis/converters/base.py index 99d194e53a6..234d4f7da4e 100644 --- a/package/MDAnalysis/converters/base.py +++ b/package/MDAnalysis/converters/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/CRD.py b/package/MDAnalysis/coordinates/CRD.py index 89322ed771c..c57d9dea0da 100644 --- a/package/MDAnalysis/coordinates/CRD.py +++ b/package/MDAnalysis/coordinates/CRD.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/DCD.py b/package/MDAnalysis/coordinates/DCD.py index b28c12ca4c2..88c8d76b3e2 100644 --- a/package/MDAnalysis/coordinates/DCD.py +++ b/package/MDAnalysis/coordinates/DCD.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/DLPoly.py b/package/MDAnalysis/coordinates/DLPoly.py index 9e5f811a51c..aad63977805 100644 --- a/package/MDAnalysis/coordinates/DLPoly.py +++ b/package/MDAnalysis/coordinates/DLPoly.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/DMS.py b/package/MDAnalysis/coordinates/DMS.py index ad2b0991845..1d207ca2bd9 100644 --- a/package/MDAnalysis/coordinates/DMS.py +++ b/package/MDAnalysis/coordinates/DMS.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/FHIAIMS.py b/package/MDAnalysis/coordinates/FHIAIMS.py index ce5bf8259e7..193d570560e 100644 --- a/package/MDAnalysis/coordinates/FHIAIMS.py +++ b/package/MDAnalysis/coordinates/FHIAIMS.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/GMS.py b/package/MDAnalysis/coordinates/GMS.py index 99b65517112..6db412461df 100644 --- a/package/MDAnalysis/coordinates/GMS.py +++ b/package/MDAnalysis/coordinates/GMS.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/GRO.py b/package/MDAnalysis/coordinates/GRO.py index aff46e5b86c..721fbe096f9 100644 --- a/package/MDAnalysis/coordinates/GRO.py +++ b/package/MDAnalysis/coordinates/GRO.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/GSD.py b/package/MDAnalysis/coordinates/GSD.py index 2dcdf9bac78..f08a3872213 100644 --- a/package/MDAnalysis/coordinates/GSD.py +++ b/package/MDAnalysis/coordinates/GSD.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/H5MD.py b/package/MDAnalysis/coordinates/H5MD.py index 48283113f43..511f904fa5a 100644 --- a/package/MDAnalysis/coordinates/H5MD.py +++ b/package/MDAnalysis/coordinates/H5MD.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/INPCRD.py b/package/MDAnalysis/coordinates/INPCRD.py index 20bf50472ff..9b90f6301e1 100644 --- a/package/MDAnalysis/coordinates/INPCRD.py +++ b/package/MDAnalysis/coordinates/INPCRD.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/LAMMPS.py b/package/MDAnalysis/coordinates/LAMMPS.py index 5099c742fcb..2a91c44e331 100644 --- a/package/MDAnalysis/coordinates/LAMMPS.py +++ b/package/MDAnalysis/coordinates/LAMMPS.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/MMTF.py b/package/MDAnalysis/coordinates/MMTF.py index 4fdb089f07a..00ef4774378 100644 --- a/package/MDAnalysis/coordinates/MMTF.py +++ b/package/MDAnalysis/coordinates/MMTF.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/MOL2.py b/package/MDAnalysis/coordinates/MOL2.py index 0039d24efdb..104283e897f 100644 --- a/package/MDAnalysis/coordinates/MOL2.py +++ b/package/MDAnalysis/coordinates/MOL2.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/NAMDBIN.py b/package/MDAnalysis/coordinates/NAMDBIN.py index 01587967963..b9425f18f98 100644 --- a/package/MDAnalysis/coordinates/NAMDBIN.py +++ b/package/MDAnalysis/coordinates/NAMDBIN.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/PDB.py b/package/MDAnalysis/coordinates/PDB.py index 5e9530cac8a..82cfb8ef003 100644 --- a/package/MDAnalysis/coordinates/PDB.py +++ b/package/MDAnalysis/coordinates/PDB.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/PDBQT.py b/package/MDAnalysis/coordinates/PDBQT.py index 41c1e97fc93..f0913d6e049 100644 --- a/package/MDAnalysis/coordinates/PDBQT.py +++ b/package/MDAnalysis/coordinates/PDBQT.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/PQR.py b/package/MDAnalysis/coordinates/PQR.py index 8ae92622e46..c93d783dc68 100644 --- a/package/MDAnalysis/coordinates/PQR.py +++ b/package/MDAnalysis/coordinates/PQR.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/ParmEd.py b/package/MDAnalysis/coordinates/ParmEd.py index 9d6af106e63..af29340a5d2 100644 --- a/package/MDAnalysis/coordinates/ParmEd.py +++ b/package/MDAnalysis/coordinates/ParmEd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/TNG.py b/package/MDAnalysis/coordinates/TNG.py index 7a44be3518b..a5d868360f0 100644 --- a/package/MDAnalysis/coordinates/TNG.py +++ b/package/MDAnalysis/coordinates/TNG.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/TRC.py b/package/MDAnalysis/coordinates/TRC.py index e1d29d2a84c..5d92db1af8c 100644 --- a/package/MDAnalysis/coordinates/TRC.py +++ b/package/MDAnalysis/coordinates/TRC.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/TRJ.py b/package/MDAnalysis/coordinates/TRJ.py index ae59073dbc5..6a1cf82c487 100644 --- a/package/MDAnalysis/coordinates/TRJ.py +++ b/package/MDAnalysis/coordinates/TRJ.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/TRR.py b/package/MDAnalysis/coordinates/TRR.py index 1eca1bdd40c..24d37af66de 100644 --- a/package/MDAnalysis/coordinates/TRR.py +++ b/package/MDAnalysis/coordinates/TRR.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/TRZ.py b/package/MDAnalysis/coordinates/TRZ.py index f0c96c53b8f..37bd6a0c065 100644 --- a/package/MDAnalysis/coordinates/TRZ.py +++ b/package/MDAnalysis/coordinates/TRZ.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/TXYZ.py b/package/MDAnalysis/coordinates/TXYZ.py index 0f659c63ef0..42652697583 100644 --- a/package/MDAnalysis/coordinates/TXYZ.py +++ b/package/MDAnalysis/coordinates/TXYZ.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/XDR.py b/package/MDAnalysis/coordinates/XDR.py index 0319f437ffa..6fe75982cc4 100644 --- a/package/MDAnalysis/coordinates/XDR.py +++ b/package/MDAnalysis/coordinates/XDR.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/XTC.py b/package/MDAnalysis/coordinates/XTC.py index be473669347..0555cbfbc03 100644 --- a/package/MDAnalysis/coordinates/XTC.py +++ b/package/MDAnalysis/coordinates/XTC.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/XYZ.py b/package/MDAnalysis/coordinates/XYZ.py index 20a2f75a886..c4d0a695c4c 100644 --- a/package/MDAnalysis/coordinates/XYZ.py +++ b/package/MDAnalysis/coordinates/XYZ.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/__init__.py b/package/MDAnalysis/coordinates/__init__.py index 9b6a7121bc9..602621e5ad3 100644 --- a/package/MDAnalysis/coordinates/__init__.py +++ b/package/MDAnalysis/coordinates/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/base.py b/package/MDAnalysis/coordinates/base.py index dda4a61a7ce..61afa29e7da 100644 --- a/package/MDAnalysis/coordinates/base.py +++ b/package/MDAnalysis/coordinates/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/chain.py b/package/MDAnalysis/coordinates/chain.py index 0c09a596d95..245b760acd9 100644 --- a/package/MDAnalysis/coordinates/chain.py +++ b/package/MDAnalysis/coordinates/chain.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/chemfiles.py b/package/MDAnalysis/coordinates/chemfiles.py index a7e2bd828c5..f7a6ebb32c2 100644 --- a/package/MDAnalysis/coordinates/chemfiles.py +++ b/package/MDAnalysis/coordinates/chemfiles.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/core.py b/package/MDAnalysis/coordinates/core.py index 45eb7659382..fe87cd005a3 100644 --- a/package/MDAnalysis/coordinates/core.py +++ b/package/MDAnalysis/coordinates/core.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/memory.py b/package/MDAnalysis/coordinates/memory.py index d2521b9a21b..288ceceac3b 100644 --- a/package/MDAnalysis/coordinates/memory.py +++ b/package/MDAnalysis/coordinates/memory.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Author: Wouter Boomsma :Year: 2016 -:Copyright: GNU Public License v2 +:Copyright: Lesser GNU Public License v2.1+ :Maintainer: Wouter Boomsma , wouterboomsma on github diff --git a/package/MDAnalysis/coordinates/null.py b/package/MDAnalysis/coordinates/null.py index f27fc2088e8..71b490aad24 100644 --- a/package/MDAnalysis/coordinates/null.py +++ b/package/MDAnalysis/coordinates/null.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/coordinates/timestep.pyx b/package/MDAnalysis/coordinates/timestep.pyx index f2649d44c63..ee12feae375 100644 --- a/package/MDAnalysis/coordinates/timestep.pyx +++ b/package/MDAnalysis/coordinates/timestep.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/__init__.py b/package/MDAnalysis/core/__init__.py index 7345700158d..254cc6fead1 100644 --- a/package/MDAnalysis/core/__init__.py +++ b/package/MDAnalysis/core/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/_get_readers.py b/package/MDAnalysis/core/_get_readers.py index 3ecdcf548d5..a1d603965e7 100644 --- a/package/MDAnalysis/core/_get_readers.py +++ b/package/MDAnalysis/core/_get_readers.py @@ -5,7 +5,7 @@ # Copyright (c) 2006-2015 Naveen Michaud-Agrawal, Elizabeth J. Denning, Oliver Beckstein # and contributors (see AUTHORS for the full list) # -# Released under the GNU Public Licence, v2 or any higher version +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. diff --git a/package/MDAnalysis/core/accessors.py b/package/MDAnalysis/core/accessors.py index 40ec0916f77..3338d009702 100644 --- a/package/MDAnalysis/core/accessors.py +++ b/package/MDAnalysis/core/accessors.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 7c9a3650dfd..e8bf30ba110 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index 2edbf79e01b..591c074030d 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/topology.py b/package/MDAnalysis/core/topology.py index 2d97c5e789b..899260721c3 100644 --- a/package/MDAnalysis/core/topology.py +++ b/package/MDAnalysis/core/topology.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index ef5897268c9..d1b103e3410 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/topologyobjects.py b/package/MDAnalysis/core/topologyobjects.py index 5d2e37965e4..436ecc5dd5d 100644 --- a/package/MDAnalysis/core/topologyobjects.py +++ b/package/MDAnalysis/core/topologyobjects.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index a2bc60c25f9..7fed2cde8c6 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/exceptions.py b/package/MDAnalysis/exceptions.py index 3c42c617207..dd4d2f54f11 100644 --- a/package/MDAnalysis/exceptions.py +++ b/package/MDAnalysis/exceptions.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/guesser/tables.py b/package/MDAnalysis/guesser/tables.py index 5e373616b7e..fb66ce6c133 100644 --- a/package/MDAnalysis/guesser/tables.py +++ b/package/MDAnalysis/guesser/tables.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/NeighborSearch.py b/package/MDAnalysis/lib/NeighborSearch.py index 572973afcd1..d09284773ec 100644 --- a/package/MDAnalysis/lib/NeighborSearch.py +++ b/package/MDAnalysis/lib/NeighborSearch.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/__init__.py b/package/MDAnalysis/lib/__init__.py index 2ba03b03274..a5bc6f8e877 100644 --- a/package/MDAnalysis/lib/__init__.py +++ b/package/MDAnalysis/lib/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/_augment.pyx b/package/MDAnalysis/lib/_augment.pyx index d8a976ccf1f..31457920333 100644 --- a/package/MDAnalysis/lib/_augment.pyx +++ b/package/MDAnalysis/lib/_augment.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/_cutil.pyx b/package/MDAnalysis/lib/_cutil.pyx index 549c29df5d7..5c447eada86 100644 --- a/package/MDAnalysis/lib/_cutil.pyx +++ b/package/MDAnalysis/lib/_cutil.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/_distopia.py b/package/MDAnalysis/lib/_distopia.py index 5344393fe14..c2564bc2d23 100644 --- a/package/MDAnalysis/lib/_distopia.py +++ b/package/MDAnalysis/lib/_distopia.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/c_distances.pyx b/package/MDAnalysis/lib/c_distances.pyx index c4e33ae263a..1b887b1885c 100644 --- a/package/MDAnalysis/lib/c_distances.pyx +++ b/package/MDAnalysis/lib/c_distances.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/c_distances_openmp.pyx b/package/MDAnalysis/lib/c_distances_openmp.pyx index c75c7c12780..8e3c9da8da2 100644 --- a/package/MDAnalysis/lib/c_distances_openmp.pyx +++ b/package/MDAnalysis/lib/c_distances_openmp.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/correlations.py b/package/MDAnalysis/lib/correlations.py index bab60e30d51..1ce0338c676 100644 --- a/package/MDAnalysis/lib/correlations.py +++ b/package/MDAnalysis/lib/correlations.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -27,7 +27,7 @@ :Authors: Paul Smith & Mateusz Bieniek :Year: 2020 -:Copyright: GNU Public License v2 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 1.0.0 diff --git a/package/MDAnalysis/lib/distances.py b/package/MDAnalysis/lib/distances.py index 062b212ea30..a6c30abacd0 100644 --- a/package/MDAnalysis/lib/distances.py +++ b/package/MDAnalysis/lib/distances.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/__init__.py b/package/MDAnalysis/lib/formats/__init__.py index 770b88b3fad..cf484ea4778 100644 --- a/package/MDAnalysis/lib/formats/__init__.py +++ b/package/MDAnalysis/lib/formats/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/cython_util.pxd b/package/MDAnalysis/lib/formats/cython_util.pxd index 0689346647d..c1ff2f45487 100644 --- a/package/MDAnalysis/lib/formats/cython_util.pxd +++ b/package/MDAnalysis/lib/formats/cython_util.pxd @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/cython_util.pyx b/package/MDAnalysis/lib/formats/cython_util.pyx index 26c694a40fd..93884df371b 100644 --- a/package/MDAnalysis/lib/formats/cython_util.pyx +++ b/package/MDAnalysis/lib/formats/cython_util.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/include/trr_seek.h b/package/MDAnalysis/lib/formats/include/trr_seek.h index a571236ea03..3927b695942 100644 --- a/package/MDAnalysis/lib/formats/include/trr_seek.h +++ b/package/MDAnalysis/lib/formats/include/trr_seek.h @@ -5,7 +5,7 @@ * Copyright (c) 2006-2015 Naveen Michaud-Agrawal, Elizabeth J. Denning, Oliver * Beckstein and contributors (see AUTHORS for the full list) * - * Released under the GNU Public Licence, v2 or any higher version + * Released under the Lesser GNU Public Licence, v2.1 or any higher version * * Please cite your use of MDAnalysis in published work: * diff --git a/package/MDAnalysis/lib/formats/include/xtc_seek.h b/package/MDAnalysis/lib/formats/include/xtc_seek.h index a3efbb65b88..a29320f3100 100644 --- a/package/MDAnalysis/lib/formats/include/xtc_seek.h +++ b/package/MDAnalysis/lib/formats/include/xtc_seek.h @@ -5,7 +5,7 @@ * Copyright (c) 2006-2015 Naveen Michaud-Agrawal, Elizabeth J. Denning, Oliver * Beckstein and contributors (see AUTHORS for the full list) * - * Released under the GNU Public Licence, v2 or any higher version + * Released under the Lesser GNU Public Licence, v2.1 or any higher version * * Please cite your use of MDAnalysis in published work: * diff --git a/package/MDAnalysis/lib/formats/libdcd.pxd b/package/MDAnalysis/lib/formats/libdcd.pxd index 2af86cb4292..ac1100c6c43 100644 --- a/package/MDAnalysis/lib/formats/libdcd.pxd +++ b/package/MDAnalysis/lib/formats/libdcd.pxd @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/libdcd.pyx b/package/MDAnalysis/lib/formats/libdcd.pyx index 2c77df11a41..08ba4f120f4 100644 --- a/package/MDAnalysis/lib/formats/libdcd.pyx +++ b/package/MDAnalysis/lib/formats/libdcd.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/libmdaxdr.pxd b/package/MDAnalysis/lib/formats/libmdaxdr.pxd index 1c7307e3cc9..45a3455d770 100644 --- a/package/MDAnalysis/lib/formats/libmdaxdr.pxd +++ b/package/MDAnalysis/lib/formats/libmdaxdr.pxd @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/libmdaxdr.pyx b/package/MDAnalysis/lib/formats/libmdaxdr.pyx index b85e62de94d..4a691b3ae95 100644 --- a/package/MDAnalysis/lib/formats/libmdaxdr.pyx +++ b/package/MDAnalysis/lib/formats/libmdaxdr.pyx @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/formats/src/trr_seek.c b/package/MDAnalysis/lib/formats/src/trr_seek.c index 984e4f359fa..ab11ae49b79 100644 --- a/package/MDAnalysis/lib/formats/src/trr_seek.c +++ b/package/MDAnalysis/lib/formats/src/trr_seek.c @@ -6,7 +6,7 @@ Copyright (c) 2006-2016 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 + Released under the Lesser GNU Public Licence, v2.1 or any higher version Please cite your use of MDAnalysis in published work: diff --git a/package/MDAnalysis/lib/formats/src/xtc_seek.c b/package/MDAnalysis/lib/formats/src/xtc_seek.c index 278af05df34..d11b3445029 100644 --- a/package/MDAnalysis/lib/formats/src/xtc_seek.c +++ b/package/MDAnalysis/lib/formats/src/xtc_seek.c @@ -6,7 +6,7 @@ Copyright (c) 2006-2016 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 + Released under the Lesser GNU Public Licence, v2.1 or any higher version Please cite your use of MDAnalysis in published work: diff --git a/package/MDAnalysis/lib/include/calc_distances.h b/package/MDAnalysis/lib/include/calc_distances.h index 538b4f66d41..ff130d2c049 100644 --- a/package/MDAnalysis/lib/include/calc_distances.h +++ b/package/MDAnalysis/lib/include/calc_distances.h @@ -5,7 +5,7 @@ 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 + Released under the Lesser GNU Public Licence, v2.1 or any higher version Please cite your use of MDAnalysis in published work: diff --git a/package/MDAnalysis/lib/log.py b/package/MDAnalysis/lib/log.py index a5cfc2f5018..15100ef4884 100644 --- a/package/MDAnalysis/lib/log.py +++ b/package/MDAnalysis/lib/log.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/mdamath.py b/package/MDAnalysis/lib/mdamath.py index 93285ad2df7..e904116a1a5 100644 --- a/package/MDAnalysis/lib/mdamath.py +++ b/package/MDAnalysis/lib/mdamath.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/picklable_file_io.py b/package/MDAnalysis/lib/picklable_file_io.py index 91413619c5b..e27bca4b779 100644 --- a/package/MDAnalysis/lib/picklable_file_io.py +++ b/package/MDAnalysis/lib/picklable_file_io.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/pkdtree.py b/package/MDAnalysis/lib/pkdtree.py index 33d48fa1ddf..f50d16da9f8 100644 --- a/package/MDAnalysis/lib/pkdtree.py +++ b/package/MDAnalysis/lib/pkdtree.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/lib/util.py b/package/MDAnalysis/lib/util.py index 666a8c49279..7f576af0ade 100644 --- a/package/MDAnalysis/lib/util.py +++ b/package/MDAnalysis/lib/util.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/selections/__init__.py b/package/MDAnalysis/selections/__init__.py index 92c1eef8d92..3ccecf7d0b0 100644 --- a/package/MDAnalysis/selections/__init__.py +++ b/package/MDAnalysis/selections/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/selections/base.py b/package/MDAnalysis/selections/base.py index fccc0b7e2b7..eb55a73897e 100644 --- a/package/MDAnalysis/selections/base.py +++ b/package/MDAnalysis/selections/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/selections/charmm.py b/package/MDAnalysis/selections/charmm.py index 1ede0a646bc..5f9b4b4b9b0 100644 --- a/package/MDAnalysis/selections/charmm.py +++ b/package/MDAnalysis/selections/charmm.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/selections/gromacs.py b/package/MDAnalysis/selections/gromacs.py index 1129388d409..3dc8ea79502 100644 --- a/package/MDAnalysis/selections/gromacs.py +++ b/package/MDAnalysis/selections/gromacs.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/selections/jmol.py b/package/MDAnalysis/selections/jmol.py index a090b5bfe54..72462b97d2e 100644 --- a/package/MDAnalysis/selections/jmol.py +++ b/package/MDAnalysis/selections/jmol.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/selections/pymol.py b/package/MDAnalysis/selections/pymol.py index 0a86d23e918..080d83817f0 100644 --- a/package/MDAnalysis/selections/pymol.py +++ b/package/MDAnalysis/selections/pymol.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/selections/vmd.py b/package/MDAnalysis/selections/vmd.py index d9f3043ed90..dc449167511 100644 --- a/package/MDAnalysis/selections/vmd.py +++ b/package/MDAnalysis/selections/vmd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/tests/__init__.py b/package/MDAnalysis/tests/__init__.py index bf25ad2ebee..13a1c6e5122 100644 --- a/package/MDAnalysis/tests/__init__.py +++ b/package/MDAnalysis/tests/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/tests/datafiles.py b/package/MDAnalysis/tests/datafiles.py index 8f879873b12..30d3af12534 100644 --- a/package/MDAnalysis/tests/datafiles.py +++ b/package/MDAnalysis/tests/datafiles.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/CRDParser.py b/package/MDAnalysis/topology/CRDParser.py index 9a1fc72ec00..d1896423a84 100644 --- a/package/MDAnalysis/topology/CRDParser.py +++ b/package/MDAnalysis/topology/CRDParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/DLPolyParser.py b/package/MDAnalysis/topology/DLPolyParser.py index 4148a38c064..5452dbad3ce 100644 --- a/package/MDAnalysis/topology/DLPolyParser.py +++ b/package/MDAnalysis/topology/DLPolyParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/DMSParser.py b/package/MDAnalysis/topology/DMSParser.py index f37a854c725..84c04fd2ac8 100644 --- a/package/MDAnalysis/topology/DMSParser.py +++ b/package/MDAnalysis/topology/DMSParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/ExtendedPDBParser.py b/package/MDAnalysis/topology/ExtendedPDBParser.py index b41463403e1..ef4f25fee48 100644 --- a/package/MDAnalysis/topology/ExtendedPDBParser.py +++ b/package/MDAnalysis/topology/ExtendedPDBParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/FHIAIMSParser.py b/package/MDAnalysis/topology/FHIAIMSParser.py index 8738d5e3ce9..a47d367ec9e 100644 --- a/package/MDAnalysis/topology/FHIAIMSParser.py +++ b/package/MDAnalysis/topology/FHIAIMSParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/GMSParser.py b/package/MDAnalysis/topology/GMSParser.py index 2223cc42756..2ea7fe23004 100644 --- a/package/MDAnalysis/topology/GMSParser.py +++ b/package/MDAnalysis/topology/GMSParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/GROParser.py b/package/MDAnalysis/topology/GROParser.py index ebb51e7cd02..368b7d5daf0 100644 --- a/package/MDAnalysis/topology/GROParser.py +++ b/package/MDAnalysis/topology/GROParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/GSDParser.py b/package/MDAnalysis/topology/GSDParser.py index bd62d0f5f98..64746dd87ef 100644 --- a/package/MDAnalysis/topology/GSDParser.py +++ b/package/MDAnalysis/topology/GSDParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/HoomdXMLParser.py b/package/MDAnalysis/topology/HoomdXMLParser.py index f2d1cea9526..b64e91de2ff 100644 --- a/package/MDAnalysis/topology/HoomdXMLParser.py +++ b/package/MDAnalysis/topology/HoomdXMLParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/ITPParser.py b/package/MDAnalysis/topology/ITPParser.py index 5649e5cb384..83b43711c8a 100644 --- a/package/MDAnalysis/topology/ITPParser.py +++ b/package/MDAnalysis/topology/ITPParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/LAMMPSParser.py b/package/MDAnalysis/topology/LAMMPSParser.py index 52a58f77291..2f2ef6ac94a 100644 --- a/package/MDAnalysis/topology/LAMMPSParser.py +++ b/package/MDAnalysis/topology/LAMMPSParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/MMTFParser.py b/package/MDAnalysis/topology/MMTFParser.py index 5a58f1b2454..3abc6a281cb 100644 --- a/package/MDAnalysis/topology/MMTFParser.py +++ b/package/MDAnalysis/topology/MMTFParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/MOL2Parser.py b/package/MDAnalysis/topology/MOL2Parser.py index f5549858755..5c81e7346c6 100644 --- a/package/MDAnalysis/topology/MOL2Parser.py +++ b/package/MDAnalysis/topology/MOL2Parser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/MinimalParser.py b/package/MDAnalysis/topology/MinimalParser.py index 6265018dde3..ce0598bf3e7 100644 --- a/package/MDAnalysis/topology/MinimalParser.py +++ b/package/MDAnalysis/topology/MinimalParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/PDBParser.py b/package/MDAnalysis/topology/PDBParser.py index 8349be9133b..5c5c0c185de 100644 --- a/package/MDAnalysis/topology/PDBParser.py +++ b/package/MDAnalysis/topology/PDBParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/PDBQTParser.py b/package/MDAnalysis/topology/PDBQTParser.py index 435ec5678c8..88c3fe3ba40 100644 --- a/package/MDAnalysis/topology/PDBQTParser.py +++ b/package/MDAnalysis/topology/PDBQTParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/PQRParser.py b/package/MDAnalysis/topology/PQRParser.py index 9ef6d3e6f95..65c98a70d6e 100644 --- a/package/MDAnalysis/topology/PQRParser.py +++ b/package/MDAnalysis/topology/PQRParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/PSFParser.py b/package/MDAnalysis/topology/PSFParser.py index 70cd38d51fa..f247544262d 100644 --- a/package/MDAnalysis/topology/PSFParser.py +++ b/package/MDAnalysis/topology/PSFParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/ParmEdParser.py b/package/MDAnalysis/topology/ParmEdParser.py index b4d72304f5e..2cfc0df0dae 100644 --- a/package/MDAnalysis/topology/ParmEdParser.py +++ b/package/MDAnalysis/topology/ParmEdParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/TOPParser.py b/package/MDAnalysis/topology/TOPParser.py index 9113750cf95..4f2ce631fc6 100644 --- a/package/MDAnalysis/topology/TOPParser.py +++ b/package/MDAnalysis/topology/TOPParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/TPRParser.py b/package/MDAnalysis/topology/TPRParser.py index 396211d071f..022af574a28 100644 --- a/package/MDAnalysis/topology/TPRParser.py +++ b/package/MDAnalysis/topology/TPRParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/TXYZParser.py b/package/MDAnalysis/topology/TXYZParser.py index 206f381e9e0..4b0d248e374 100644 --- a/package/MDAnalysis/topology/TXYZParser.py +++ b/package/MDAnalysis/topology/TXYZParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/XYZParser.py b/package/MDAnalysis/topology/XYZParser.py index 5fe736fec6a..956c93567bc 100644 --- a/package/MDAnalysis/topology/XYZParser.py +++ b/package/MDAnalysis/topology/XYZParser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/__init__.py b/package/MDAnalysis/topology/__init__.py index b1b756b5386..951567ec615 100644 --- a/package/MDAnalysis/topology/__init__.py +++ b/package/MDAnalysis/topology/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/base.py b/package/MDAnalysis/topology/base.py index 260251fb26e..f4ae0894e40 100644 --- a/package/MDAnalysis/topology/base.py +++ b/package/MDAnalysis/topology/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/core.py b/package/MDAnalysis/topology/core.py index b5d73183018..7ee61219827 100644 --- a/package/MDAnalysis/topology/core.py +++ b/package/MDAnalysis/topology/core.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/guessers.py b/package/MDAnalysis/topology/guessers.py index a4661c871c0..d1485bad080 100644 --- a/package/MDAnalysis/topology/guessers.py +++ b/package/MDAnalysis/topology/guessers.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/tpr/__init__.py b/package/MDAnalysis/topology/tpr/__init__.py index a1060581d10..19d53190853 100644 --- a/package/MDAnalysis/topology/tpr/__init__.py +++ b/package/MDAnalysis/topology/tpr/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/topology/tpr/obj.py b/package/MDAnalysis/topology/tpr/obj.py index 5f5040c7db8..6be8b40b746 100644 --- a/package/MDAnalysis/topology/tpr/obj.py +++ b/package/MDAnalysis/topology/tpr/obj.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -24,7 +24,7 @@ # TPR parser and tpr support module # Copyright (c) 2011 Zhuyi Xue -# Released under the GNU Public Licence, v2 +# Released under the Lesser GNU Public Licence, v2.1+ """ Class definitions for the TPRParser diff --git a/package/MDAnalysis/topology/tpr/setting.py b/package/MDAnalysis/topology/tpr/setting.py index 711154cf847..89f8ffa09aa 100644 --- a/package/MDAnalysis/topology/tpr/setting.py +++ b/package/MDAnalysis/topology/tpr/setting.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -24,7 +24,7 @@ # TPR parser and tpr support module # Copyright (c) 2011 Zhuyi Xue -# Released under the GNU Public Licence, v2 +# Released under the Lesser GNU Public Licence, v2.1+ """ TPRParser settings diff --git a/package/MDAnalysis/topology/tpr/utils.py b/package/MDAnalysis/topology/tpr/utils.py index 4e26dbfa565..9ba7d8e63ab 100644 --- a/package/MDAnalysis/topology/tpr/utils.py +++ b/package/MDAnalysis/topology/tpr/utils.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -24,7 +24,7 @@ # TPR parser and tpr support module # Copyright (c) 2011 Zhuyi Xue -# Released under the GNU Public Licence, v2 +# Released under the Lesser GNU Public Licence, v2.1+ """ Utilities for the TPRParser diff --git a/package/MDAnalysis/transformations/__init__.py b/package/MDAnalysis/transformations/__init__.py index f359363157d..6335887eabc 100644 --- a/package/MDAnalysis/transformations/__init__.py +++ b/package/MDAnalysis/transformations/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/base.py b/package/MDAnalysis/transformations/base.py index 497ab5937eb..59ad37e7fa6 100644 --- a/package/MDAnalysis/transformations/base.py +++ b/package/MDAnalysis/transformations/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/boxdimensions.py b/package/MDAnalysis/transformations/boxdimensions.py index 4ec063775a5..0f5ebbd3227 100644 --- a/package/MDAnalysis/transformations/boxdimensions.py +++ b/package/MDAnalysis/transformations/boxdimensions.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/fit.py b/package/MDAnalysis/transformations/fit.py index 2d209591e78..2356201c54a 100644 --- a/package/MDAnalysis/transformations/fit.py +++ b/package/MDAnalysis/transformations/fit.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/nojump.py b/package/MDAnalysis/transformations/nojump.py index d58487c220a..fd6dc7703e4 100644 --- a/package/MDAnalysis/transformations/nojump.py +++ b/package/MDAnalysis/transformations/nojump.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/positionaveraging.py b/package/MDAnalysis/transformations/positionaveraging.py index c4409a7206c..13145b69c44 100644 --- a/package/MDAnalysis/transformations/positionaveraging.py +++ b/package/MDAnalysis/transformations/positionaveraging.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/rotate.py b/package/MDAnalysis/transformations/rotate.py index 868247dea6b..ddb730f0694 100644 --- a/package/MDAnalysis/transformations/rotate.py +++ b/package/MDAnalysis/transformations/rotate.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/translate.py b/package/MDAnalysis/transformations/translate.py index 6bf301db8f4..6edf5d4692a 100644 --- a/package/MDAnalysis/transformations/translate.py +++ b/package/MDAnalysis/transformations/translate.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/transformations/wrap.py b/package/MDAnalysis/transformations/wrap.py index 9133a1331ff..f077f5edc19 100644 --- a/package/MDAnalysis/transformations/wrap.py +++ b/package/MDAnalysis/transformations/wrap.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/units.py b/package/MDAnalysis/units.py index 34dc71c2af7..1affd05367d 100644 --- a/package/MDAnalysis/units.py +++ b/package/MDAnalysis/units.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/version.py b/package/MDAnalysis/version.py index a8213cc90e6..66476ef9b3e 100644 --- a/package/MDAnalysis/version.py +++ b/package/MDAnalysis/version.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/visualization/__init__.py b/package/MDAnalysis/visualization/__init__.py index 72e488329a2..66d92c5d3f7 100644 --- a/package/MDAnalysis/visualization/__init__.py +++ b/package/MDAnalysis/visualization/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/package/MDAnalysis/visualization/streamlines.py b/package/MDAnalysis/visualization/streamlines.py index 965074a43d7..16f844f9fa3 100644 --- a/package/MDAnalysis/visualization/streamlines.py +++ b/package/MDAnalysis/visualization/streamlines.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Authors: Tyler Reddy and Matthieu Chavent :Year: 2014 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ The :func:`generate_streamlines` function can generate a 2D flow field from a diff --git a/package/MDAnalysis/visualization/streamlines_3D.py b/package/MDAnalysis/visualization/streamlines_3D.py index 1f85851c16a..7e48b138fd4 100644 --- a/package/MDAnalysis/visualization/streamlines_3D.py +++ b/package/MDAnalysis/visualization/streamlines_3D.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # @@ -26,7 +26,7 @@ :Authors: Tyler Reddy and Matthieu Chavent :Year: 2014 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ The :func:`generate_streamlines_3d` function can generate a 3D flow field from diff --git a/package/README b/package/README index 277c62dfed3..baf0027cd40 100644 --- a/package/README +++ b/package/README @@ -34,8 +34,10 @@ This software is copyright listed in the file AUTHORS unless stated otherwise in the source files. -MDAnalysis is released under the GPL software license, version 2, with -the following exceptions (see AUTHORS and LICENSE for details): +MDAnalysis is packaged under the GNU Lesser General Public License, version 3 +or any later version (LGPLv3+). Invidiual source code components are provided under the +GNU Lesser General Public License, version 2.1 or any later version (LGPLv2.1+), +with the following exceptions (see AUTHORS and LICENSE for details): - The DCD reading/writing code is licensed under the UIUC Open Source License. diff --git a/package/doc/sphinx/source/documentation_pages/analysis/encore.rst b/package/doc/sphinx/source/documentation_pages/analysis/encore.rst index d4da4612601..ee165ae7559 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis/encore.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis/encore.rst @@ -4,7 +4,7 @@ :Author: Matteo Tiberti, Wouter Boomsma, Tone Bengtsen :Year: 2015-2017 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ :Maintainer: Matteo Tiberti , mtiberti on github .. versionadded:: 0.16.0 diff --git a/package/doc/sphinx/source/documentation_pages/analysis/hole2.rst b/package/doc/sphinx/source/documentation_pages/analysis/hole2.rst index 1d8b962a175..6c49fdfff61 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis/hole2.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis/hole2.rst @@ -4,7 +4,7 @@ :Author: Lily Wang :Year: 2020 -:Copyright: GNU Public License v3 +:Copyright: Lesser GNU Public License v2.1+ .. versionadded:: 1.0.0 diff --git a/package/doc/sphinx/source/index.rst b/package/doc/sphinx/source/index.rst index 29e800d6a59..f0aaa5334ec 100644 --- a/package/doc/sphinx/source/index.rst +++ b/package/doc/sphinx/source/index.rst @@ -141,8 +141,8 @@ Source Code **Source code** is available from https://github.com/MDAnalysis/mdanalysis/ and is packaged under the -`GNU Public Licence, version 3 or any later version`_. Individual components -of the source code are provided under GPL compatible licenses, details can be +`Lesser GNU Public Licence, version 3 or any later version`_. Individual components +of the source code are provided under LGPL compatible licenses, details can be found in the `MDAnalysis license file`_. Obtain the sources with `git`_. .. code-block:: bash @@ -153,8 +153,8 @@ found in the `MDAnalysis license file`_. Obtain the sources with `git`_. The `User Guide`_ provides more information on how to `install the development version`_ of MDAnalysis. -.. _GNU Public Licence, version 3 or any later version: - https://www.gnu.org/licenses/gpl-3.0.en.html +.. _Lesser GNU Public Licence, version 3 or any later version: + https://www.gnu.org/licenses/lgpl-3.0.en.html .. _MDAnalysis license file: https://github.com/MDAnalysis/mdanalysis/blob/develop/LICENSE .. _git: https://git-scm.com/ diff --git a/package/pyproject.toml b/package/pyproject.toml index cd7ec3a7806..4dc2275df49 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -53,7 +53,7 @@ classifiers = [ 'Development Status :: 6 - Mature', 'Environment :: Console', 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', diff --git a/package/setup.py b/package/setup.py index a65641f93b2..3e9bf27f85e 100755 --- a/package/setup.py +++ b/package/setup.py @@ -6,7 +6,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/LICENSE b/testsuite/LICENSE index b6b2162c890..8d0eeadd954 100644 --- a/testsuite/LICENSE +++ b/testsuite/LICENSE @@ -2,22 +2,17 @@ Licensing of the MDAnalysis library ========================================================================== -As of MDAnalysis version 2.6.0, the MDAnalyis library is packaged under -the terms of the GNU General Public License version 3 or any later version -(GPLv3+). - -Developer contributions to the library have, unless otherwise stated, been -made under the following conditions: - - From the 31st of July 2023 onwards, all contributions are made under - the terms of the GNU Lesser General Public License v2.1 or any later - version (LGPLv2.1+) - - Before the 31st of July 2023, contributions were made under the GNU - General Public License version 2 or any later version (GPLv2+). +The MDAnalyis library is packaged under the terms of the GNU Lesser +General Public License version 3 or any later version (LGPLv3+). + +Developer contributions to the library are, unless otherwise stated, +made under the GNU Lesser General Public License version 2.1 or any +later version (LGPLv2.1+). The MDAnalysis library also includes external codes provided under licenses -compatible with the terms of the GNU General Public License version 3 or any -later version (GPLv3+). These are outlined under "Licenses of components of -MDAnalysis". +compatible with the terms of the GNU Lesser General Public License version +3 or any later version (LGPLv3+). These are outlined under +"Licenses of components of MDAnalysis". ========================================================================== Licenses of components of MDAnalysis @@ -529,6 +524,175 @@ necessary. Here is a sample; alter the names: That's all there is to it! +========================================================================== + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + ========================================================================== GNU GENERAL PUBLIC LICENSE @@ -1206,351 +1370,6 @@ the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . - -========================================================================== - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. - - ========================================================================== Gromacs xdrfile library for reading XTC/TRR trajectories @@ -1765,6 +1584,33 @@ PyQCPROT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================================================== + +DSSP module code for protein secondary structure assignment + - analysis/dssp/pydssp_numpy.py + +MIT License + +Copyright (c) 2022 Shintaro Minami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ========================================================================== MDAnalysis logo (see doc/sphinx/source/logos) diff --git a/testsuite/MDAnalysisTests/__init__.py b/testsuite/MDAnalysisTests/__init__.py index 3924a570855..5752ec98588 100644 --- a/testsuite/MDAnalysisTests/__init__.py +++ b/testsuite/MDAnalysisTests/__init__.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_align.py b/testsuite/MDAnalysisTests/analysis/test_align.py index 63dfd25db80..31455198bec 100644 --- a/testsuite/MDAnalysisTests/analysis/test_align.py +++ b/testsuite/MDAnalysisTests/analysis/test_align.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py b/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py index fb247a32ea9..443173cff70 100644 --- a/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py +++ b/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index ab7748a20c7..90887b2ad0b 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_bat.py b/testsuite/MDAnalysisTests/analysis/test_bat.py index 5fb2603df62..f6bf24a56a8 100644 --- a/testsuite/MDAnalysisTests/analysis/test_bat.py +++ b/testsuite/MDAnalysisTests/analysis/test_bat.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_contacts.py b/testsuite/MDAnalysisTests/analysis/test_contacts.py index ad4f96caf44..85546cbc3f5 100644 --- a/testsuite/MDAnalysisTests/analysis/test_contacts.py +++ b/testsuite/MDAnalysisTests/analysis/test_contacts.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_data.py b/testsuite/MDAnalysisTests/analysis/test_data.py index 1dbf151fade..44853346c85 100644 --- a/testsuite/MDAnalysisTests/analysis/test_data.py +++ b/testsuite/MDAnalysisTests/analysis/test_data.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index 80ff81bd5be..b00a8234c17 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_dielectric.py b/testsuite/MDAnalysisTests/analysis/test_dielectric.py index 21992cf5d5c..a1a5ccc5062 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dielectric.py +++ b/testsuite/MDAnalysisTests/analysis/test_dielectric.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py b/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py index be58365b0fa..11271fd8f4c 100644 --- a/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py +++ b/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_dihedrals.py b/testsuite/MDAnalysisTests/analysis/test_dihedrals.py index c5d291bf96d..767345bda7f 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dihedrals.py +++ b/testsuite/MDAnalysisTests/analysis/test_dihedrals.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_distances.py b/testsuite/MDAnalysisTests/analysis/test_distances.py index 2a71ac31654..8e3a14f8224 100644 --- a/testsuite/MDAnalysisTests/analysis/test_distances.py +++ b/testsuite/MDAnalysisTests/analysis/test_distances.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_encore.py b/testsuite/MDAnalysisTests/analysis/test_encore.py index 424aae54278..948575adfff 100644 --- a/testsuite/MDAnalysisTests/analysis/test_encore.py +++ b/testsuite/MDAnalysisTests/analysis/test_encore.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_gnm.py b/testsuite/MDAnalysisTests/analysis/test_gnm.py index d8a547a5428..e69ac7056fe 100644 --- a/testsuite/MDAnalysisTests/analysis/test_gnm.py +++ b/testsuite/MDAnalysisTests/analysis/test_gnm.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py b/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py index 1e39147be81..dbde56fde4e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py +++ b/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_hole2.py b/testsuite/MDAnalysisTests/analysis/test_hole2.py index fbb34aac698..fb4a50806d5 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hole2.py +++ b/testsuite/MDAnalysisTests/analysis/test_hole2.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py index de2993fde8c..6a4970edba1 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py index c51df85f319..7a372d53c54 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py index 503560a648f..bef6b03331d 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_leaflet.py b/testsuite/MDAnalysisTests/analysis/test_leaflet.py index 0f99fee8580..0c4839f36b5 100644 --- a/testsuite/MDAnalysisTests/analysis/test_leaflet.py +++ b/testsuite/MDAnalysisTests/analysis/test_leaflet.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py index 2711d24e5ac..2b6ce161cb6 100644 --- a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py +++ b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_msd.py b/testsuite/MDAnalysisTests/analysis/test_msd.py index 0a1af15ff58..3b96e40c61a 100644 --- a/testsuite/MDAnalysisTests/analysis/test_msd.py +++ b/testsuite/MDAnalysisTests/analysis/test_msd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py b/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py index fb7d39374cd..5f90c3b0c1d 100644 --- a/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py +++ b/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py b/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py index 5af2fea86ce..ea6c3e03fbf 100644 --- a/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py +++ b/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_pca.py b/testsuite/MDAnalysisTests/analysis/test_pca.py index b0358ba4243..19dca6cf3b0 100644 --- a/testsuite/MDAnalysisTests/analysis/test_pca.py +++ b/testsuite/MDAnalysisTests/analysis/test_pca.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py index 6914573a11b..5d8790ab3db 100644 --- a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py +++ b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_psa.py b/testsuite/MDAnalysisTests/analysis/test_psa.py index 2263d50ff41..6b0776c2b62 100644 --- a/testsuite/MDAnalysisTests/analysis/test_psa.py +++ b/testsuite/MDAnalysisTests/analysis/test_psa.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_rdf.py b/testsuite/MDAnalysisTests/analysis/test_rdf.py index bc121666488..90adef77e3a 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rdf.py +++ b/testsuite/MDAnalysisTests/analysis/test_rdf.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_rdf_s.py b/testsuite/MDAnalysisTests/analysis/test_rdf_s.py index f8f41c0e165..49e2a66bd5b 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rdf_s.py +++ b/testsuite/MDAnalysisTests/analysis/test_rdf_s.py @@ -5,7 +5,7 @@ # Copyright (c) 2006-2018 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index 87822e0b79c..deb46885c82 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py b/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py index 653a04e1fdb..ed8cd64a51c 100644 --- a/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py +++ b/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/auxiliary/base.py b/testsuite/MDAnalysisTests/auxiliary/base.py index 2f400444050..7394de17806 100644 --- a/testsuite/MDAnalysisTests/auxiliary/base.py +++ b/testsuite/MDAnalysisTests/auxiliary/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/auxiliary/test_core.py b/testsuite/MDAnalysisTests/auxiliary/test_core.py index 814cbb75b9d..f06320af411 100644 --- a/testsuite/MDAnalysisTests/auxiliary/test_core.py +++ b/testsuite/MDAnalysisTests/auxiliary/test_core.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/auxiliary/test_edr.py b/testsuite/MDAnalysisTests/auxiliary/test_edr.py index 224abba4eaf..9aa6e762b07 100644 --- a/testsuite/MDAnalysisTests/auxiliary/test_edr.py +++ b/testsuite/MDAnalysisTests/auxiliary/test_edr.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/auxiliary/test_xvg.py b/testsuite/MDAnalysisTests/auxiliary/test_xvg.py index 1e9972629e1..0afaa2ce423 100644 --- a/testsuite/MDAnalysisTests/auxiliary/test_xvg.py +++ b/testsuite/MDAnalysisTests/auxiliary/test_xvg.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/converters/test_base.py b/testsuite/MDAnalysisTests/converters/test_base.py index 5b3d1ee9304..99a4deca03d 100644 --- a/testsuite/MDAnalysisTests/converters/test_base.py +++ b/testsuite/MDAnalysisTests/converters/test_base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/converters/test_openmm.py b/testsuite/MDAnalysisTests/converters/test_openmm.py index 0ee591990e7..4405547b8c2 100644 --- a/testsuite/MDAnalysisTests/converters/test_openmm.py +++ b/testsuite/MDAnalysisTests/converters/test_openmm.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/converters/test_openmm_parser.py b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py index d3d923294f1..12fdbd4857a 100644 --- a/testsuite/MDAnalysisTests/converters/test_openmm_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/converters/test_parmed.py b/testsuite/MDAnalysisTests/converters/test_parmed.py index 46eb9cfad75..e0503bd9dad 100644 --- a/testsuite/MDAnalysisTests/converters/test_parmed.py +++ b/testsuite/MDAnalysisTests/converters/test_parmed.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/converters/test_parmed_parser.py b/testsuite/MDAnalysisTests/converters/test_parmed_parser.py index 3a2c1c04256..ea73e8dc000 100644 --- a/testsuite/MDAnalysisTests/converters/test_parmed_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_parmed_parser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/converters/test_rdkit.py b/testsuite/MDAnalysisTests/converters/test_rdkit.py index 59c15af4c16..16793a44848 100644 --- a/testsuite/MDAnalysisTests/converters/test_rdkit.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py index e376ff09e37..bf432990fa4 100644 --- a/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/base.py b/testsuite/MDAnalysisTests/coordinates/base.py index 39770e1460b..dafeeedca35 100644 --- a/testsuite/MDAnalysisTests/coordinates/base.py +++ b/testsuite/MDAnalysisTests/coordinates/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/reference.py b/testsuite/MDAnalysisTests/coordinates/reference.py index 8c523c639e1..73a91809852 100644 --- a/testsuite/MDAnalysisTests/coordinates/reference.py +++ b/testsuite/MDAnalysisTests/coordinates/reference.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py b/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py index 859db99b797..66394ee4d74 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py index 9f81b8c325c..84bcd128cf5 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py +++ b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py b/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py index 5587965a1f1..fd489977fe2 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py +++ b/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_copying.py b/testsuite/MDAnalysisTests/coordinates/test_copying.py index b6f42e7aff0..91fb87fd465 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_copying.py +++ b/testsuite/MDAnalysisTests/coordinates/test_copying.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_crd.py b/testsuite/MDAnalysisTests/coordinates/test_crd.py index 2b964d9c67e..cffde92d75a 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_crd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_crd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_dcd.py b/testsuite/MDAnalysisTests/coordinates/test_dcd.py index e6fa4dfb3e3..9d27f8163f7 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_dcd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_dcd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py b/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py index 711a63669d8..81b1aded061 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py +++ b/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_dms.py b/testsuite/MDAnalysisTests/coordinates/test_dms.py index 9cead15a3d5..01823a6ec66 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_dms.py +++ b/testsuite/MDAnalysisTests/coordinates/test_dms.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py b/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py index ebdf0411769..463ddc59075 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py +++ b/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_gms.py b/testsuite/MDAnalysisTests/coordinates/test_gms.py index 796969fe485..08ac1a9bcdc 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gms.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gms.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_gro.py b/testsuite/MDAnalysisTests/coordinates/test_gro.py index e25bf969fc5..7dcdbbc029a 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gro.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gro.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_gsd.py b/testsuite/MDAnalysisTests/coordinates/test_gsd.py index 7cf38209fb1..e6b6a79ae48 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gsd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gsd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_lammps.py b/testsuite/MDAnalysisTests/coordinates/test_lammps.py index 88b4b8c35ee..39f693c75ca 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_lammps.py +++ b/testsuite/MDAnalysisTests/coordinates/test_lammps.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_memory.py b/testsuite/MDAnalysisTests/coordinates/test_memory.py index b77006cc04e..223345de155 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_memory.py +++ b/testsuite/MDAnalysisTests/coordinates/test_memory.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_mmtf.py b/testsuite/MDAnalysisTests/coordinates/test_mmtf.py index 75ed48bf89b..a8a3b6037a0 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_mmtf.py +++ b/testsuite/MDAnalysisTests/coordinates/test_mmtf.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_mol2.py b/testsuite/MDAnalysisTests/coordinates/test_mol2.py index 1439466eb94..450a972eca5 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_mol2.py +++ b/testsuite/MDAnalysisTests/coordinates/test_mol2.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_namdbin.py b/testsuite/MDAnalysisTests/coordinates/test_namdbin.py index d24835c0b66..9cbce77f2ee 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_namdbin.py +++ b/testsuite/MDAnalysisTests/coordinates/test_namdbin.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_netcdf.py b/testsuite/MDAnalysisTests/coordinates/test_netcdf.py index 7e365dad51a..dc3456addfa 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_netcdf.py +++ b/testsuite/MDAnalysisTests/coordinates/test_netcdf.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_null.py b/testsuite/MDAnalysisTests/coordinates/test_null.py index d91823df0c9..fc23803258d 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_null.py +++ b/testsuite/MDAnalysisTests/coordinates/test_null.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_pdb.py b/testsuite/MDAnalysisTests/coordinates/test_pdb.py index 58e20a5aa12..441a91864d5 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_pdb.py +++ b/testsuite/MDAnalysisTests/coordinates/test_pdb.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py b/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py index 6b07c4818e6..2caf44ecbbc 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py +++ b/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_pqr.py b/testsuite/MDAnalysisTests/coordinates/test_pqr.py index 9f1fe2f89b1..b3a54447247 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_pqr.py +++ b/testsuite/MDAnalysisTests/coordinates/test_pqr.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_reader_api.py b/testsuite/MDAnalysisTests/coordinates/test_reader_api.py index d9148e7fdd7..c2062ab995f 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_reader_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_reader_api.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py b/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py index 423a255cc49..a12934199d1 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_tng.py b/testsuite/MDAnalysisTests/coordinates/test_tng.py index df8d96631f3..c9ea9b8678e 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_tng.py +++ b/testsuite/MDAnalysisTests/coordinates/test_tng.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_trc.py b/testsuite/MDAnalysisTests/coordinates/test_trc.py index dd64f6d0a93..430eb422374 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_trc.py +++ b/testsuite/MDAnalysisTests/coordinates/test_trc.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_trj.py b/testsuite/MDAnalysisTests/coordinates/test_trj.py index 50f01140c89..1ba1271f5fb 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_trj.py +++ b/testsuite/MDAnalysisTests/coordinates/test_trj.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_trz.py b/testsuite/MDAnalysisTests/coordinates/test_trz.py index e0f20f961a6..ea455bb8c71 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_trz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_trz.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_txyz.py b/testsuite/MDAnalysisTests/coordinates/test_txyz.py index a92089983b6..fda7e62ba89 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_txyz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_txyz.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_windows.py b/testsuite/MDAnalysisTests/coordinates/test_windows.py index 1375f92a17e..723ce5e689d 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_windows.py +++ b/testsuite/MDAnalysisTests/coordinates/test_windows.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_writer_api.py b/testsuite/MDAnalysisTests/coordinates/test_writer_api.py index 230f2b0cf47..54d364fb75b 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_writer_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_writer_api.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py b/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py index 839516070ed..e5fdf19744a 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py +++ b/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_xdr.py b/testsuite/MDAnalysisTests/coordinates/test_xdr.py index 6d6a01858ad..efb15d78250 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_xdr.py +++ b/testsuite/MDAnalysisTests/coordinates/test_xdr.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/coordinates/test_xyz.py b/testsuite/MDAnalysisTests/coordinates/test_xyz.py index 1890d6e2900..6612746f1c9 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_xyz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_xyz.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_accessors.py b/testsuite/MDAnalysisTests/core/test_accessors.py index cef30982dec..9b3d22ffd0e 100644 --- a/testsuite/MDAnalysisTests/core/test_accessors.py +++ b/testsuite/MDAnalysisTests/core/test_accessors.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_accumulate.py b/testsuite/MDAnalysisTests/core/test_accumulate.py index aadbeee6454..b93a458d06e 100644 --- a/testsuite/MDAnalysisTests/core/test_accumulate.py +++ b/testsuite/MDAnalysisTests/core/test_accumulate.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_atom.py b/testsuite/MDAnalysisTests/core/test_atom.py index d63d0574f06..24479783d91 100644 --- a/testsuite/MDAnalysisTests/core/test_atom.py +++ b/testsuite/MDAnalysisTests/core/test_atom.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index fe51cf24073..fdb23b682e4 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 7db3611282f..bced4c43bde 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_copying.py b/testsuite/MDAnalysisTests/core/test_copying.py index f5279b39f8a..9f8d0c7000c 100644 --- a/testsuite/MDAnalysisTests/core/test_copying.py +++ b/testsuite/MDAnalysisTests/core/test_copying.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_fragments.py b/testsuite/MDAnalysisTests/core/test_fragments.py index d7649dc0096..02c3bb00ef2 100644 --- a/testsuite/MDAnalysisTests/core/test_fragments.py +++ b/testsuite/MDAnalysisTests/core/test_fragments.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_group_traj_access.py b/testsuite/MDAnalysisTests/core/test_group_traj_access.py index 3eaee8e499c..bc63c83466d 100644 --- a/testsuite/MDAnalysisTests/core/test_group_traj_access.py +++ b/testsuite/MDAnalysisTests/core/test_group_traj_access.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index e3400ea0f40..6137d2b4244 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_index_dtype.py b/testsuite/MDAnalysisTests/core/test_index_dtype.py index 884decd040f..b9cb0f43a09 100644 --- a/testsuite/MDAnalysisTests/core/test_index_dtype.py +++ b/testsuite/MDAnalysisTests/core/test_index_dtype.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_residue.py b/testsuite/MDAnalysisTests/core/test_residue.py index 68cd0f28268..b2bf9429105 100644 --- a/testsuite/MDAnalysisTests/core/test_residue.py +++ b/testsuite/MDAnalysisTests/core/test_residue.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_residuegroup.py b/testsuite/MDAnalysisTests/core/test_residuegroup.py index 21091c817d8..ad5521d20a1 100644 --- a/testsuite/MDAnalysisTests/core/test_residuegroup.py +++ b/testsuite/MDAnalysisTests/core/test_residuegroup.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_segment.py b/testsuite/MDAnalysisTests/core/test_segment.py index 3e0c675d5a3..60b167dd882 100644 --- a/testsuite/MDAnalysisTests/core/test_segment.py +++ b/testsuite/MDAnalysisTests/core/test_segment.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_segmentgroup.py b/testsuite/MDAnalysisTests/core/test_segmentgroup.py index c47f1f6367d..11841bb7797 100644 --- a/testsuite/MDAnalysisTests/core/test_segmentgroup.py +++ b/testsuite/MDAnalysisTests/core/test_segmentgroup.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 5489c381af2..3ece107a93d 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_topologyobjects.py b/testsuite/MDAnalysisTests/core/test_topologyobjects.py index ac5f0353386..c4bc05c6a1c 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyobjects.py +++ b/testsuite/MDAnalysisTests/core/test_topologyobjects.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_universe.py b/testsuite/MDAnalysisTests/core/test_universe.py index 4f0806728e4..d17b9c707a3 100644 --- a/testsuite/MDAnalysisTests/core/test_universe.py +++ b/testsuite/MDAnalysisTests/core/test_universe.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_unwrap.py b/testsuite/MDAnalysisTests/core/test_unwrap.py index 656d9b08144..fe69d945760 100644 --- a/testsuite/MDAnalysisTests/core/test_unwrap.py +++ b/testsuite/MDAnalysisTests/core/test_unwrap.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py b/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py index 224ed10f5ed..51c3eecf500 100644 --- a/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/test_wrap.py b/testsuite/MDAnalysisTests/core/test_wrap.py index ecbe61fcee5..186ac3dadee 100644 --- a/testsuite/MDAnalysisTests/core/test_wrap.py +++ b/testsuite/MDAnalysisTests/core/test_wrap.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/core/util.py b/testsuite/MDAnalysisTests/core/util.py index 22bd3add97d..42f2c3a8ed9 100644 --- a/testsuite/MDAnalysisTests/core/util.py +++ b/testsuite/MDAnalysisTests/core/util.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/datafiles.py b/testsuite/MDAnalysisTests/datafiles.py index ef0bea4036a..9a63d33716c 100644 --- a/testsuite/MDAnalysisTests/datafiles.py +++ b/testsuite/MDAnalysisTests/datafiles.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/dummy.py b/testsuite/MDAnalysisTests/dummy.py index 1da3799fbdf..fc4c77326b3 100644 --- a/testsuite/MDAnalysisTests/dummy.py +++ b/testsuite/MDAnalysisTests/dummy.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/formats/test_libdcd.py b/testsuite/MDAnalysisTests/formats/test_libdcd.py index 0fdc53a7321..f44ae5ba1ae 100644 --- a/testsuite/MDAnalysisTests/formats/test_libdcd.py +++ b/testsuite/MDAnalysisTests/formats/test_libdcd.py @@ -5,7 +5,7 @@ # Copyright (c) 2006-2015 Naveen Michaud-Agrawal, Elizabeth J. Denning, Oliver # Beckstein and contributors (see AUTHORS for the full list) # -# Released under the GNU Public Licence, v2 or any higher version +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py b/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py index 78f31afc109..cd6a73a28e7 100644 --- a/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py +++ b/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/import/fork_called.py b/testsuite/MDAnalysisTests/import/fork_called.py index 0e2ab5988b6..823f122a4e5 100644 --- a/testsuite/MDAnalysisTests/import/fork_called.py +++ b/testsuite/MDAnalysisTests/import/fork_called.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/import/test_import.py b/testsuite/MDAnalysisTests/import/test_import.py index 6afcb72a281..d8065f4ac18 100644 --- a/testsuite/MDAnalysisTests/import/test_import.py +++ b/testsuite/MDAnalysisTests/import/test_import.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_augment.py b/testsuite/MDAnalysisTests/lib/test_augment.py index 4e56e567df2..bb9d5f54d49 100644 --- a/testsuite/MDAnalysisTests/lib/test_augment.py +++ b/testsuite/MDAnalysisTests/lib/test_augment.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_cutil.py b/testsuite/MDAnalysisTests/lib/test_cutil.py index c00e68eedd6..9f710984df0 100644 --- a/testsuite/MDAnalysisTests/lib/test_cutil.py +++ b/testsuite/MDAnalysisTests/lib/test_cutil.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_distances.py b/testsuite/MDAnalysisTests/lib/test_distances.py index 4f7cd238bab..0586ba071fe 100644 --- a/testsuite/MDAnalysisTests/lib/test_distances.py +++ b/testsuite/MDAnalysisTests/lib/test_distances.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_log.py b/testsuite/MDAnalysisTests/lib/test_log.py index acaa876df92..cab2994a87d 100644 --- a/testsuite/MDAnalysisTests/lib/test_log.py +++ b/testsuite/MDAnalysisTests/lib/test_log.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_neighborsearch.py b/testsuite/MDAnalysisTests/lib/test_neighborsearch.py index a6f06b3d6e2..7ae209485ba 100644 --- a/testsuite/MDAnalysisTests/lib/test_neighborsearch.py +++ b/testsuite/MDAnalysisTests/lib/test_neighborsearch.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_nsgrid.py b/testsuite/MDAnalysisTests/lib/test_nsgrid.py index bc9c77502f7..69e7fa1f89f 100644 --- a/testsuite/MDAnalysisTests/lib/test_nsgrid.py +++ b/testsuite/MDAnalysisTests/lib/test_nsgrid.py @@ -5,7 +5,7 @@ # Copyright (c) 2006-2018 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_pkdtree.py b/testsuite/MDAnalysisTests/lib/test_pkdtree.py index 513ff430c0c..f92a87e73e9 100644 --- a/testsuite/MDAnalysisTests/lib/test_pkdtree.py +++ b/testsuite/MDAnalysisTests/lib/test_pkdtree.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_qcprot.py b/testsuite/MDAnalysisTests/lib/test_qcprot.py index 2f7130d135c..a62ae73f971 100644 --- a/testsuite/MDAnalysisTests/lib/test_qcprot.py +++ b/testsuite/MDAnalysisTests/lib/test_qcprot.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/lib/test_util.py b/testsuite/MDAnalysisTests/lib/test_util.py index cd641133586..839b0ef61e4 100644 --- a/testsuite/MDAnalysisTests/lib/test_util.py +++ b/testsuite/MDAnalysisTests/lib/test_util.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py b/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py index e92ff80bf14..6cfc6816696 100644 --- a/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py +++ b/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py b/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py index d663c75cff5..14911079b34 100644 --- a/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py +++ b/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/test_api.py b/testsuite/MDAnalysisTests/test_api.py index 82e8ab6daa4..a3a476825cf 100644 --- a/testsuite/MDAnalysisTests/test_api.py +++ b/testsuite/MDAnalysisTests/test_api.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/base.py b/testsuite/MDAnalysisTests/topology/base.py index 142e7954abb..6527ab8ae34 100644 --- a/testsuite/MDAnalysisTests/topology/base.py +++ b/testsuite/MDAnalysisTests/topology/base.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_altloc.py b/testsuite/MDAnalysisTests/topology/test_altloc.py index 00e09279332..1007c3e0673 100644 --- a/testsuite/MDAnalysisTests/topology/test_altloc.py +++ b/testsuite/MDAnalysisTests/topology/test_altloc.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_crd.py b/testsuite/MDAnalysisTests/topology/test_crd.py index 3062ba12f3b..7c9b0a72419 100644 --- a/testsuite/MDAnalysisTests/topology/test_crd.py +++ b/testsuite/MDAnalysisTests/topology/test_crd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_dlpoly.py b/testsuite/MDAnalysisTests/topology/test_dlpoly.py index a21f7134ca1..da1e871dcdd 100644 --- a/testsuite/MDAnalysisTests/topology/test_dlpoly.py +++ b/testsuite/MDAnalysisTests/topology/test_dlpoly.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_dms.py b/testsuite/MDAnalysisTests/topology/test_dms.py index d9f7944aaa0..b1eb8d77b34 100644 --- a/testsuite/MDAnalysisTests/topology/test_dms.py +++ b/testsuite/MDAnalysisTests/topology/test_dms.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_fhiaims.py b/testsuite/MDAnalysisTests/topology/test_fhiaims.py index 39097473871..b8bbc29e46e 100644 --- a/testsuite/MDAnalysisTests/topology/test_fhiaims.py +++ b/testsuite/MDAnalysisTests/topology/test_fhiaims.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_gms.py b/testsuite/MDAnalysisTests/topology/test_gms.py index cb187adad37..65935c14baf 100644 --- a/testsuite/MDAnalysisTests/topology/test_gms.py +++ b/testsuite/MDAnalysisTests/topology/test_gms.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_gro.py b/testsuite/MDAnalysisTests/topology/test_gro.py index f95deea52b0..f9d506fdba5 100644 --- a/testsuite/MDAnalysisTests/topology/test_gro.py +++ b/testsuite/MDAnalysisTests/topology/test_gro.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_gsd.py b/testsuite/MDAnalysisTests/topology/test_gsd.py index b80df4ede11..d183642013c 100644 --- a/testsuite/MDAnalysisTests/topology/test_gsd.py +++ b/testsuite/MDAnalysisTests/topology/test_gsd.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_guessers.py b/testsuite/MDAnalysisTests/topology/test_guessers.py index 939d147d34b..46581c6c999 100644 --- a/testsuite/MDAnalysisTests/topology/test_guessers.py +++ b/testsuite/MDAnalysisTests/topology/test_guessers.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_hoomdxml.py b/testsuite/MDAnalysisTests/topology/test_hoomdxml.py index d85a25c8465..759a2aae78d 100644 --- a/testsuite/MDAnalysisTests/topology/test_hoomdxml.py +++ b/testsuite/MDAnalysisTests/topology/test_hoomdxml.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index 9702141ee53..81a73cd3316 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_lammpsdata.py b/testsuite/MDAnalysisTests/topology/test_lammpsdata.py index 2f65eda3cbe..7e76c2e7a3d 100644 --- a/testsuite/MDAnalysisTests/topology/test_lammpsdata.py +++ b/testsuite/MDAnalysisTests/topology/test_lammpsdata.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_minimal.py b/testsuite/MDAnalysisTests/topology/test_minimal.py index 60f009b44b0..1a1cee1d6a0 100644 --- a/testsuite/MDAnalysisTests/topology/test_minimal.py +++ b/testsuite/MDAnalysisTests/topology/test_minimal.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_mol2.py b/testsuite/MDAnalysisTests/topology/test_mol2.py index b6084b861ef..604fbe63628 100644 --- a/testsuite/MDAnalysisTests/topology/test_mol2.py +++ b/testsuite/MDAnalysisTests/topology/test_mol2.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_pdb.py b/testsuite/MDAnalysisTests/topology/test_pdb.py index c176d50be13..51822e96710 100644 --- a/testsuite/MDAnalysisTests/topology/test_pdb.py +++ b/testsuite/MDAnalysisTests/topology/test_pdb.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_pdbqt.py b/testsuite/MDAnalysisTests/topology/test_pdbqt.py index b2511a889e2..9578c1e9483 100644 --- a/testsuite/MDAnalysisTests/topology/test_pdbqt.py +++ b/testsuite/MDAnalysisTests/topology/test_pdbqt.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_pqr.py b/testsuite/MDAnalysisTests/topology/test_pqr.py index aa03d789ac4..fa35171efe7 100644 --- a/testsuite/MDAnalysisTests/topology/test_pqr.py +++ b/testsuite/MDAnalysisTests/topology/test_pqr.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_psf.py b/testsuite/MDAnalysisTests/topology/test_psf.py index 895f4185146..ccfbb0bddd8 100644 --- a/testsuite/MDAnalysisTests/topology/test_psf.py +++ b/testsuite/MDAnalysisTests/topology/test_psf.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_top.py b/testsuite/MDAnalysisTests/topology/test_top.py index 853b4c12e0c..3a8227227c1 100644 --- a/testsuite/MDAnalysisTests/topology/test_top.py +++ b/testsuite/MDAnalysisTests/topology/test_top.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_topology_str_types.py b/testsuite/MDAnalysisTests/topology/test_topology_str_types.py index 8f7254702e2..66bf89b3e09 100644 --- a/testsuite/MDAnalysisTests/topology/test_topology_str_types.py +++ b/testsuite/MDAnalysisTests/topology/test_topology_str_types.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_tprparser.py b/testsuite/MDAnalysisTests/topology/test_tprparser.py index bd1444a5661..34461c3d66d 100644 --- a/testsuite/MDAnalysisTests/topology/test_tprparser.py +++ b/testsuite/MDAnalysisTests/topology/test_tprparser.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_txyz.py b/testsuite/MDAnalysisTests/topology/test_txyz.py index 72ab11d6525..06c2e757e0f 100644 --- a/testsuite/MDAnalysisTests/topology/test_txyz.py +++ b/testsuite/MDAnalysisTests/topology/test_txyz.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_xpdb.py b/testsuite/MDAnalysisTests/topology/test_xpdb.py index 617d5caf7bf..2be72fb8e7e 100644 --- a/testsuite/MDAnalysisTests/topology/test_xpdb.py +++ b/testsuite/MDAnalysisTests/topology/test_xpdb.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/topology/test_xyz.py b/testsuite/MDAnalysisTests/topology/test_xyz.py index 8ce6ce45c72..07b0159d6dc 100644 --- a/testsuite/MDAnalysisTests/topology/test_xyz.py +++ b/testsuite/MDAnalysisTests/topology/test_xyz.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/transformations/test_base.py b/testsuite/MDAnalysisTests/transformations/test_base.py index acf76ee4df8..5aa170f5604 100644 --- a/testsuite/MDAnalysisTests/transformations/test_base.py +++ b/testsuite/MDAnalysisTests/transformations/test_base.py @@ -6,7 +6,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py b/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py index 6cdc617c150..f8bb30a7f2c 100644 --- a/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py +++ b/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/transformations/test_fit.py b/testsuite/MDAnalysisTests/transformations/test_fit.py index 36469ec6d7f..9c44f88e0d1 100644 --- a/testsuite/MDAnalysisTests/transformations/test_fit.py +++ b/testsuite/MDAnalysisTests/transformations/test_fit.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/transformations/test_rotate.py b/testsuite/MDAnalysisTests/transformations/test_rotate.py index cf7342fab8f..77ffd561647 100644 --- a/testsuite/MDAnalysisTests/transformations/test_rotate.py +++ b/testsuite/MDAnalysisTests/transformations/test_rotate.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/transformations/test_translate.py b/testsuite/MDAnalysisTests/transformations/test_translate.py index 3d716e2e4d1..d8bde95009b 100644 --- a/testsuite/MDAnalysisTests/transformations/test_translate.py +++ b/testsuite/MDAnalysisTests/transformations/test_translate.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/transformations/test_wrap.py b/testsuite/MDAnalysisTests/transformations/test_wrap.py index 8b9cfecf2d9..a9fa34a36a4 100644 --- a/testsuite/MDAnalysisTests/transformations/test_wrap.py +++ b/testsuite/MDAnalysisTests/transformations/test_wrap.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/util.py b/testsuite/MDAnalysisTests/util.py index 57b65df42c8..549a9f418a2 100644 --- a/testsuite/MDAnalysisTests/util.py +++ b/testsuite/MDAnalysisTests/util.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_authors.py b/testsuite/MDAnalysisTests/utils/test_authors.py index 67131e5034e..7e1a69960d2 100644 --- a/testsuite/MDAnalysisTests/utils/test_authors.py +++ b/testsuite/MDAnalysisTests/utils/test_authors.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_datafiles.py b/testsuite/MDAnalysisTests/utils/test_datafiles.py index 7f2f1ec2e9f..92caf2348f6 100644 --- a/testsuite/MDAnalysisTests/utils/test_datafiles.py +++ b/testsuite/MDAnalysisTests/utils/test_datafiles.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_duecredit.py b/testsuite/MDAnalysisTests/utils/test_duecredit.py index adb32a5f50d..d567d256f5d 100644 --- a/testsuite/MDAnalysisTests/utils/test_duecredit.py +++ b/testsuite/MDAnalysisTests/utils/test_duecredit.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_failure.py b/testsuite/MDAnalysisTests/utils/test_failure.py index 551e9c4e103..b1ec9f1e869 100644 --- a/testsuite/MDAnalysisTests/utils/test_failure.py +++ b/testsuite/MDAnalysisTests/utils/test_failure.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_imports.py b/testsuite/MDAnalysisTests/utils/test_imports.py index 6d52b162ae3..bd343e2b992 100644 --- a/testsuite/MDAnalysisTests/utils/test_imports.py +++ b/testsuite/MDAnalysisTests/utils/test_imports.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_log.py b/testsuite/MDAnalysisTests/utils/test_log.py index dbe21c3de9d..2e95edb39b0 100644 --- a/testsuite/MDAnalysisTests/utils/test_log.py +++ b/testsuite/MDAnalysisTests/utils/test_log.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_meta.py b/testsuite/MDAnalysisTests/utils/test_meta.py index 15a30961c44..def0a35e740 100644 --- a/testsuite/MDAnalysisTests/utils/test_meta.py +++ b/testsuite/MDAnalysisTests/utils/test_meta.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_modelling.py b/testsuite/MDAnalysisTests/utils/test_modelling.py index cdd0711a36e..bae825da3ac 100644 --- a/testsuite/MDAnalysisTests/utils/test_modelling.py +++ b/testsuite/MDAnalysisTests/utils/test_modelling.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_persistence.py b/testsuite/MDAnalysisTests/utils/test_persistence.py index 4fda0de2efe..c2c00e7396d 100644 --- a/testsuite/MDAnalysisTests/utils/test_persistence.py +++ b/testsuite/MDAnalysisTests/utils/test_persistence.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_pickleio.py b/testsuite/MDAnalysisTests/utils/test_pickleio.py index 824261ed218..64dc6a9a66b 100644 --- a/testsuite/MDAnalysisTests/utils/test_pickleio.py +++ b/testsuite/MDAnalysisTests/utils/test_pickleio.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_qcprot.py b/testsuite/MDAnalysisTests/utils/test_qcprot.py index f7166b74cc9..484fb78c59e 100644 --- a/testsuite/MDAnalysisTests/utils/test_qcprot.py +++ b/testsuite/MDAnalysisTests/utils/test_qcprot.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_selections.py b/testsuite/MDAnalysisTests/utils/test_selections.py index 2dec2698d8d..d271f2f09f6 100644 --- a/testsuite/MDAnalysisTests/utils/test_selections.py +++ b/testsuite/MDAnalysisTests/utils/test_selections.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_streamio.py b/testsuite/MDAnalysisTests/utils/test_streamio.py index f03fa3748e7..53eb1a95c8e 100644 --- a/testsuite/MDAnalysisTests/utils/test_streamio.py +++ b/testsuite/MDAnalysisTests/utils/test_streamio.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_transformations.py b/testsuite/MDAnalysisTests/utils/test_transformations.py index 28811047560..8a3a4baec98 100644 --- a/testsuite/MDAnalysisTests/utils/test_transformations.py +++ b/testsuite/MDAnalysisTests/utils/test_transformations.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/utils/test_units.py b/testsuite/MDAnalysisTests/utils/test_units.py index 618550396e5..7789df13597 100644 --- a/testsuite/MDAnalysisTests/utils/test_units.py +++ b/testsuite/MDAnalysisTests/utils/test_units.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/MDAnalysisTests/visualization/test_streamlines.py b/testsuite/MDAnalysisTests/visualization/test_streamlines.py index e60179dcf1e..767903c74ad 100644 --- a/testsuite/MDAnalysisTests/visualization/test_streamlines.py +++ b/testsuite/MDAnalysisTests/visualization/test_streamlines.py @@ -5,7 +5,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index c81800660f1..8f23629b706 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ "Development Status :: 6 - Mature", "Environment :: Console", "Intended Audience :: Science/Research", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows ", diff --git a/testsuite/setup.py b/testsuite/setup.py index 58f314e7b40..5f786f94d90 100755 --- a/testsuite/setup.py +++ b/testsuite/setup.py @@ -6,7 +6,7 @@ # 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 +# Released under the Lesser GNU Public Licence, v2.1 or any higher version # # Please cite your use of MDAnalysis in published work: # From c48962e1b95cd04c2e79d348be6c530b33e40ebd Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Fri, 22 Nov 2024 15:47:23 +0000 Subject: [PATCH 39/57] Fix deployment workflow (#4795) --- .github/workflows/deploy.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index dd0570bc464..7171ff3e82b 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -139,7 +139,7 @@ jobs: - name: move_test_dist run: | mkdir -p testsuite/dist - mv dist/MDAnalysisTests-* testsuite/dist + mv dist/mdanalysistests-* testsuite/dist - name: upload_source_and_wheels uses: pypa/gh-action-pypi-publish@v1.11.0 @@ -168,7 +168,7 @@ jobs: - name: move_test_dist run: | mkdir -p testsuite/dist - mv dist/MDAnalysisTests-* testsuite/dist + mv dist/mdanalysistests-* testsuite/dist - name: upload_tests uses: pypa/gh-action-pypi-publish@v1.11.0 @@ -198,7 +198,7 @@ jobs: - name: move_test_dist run: | mkdir -p testsuite/dist - mv dist/MDAnalysisTests-* testsuite/dist + mv dist/mdanalysistests-* testsuite/dist - name: upload_source_and_wheels uses: pypa/gh-action-pypi-publish@v1.11.0 @@ -224,7 +224,7 @@ jobs: - name: move_test_dist run: | mkdir -p testsuite/dist - mv dist/MDAnalysisTests-* testsuite/dist + mv dist/mdanalysistests-* testsuite/dist - name: upload_tests uses: pypa/gh-action-pypi-publish@v1.11.0 From 277f8ee3daa882d137f50bed4794560b5ad972b2 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:29:03 +0100 Subject: [PATCH 40/57] Addition of test for missing aggregation function when `require_all_aggregators=True` (#4770) - fix #4650 - add `test_missing_aggregator` test. --- testsuite/MDAnalysisTests/analysis/test_results.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/testsuite/MDAnalysisTests/analysis/test_results.py b/testsuite/MDAnalysisTests/analysis/test_results.py index 97d299de101..e3d8fa6ca95 100644 --- a/testsuite/MDAnalysisTests/analysis/test_results.py +++ b/testsuite/MDAnalysisTests/analysis/test_results.py @@ -5,6 +5,7 @@ import pytest from MDAnalysis.analysis import results as results_module from numpy.testing import assert_equal +from itertools import cycle class Test_Results: @@ -155,8 +156,6 @@ def merger(self): @pytest.mark.parametrize("n", [1, 2, 5, 14]) def test_all_results(self, results_0, results_1, merger, n): - from itertools import cycle - objects = [obj for obj, _ in zip(cycle([results_0, results_1]), range(n))] arr = [i for _, i in zip(range(n), cycle([0, 1]))] @@ -171,3 +170,13 @@ def test_all_results(self, results_0, results_1, merger, n): results = merger.merge(objects) for attr, merged_value in results.items(): assert_equal(merged_value, answers.get(attr), err_msg=f"{attr=}, {merged_value=}, {arr=}, {objects=}") + + def test_missing_aggregator(self, results_0, results_1, merger): + original_float_lookup = merger._lookup.get("float") + merger._lookup["float"] = None + + with pytest.raises(ValueError, + match="No aggregation function for key='float'"): + merger.merge([results_0, results_1], require_all_aggregators=True) + + merger._lookup["float"] = original_float_lookup From 46be788d84a6cb149d90e4493a726c3a30b3cca0 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 26 Nov 2024 22:33:08 +0100 Subject: [PATCH 41/57] [fmt] setup.py and visualization (#4726) --- package/MDAnalysis/visualization/__init__.py | 2 +- .../MDAnalysis/visualization/streamlines.py | 253 ++++++--- .../visualization/streamlines_3D.py | 446 ++++++++++----- package/pyproject.toml | 3 + package/setup.py | 522 ++++++++++-------- 5 files changed, 799 insertions(+), 427 deletions(-) diff --git a/package/MDAnalysis/visualization/__init__.py b/package/MDAnalysis/visualization/__init__.py index 66d92c5d3f7..2d713351b82 100644 --- a/package/MDAnalysis/visualization/__init__.py +++ b/package/MDAnalysis/visualization/__init__.py @@ -24,4 +24,4 @@ from . import streamlines from . import streamlines_3D -__all__ = ['streamlines', 'streamlines_3D'] +__all__ = ["streamlines", "streamlines_3D"] diff --git a/package/MDAnalysis/visualization/streamlines.py b/package/MDAnalysis/visualization/streamlines.py index 16f844f9fa3..a06200db8fc 100644 --- a/package/MDAnalysis/visualization/streamlines.py +++ b/package/MDAnalysis/visualization/streamlines.py @@ -52,16 +52,15 @@ import matplotlib.path except ImportError: raise ImportError( - '2d streamplot module requires: matplotlib.path for its ' - 'path.Path.contains_points method. The installation ' - 'instructions for the matplotlib module can be found here: ' - 'http://matplotlib.org/faq/installing_faq.html?highlight=install' - ) from None + "2d streamplot module requires: matplotlib.path for its " + "path.Path.contains_points method. The installation " + "instructions for the matplotlib module can be found here: " + "http://matplotlib.org/faq/installing_faq.html?highlight=install" + ) from None import MDAnalysis - def produce_grid(tuple_of_limits, grid_spacing): """Produce a 2D grid for the simulation system. @@ -120,12 +119,16 @@ def split_grid(grid, num_cores): # produce an array containing the cartesian coordinates of all vertices in the grid: x_array, y_array = grid grid_vertex_cartesian_array = np.dstack((x_array, y_array)) - #the grid_vertex_cartesian_array has N_rows, with each row corresponding to a column of coordinates in the grid ( + # the grid_vertex_cartesian_array has N_rows, with each row corresponding to a column of coordinates in the grid ( # so a given row has shape N_rows, 2); overall shape (N_columns_in_grid, N_rows_in_a_column, 2) - #although I'll eventually want a pure numpy/scipy/vector-based solution, for now I'll allow loops to simplify the + # although I'll eventually want a pure numpy/scipy/vector-based solution, for now I'll allow loops to simplify the # division of the cartesian coordinates into a list of the squares in the grid - list_all_squares_in_grid = [] # should eventually be a nested list of all the square vertices in the grid/system - list_parent_index_values = [] # want an ordered list of assignment indices for reconstructing the grid positions + list_all_squares_in_grid = ( + [] + ) # should eventually be a nested list of all the square vertices in the grid/system + list_parent_index_values = ( + [] + ) # want an ordered list of assignment indices for reconstructing the grid positions # in the parent process current_column = 0 while current_column < grid_vertex_cartesian_array.shape[0] - 1: @@ -134,100 +137,182 @@ def split_grid(grid, num_cores): current_row = 0 while current_row < grid_vertex_cartesian_array.shape[1] - 1: # all rows except the top row, which doesn't have a row above it for forming squares - bottom_left_vertex_current_square = grid_vertex_cartesian_array[current_column, current_row] - bottom_right_vertex_current_square = grid_vertex_cartesian_array[current_column + 1, current_row] - top_right_vertex_current_square = grid_vertex_cartesian_array[current_column + 1, current_row + 1] - top_left_vertex_current_square = grid_vertex_cartesian_array[current_column, current_row + 1] - #append the vertices of this square to the overall list of square vertices: + bottom_left_vertex_current_square = grid_vertex_cartesian_array[ + current_column, current_row + ] + bottom_right_vertex_current_square = grid_vertex_cartesian_array[ + current_column + 1, current_row + ] + top_right_vertex_current_square = grid_vertex_cartesian_array[ + current_column + 1, current_row + 1 + ] + top_left_vertex_current_square = grid_vertex_cartesian_array[ + current_column, current_row + 1 + ] + # append the vertices of this square to the overall list of square vertices: list_all_squares_in_grid.append( - [bottom_left_vertex_current_square, bottom_right_vertex_current_square, top_right_vertex_current_square, - top_left_vertex_current_square]) + [ + bottom_left_vertex_current_square, + bottom_right_vertex_current_square, + top_right_vertex_current_square, + top_left_vertex_current_square, + ] + ) list_parent_index_values.append([current_row, current_column]) current_row += 1 current_column += 1 - #split the list of square vertices [[v1,v2,v3,v4],[v1,v2,v3,v4],...,...] into roughly equally-sized sublists to + # split the list of square vertices [[v1,v2,v3,v4],[v1,v2,v3,v4],...,...] into roughly equally-sized sublists to # be distributed over the available cores on the system: - list_square_vertex_arrays_per_core = np.array_split(list_all_squares_in_grid, num_cores) - list_parent_index_values = np.array_split(list_parent_index_values, num_cores) - return [list_square_vertex_arrays_per_core, list_parent_index_values, current_row, current_column] - - -def per_core_work(topology_file_path, trajectory_file_path, list_square_vertex_arrays_this_core, MDA_selection, - start_frame, end_frame, reconstruction_index_list, maximum_delta_magnitude): + list_square_vertex_arrays_per_core = np.array_split( + list_all_squares_in_grid, num_cores + ) + list_parent_index_values = np.array_split( + list_parent_index_values, num_cores + ) + return [ + list_square_vertex_arrays_per_core, + list_parent_index_values, + current_row, + current_column, + ] + + +def per_core_work( + topology_file_path, + trajectory_file_path, + list_square_vertex_arrays_this_core, + MDA_selection, + start_frame, + end_frame, + reconstruction_index_list, + maximum_delta_magnitude, +): """Run the analysis on one core. The code to perform on a given core given the list of square vertices assigned to it. """ # obtain the relevant coordinates for particles of interest - universe_object = MDAnalysis.Universe(topology_file_path, trajectory_file_path) + universe_object = MDAnalysis.Universe( + topology_file_path, trajectory_file_path + ) list_previous_frame_centroids = [] list_previous_frame_indices = [] - #define some utility functions for trajectory iteration: + # define some utility functions for trajectory iteration: def produce_list_indices_point_in_polygon_this_frame(vertex_coord_list): list_indices_point_in_polygon = [] for square_vertices in vertex_coord_list: path_object = matplotlib.path.Path(square_vertices) - index_list_in_polygon = np.where(path_object.contains_points(relevant_particle_coordinate_array_xy)) + index_list_in_polygon = np.where( + path_object.contains_points( + relevant_particle_coordinate_array_xy + ) + ) list_indices_point_in_polygon.append(index_list_in_polygon) return list_indices_point_in_polygon def produce_list_centroids_this_frame(list_indices_in_polygon): list_centroids_this_frame = [] for indices in list_indices_in_polygon: - if not indices[0].size > 0: # if there are no particles of interest in this particular square + if ( + not indices[0].size > 0 + ): # if there are no particles of interest in this particular square list_centroids_this_frame.append(None) else: - current_coordinate_array_in_square = relevant_particle_coordinate_array_xy[indices] - current_square_indices_centroid = np.average(current_coordinate_array_in_square, axis=0) - list_centroids_this_frame.append(current_square_indices_centroid) + current_coordinate_array_in_square = ( + relevant_particle_coordinate_array_xy[indices] + ) + current_square_indices_centroid = np.average( + current_coordinate_array_in_square, axis=0 + ) + list_centroids_this_frame.append( + current_square_indices_centroid + ) return list_centroids_this_frame # a list of numpy xy centroid arrays for this frame for ts in universe_object.trajectory: if ts.frame < start_frame: # don't start until first specified frame continue - relevant_particle_coordinate_array_xy = universe_object.select_atoms(MDA_selection).positions[..., :-1] + relevant_particle_coordinate_array_xy = universe_object.select_atoms( + MDA_selection + ).positions[..., :-1] # only 2D / xy coords for now - #I will need a list of indices for relevant particles falling within each square in THIS frame: - list_indices_in_squares_this_frame = produce_list_indices_point_in_polygon_this_frame( - list_square_vertex_arrays_this_core) - #likewise, I will need a list of centroids of particles in each square (same order as above list): - list_centroids_in_squares_this_frame = produce_list_centroids_this_frame(list_indices_in_squares_this_frame) - if list_previous_frame_indices: # if the previous frame had indices in at least one square I will need to use + # I will need a list of indices for relevant particles falling within each square in THIS frame: + list_indices_in_squares_this_frame = ( + produce_list_indices_point_in_polygon_this_frame( + list_square_vertex_arrays_this_core + ) + ) + # likewise, I will need a list of centroids of particles in each square (same order as above list): + list_centroids_in_squares_this_frame = ( + produce_list_centroids_this_frame( + list_indices_in_squares_this_frame + ) + ) + if ( + list_previous_frame_indices + ): # if the previous frame had indices in at least one square I will need to use # those indices to generate the updates to the corresponding centroids in this frame: - list_centroids_this_frame_using_indices_from_last_frame = produce_list_centroids_this_frame( - list_previous_frame_indices) - #I need to write a velocity of zero if there are any 'empty' squares in either frame: + list_centroids_this_frame_using_indices_from_last_frame = ( + produce_list_centroids_this_frame(list_previous_frame_indices) + ) + # I need to write a velocity of zero if there are any 'empty' squares in either frame: xy_deltas_to_write = [] - for square_1_centroid, square_2_centroid in zip(list_centroids_this_frame_using_indices_from_last_frame, - list_previous_frame_centroids): + for square_1_centroid, square_2_centroid in zip( + list_centroids_this_frame_using_indices_from_last_frame, + list_previous_frame_centroids, + ): if square_1_centroid is None or square_2_centroid is None: xy_deltas_to_write.append([0, 0]) else: - xy_deltas_to_write.append(np.subtract(square_1_centroid, square_2_centroid).tolist()) + xy_deltas_to_write.append( + np.subtract( + square_1_centroid, square_2_centroid + ).tolist() + ) - #xy_deltas_to_write = np.subtract(np.array( + # xy_deltas_to_write = np.subtract(np.array( # list_centroids_this_frame_using_indices_from_last_frame),np.array(list_previous_frame_centroids)) xy_deltas_to_write = np.array(xy_deltas_to_write) - #now filter the array to only contain distances in the range [-8,8] as a placeholder for dealing with PBC + # now filter the array to only contain distances in the range [-8,8] as a placeholder for dealing with PBC # issues (Matthieu seemed to use a limit of 8 as well); - xy_deltas_to_write = np.clip(xy_deltas_to_write, -maximum_delta_magnitude, maximum_delta_magnitude) + xy_deltas_to_write = np.clip( + xy_deltas_to_write, + -maximum_delta_magnitude, + maximum_delta_magnitude, + ) - #with the xy and dx,dy values calculated I need to set the values from this frame to previous frame + # with the xy and dx,dy values calculated I need to set the values from this frame to previous frame # values in anticipation of the next frame: - list_previous_frame_centroids = list_centroids_in_squares_this_frame[:] + list_previous_frame_centroids = ( + list_centroids_in_squares_this_frame[:] + ) list_previous_frame_indices = list_indices_in_squares_this_frame[:] else: # either no points in squares or after the first frame I'll just reset the 'previous' values so they # can be used when consecutive frames have proper values - list_previous_frame_centroids = list_centroids_in_squares_this_frame[:] + list_previous_frame_centroids = ( + list_centroids_in_squares_this_frame[:] + ) list_previous_frame_indices = list_indices_in_squares_this_frame[:] if ts.frame > end_frame: break # stop here return list(zip(reconstruction_index_list, xy_deltas_to_write.tolist())) -def generate_streamlines(topology_file_path, trajectory_file_path, grid_spacing, MDA_selection, start_frame, - end_frame, xmin, xmax, ymin, ymax, maximum_delta_magnitude, num_cores='maximum'): +def generate_streamlines( + topology_file_path, + trajectory_file_path, + grid_spacing, + MDA_selection, + start_frame, + end_frame, + xmin, + xmax, + ymin, + ymax, + maximum_delta_magnitude, + num_cores="maximum", +): r"""Produce the x and y components of a 2D streamplot data set. Parameters @@ -311,35 +396,58 @@ def generate_streamlines(topology_file_path, trajectory_file_path, grid_spacing, """ # work out the number of cores to use: - if num_cores == 'maximum': + if num_cores == "maximum": num_cores = multiprocessing.cpu_count() # use all available cores else: num_cores = num_cores # use the value specified by the user - #assert isinstance(num_cores,(int,long)), "The number of specified cores must (of course) be an integer." - np.seterr(all='warn', over='raise') + # assert isinstance(num_cores,(int,long)), "The number of specified cores must (of course) be an integer." + np.seterr(all="warn", over="raise") parent_list_deltas = [] # collect all data from child processes here def log_result_to_parent(delta_array): parent_list_deltas.extend(delta_array) tuple_of_limits = (xmin, xmax, ymin, ymax) - grid = produce_grid(tuple_of_limits=tuple_of_limits, grid_spacing=grid_spacing) - list_square_vertex_arrays_per_core, list_parent_index_values, total_rows, total_columns = \ - split_grid(grid=grid, - num_cores=num_cores) + grid = produce_grid( + tuple_of_limits=tuple_of_limits, grid_spacing=grid_spacing + ) + ( + list_square_vertex_arrays_per_core, + list_parent_index_values, + total_rows, + total_columns, + ) = split_grid(grid=grid, num_cores=num_cores) pool = multiprocessing.Pool(num_cores) - for vertex_sublist, index_sublist in zip(list_square_vertex_arrays_per_core, list_parent_index_values): - pool.apply_async(per_core_work, args=( - topology_file_path, trajectory_file_path, vertex_sublist, MDA_selection, start_frame, end_frame, - index_sublist, maximum_delta_magnitude), callback=log_result_to_parent) + for vertex_sublist, index_sublist in zip( + list_square_vertex_arrays_per_core, list_parent_index_values + ): + pool.apply_async( + per_core_work, + args=( + topology_file_path, + trajectory_file_path, + vertex_sublist, + MDA_selection, + start_frame, + end_frame, + index_sublist, + maximum_delta_magnitude, + ), + callback=log_result_to_parent, + ) pool.close() pool.join() dx_array = np.zeros((total_rows, total_columns)) dy_array = np.zeros((total_rows, total_columns)) - #the parent_list_deltas is shaped like this: [ ([row_index,column_index],[dx,dy]), ... (...),...,] - for index_array, delta_array in parent_list_deltas: # go through the list in the parent process and assign to the + # the parent_list_deltas is shaped like this: [ ([row_index,column_index],[dx,dy]), ... (...),...,] + for ( + index_array, + delta_array, + ) in ( + parent_list_deltas + ): # go through the list in the parent process and assign to the # appropriate positions in the dx and dy matrices: - #build in a filter to replace all values at the cap (currently between -8,8) with 0 to match Matthieu's code + # build in a filter to replace all values at the cap (currently between -8,8) with 0 to match Matthieu's code # (I think eventually we'll reduce the cap to a narrower boundary though) index_1 = index_array.tolist()[0] index_2 = index_array.tolist()[1] @@ -352,9 +460,14 @@ def log_result_to_parent(delta_array): else: dy_array[index_1, index_2] = delta_array[1] - #at Matthieu's request, we now want to calculate the average and standard deviation of the displacement values: - displacement_array = np.sqrt(dx_array ** 2 + dy_array ** 2) + # at Matthieu's request, we now want to calculate the average and standard deviation of the displacement values: + displacement_array = np.sqrt(dx_array**2 + dy_array**2) average_displacement = np.average(displacement_array) standard_deviation_of_displacement = np.std(displacement_array) - return (dx_array, dy_array, average_displacement, standard_deviation_of_displacement) + return ( + dx_array, + dy_array, + average_displacement, + standard_deviation_of_displacement, + ) diff --git a/package/MDAnalysis/visualization/streamlines_3D.py b/package/MDAnalysis/visualization/streamlines_3D.py index 7e48b138fd4..4d2dace77bb 100644 --- a/package/MDAnalysis/visualization/streamlines_3D.py +++ b/package/MDAnalysis/visualization/streamlines_3D.py @@ -56,8 +56,9 @@ import MDAnalysis -def determine_container_limits(topology_file_path, trajectory_file_path, - buffer_value): +def determine_container_limits( + topology_file_path, trajectory_file_path, buffer_value +): """Calculate the extent of the atom coordinates + buffer. A function for the parent process which should take the input trajectory @@ -73,19 +74,29 @@ def determine_container_limits(topology_file_path, trajectory_file_path, buffer_value : float buffer value (padding) in +/- {x, y, z} """ - universe_object = MDAnalysis.Universe(topology_file_path, trajectory_file_path) - all_atom_selection = universe_object.select_atoms('all') # select all particles + universe_object = MDAnalysis.Universe( + topology_file_path, trajectory_file_path + ) + all_atom_selection = universe_object.select_atoms( + "all" + ) # select all particles all_atom_coordinate_array = all_atom_selection.positions x_min, x_max, y_min, y_max, z_min, z_max = [ all_atom_coordinate_array[..., 0].min(), - all_atom_coordinate_array[..., 0].max(), all_atom_coordinate_array[..., 1].min(), - all_atom_coordinate_array[..., 1].max(), all_atom_coordinate_array[..., 2].min(), - all_atom_coordinate_array[..., 2].max()] - tuple_of_limits = \ - ( - x_min - buffer_value, - x_max + buffer_value, y_min - buffer_value, y_max + buffer_value, z_min - buffer_value, - z_max + buffer_value) # using buffer_value to catch particles near edges + all_atom_coordinate_array[..., 0].max(), + all_atom_coordinate_array[..., 1].min(), + all_atom_coordinate_array[..., 1].max(), + all_atom_coordinate_array[..., 2].min(), + all_atom_coordinate_array[..., 2].max(), + ] + tuple_of_limits = ( + x_min - buffer_value, + x_max + buffer_value, + y_min - buffer_value, + y_max + buffer_value, + z_min - buffer_value, + z_max + buffer_value, + ) # using buffer_value to catch particles near edges return tuple_of_limits @@ -109,7 +120,11 @@ def produce_grid(tuple_of_limits, grid_spacing): """ x_min, x_max, y_min, y_max, z_min, z_max = tuple_of_limits - grid = np.mgrid[x_min:x_max:grid_spacing, y_min:y_max:grid_spacing, z_min:z_max:grid_spacing] + grid = np.mgrid[ + x_min:x_max:grid_spacing, + y_min:y_max:grid_spacing, + z_min:z_max:grid_spacing, + ] return grid @@ -139,78 +154,124 @@ def split_grid(grid, num_cores): num_z_values = z.shape[-1] num_sheets = z.shape[0] delta_array_shape = tuple( - [n - 1 for n in x.shape]) # the final target shape for return delta arrays is n-1 in each dimension + [n - 1 for n in x.shape] + ) # the final target shape for return delta arrays is n-1 in each dimension ordered_list_per_sheet_x_values = [] - for x_sheet in x: # each x_sheet should have shape (25,23) and the same x value in each element + for ( + x_sheet + ) in ( + x + ): # each x_sheet should have shape (25,23) and the same x value in each element array_all_x_values_current_sheet = x_sheet.flatten() - ordered_list_per_sheet_x_values.append(array_all_x_values_current_sheet) + ordered_list_per_sheet_x_values.append( + array_all_x_values_current_sheet + ) ordered_list_per_sheet_y_values = [] for y_columns in y: array_all_y_values_current_sheet = y_columns.flatten() - ordered_list_per_sheet_y_values.append(array_all_y_values_current_sheet) + ordered_list_per_sheet_y_values.append( + array_all_y_values_current_sheet + ) ordered_list_per_sheet_z_values = [] for z_slices in z: array_all_z_values_current_sheet = z_slices.flatten() - ordered_list_per_sheet_z_values.append(array_all_z_values_current_sheet) + ordered_list_per_sheet_z_values.append( + array_all_z_values_current_sheet + ) ordered_list_cartesian_coordinates_per_sheet = [] - for x_sheet_coords, y_sheet_coords, z_sheet_coords in zip(ordered_list_per_sheet_x_values, - ordered_list_per_sheet_y_values, - ordered_list_per_sheet_z_values): - ordered_list_cartesian_coordinates_per_sheet.append(list(zip(x_sheet_coords, y_sheet_coords, z_sheet_coords))) - array_ordered_cartesian_coords_per_sheet = np.array(ordered_list_cartesian_coordinates_per_sheet) - #now I'm going to want to build cubes in an ordered fashion, and in such a way that I can track the index / + for x_sheet_coords, y_sheet_coords, z_sheet_coords in zip( + ordered_list_per_sheet_x_values, + ordered_list_per_sheet_y_values, + ordered_list_per_sheet_z_values, + ): + ordered_list_cartesian_coordinates_per_sheet.append( + list(zip(x_sheet_coords, y_sheet_coords, z_sheet_coords)) + ) + array_ordered_cartesian_coords_per_sheet = np.array( + ordered_list_cartesian_coordinates_per_sheet + ) + # now I'm going to want to build cubes in an ordered fashion, and in such a way that I can track the index / # centroid of each cube for domain decomposition / reconstruction and mayavi mlab.flow() input - #cubes will be formed from N - 1 base sheets combined with subsequent sheets + # cubes will be formed from N - 1 base sheets combined with subsequent sheets current_base_sheet = 0 dictionary_cubes_centroids_indices = {} cube_counter = 0 while current_base_sheet < num_sheets - 1: - current_base_sheet_array = array_ordered_cartesian_coords_per_sheet[current_base_sheet] + current_base_sheet_array = array_ordered_cartesian_coords_per_sheet[ + current_base_sheet + ] current_top_sheet_array = array_ordered_cartesian_coords_per_sheet[ - current_base_sheet + 1] # the points of the sheet 'to the right' in the grid + current_base_sheet + 1 + ] # the points of the sheet 'to the right' in the grid current_index = 0 while current_index < current_base_sheet_array.shape[0] - num_z_values: # iterate through all the indices in each of the sheet arrays (careful to avoid extra # points not needed for cubes) - column_z_level = 0 # start at the bottom of a given 4-point column and work up + column_z_level = ( + 0 # start at the bottom of a given 4-point column and work up + ) while column_z_level < num_z_values - 1: current_list_cube_vertices = [] - first_two_vertices_base_sheet = current_base_sheet_array[current_index:current_index + 2, ...].tolist() - first_two_vertices_top_sheet = current_top_sheet_array[current_index:current_index + 2, ...].tolist() - next_two_vertices_base_sheet = current_base_sheet_array[current_index + - num_z_values: 2 + - num_z_values + current_index, ...].tolist() - next_two_vertices_top_sheet = current_top_sheet_array[current_index + - num_z_values: 2 + - num_z_values + current_index, ...].tolist() + first_two_vertices_base_sheet = current_base_sheet_array[ + current_index : current_index + 2, ... + ].tolist() + first_two_vertices_top_sheet = current_top_sheet_array[ + current_index : current_index + 2, ... + ].tolist() + next_two_vertices_base_sheet = current_base_sheet_array[ + current_index + + num_z_values : 2 + + num_z_values + + current_index, + ..., + ].tolist() + next_two_vertices_top_sheet = current_top_sheet_array[ + current_index + + num_z_values : 2 + + num_z_values + + current_index, + ..., + ].tolist() for vertex_set in [ - first_two_vertices_base_sheet, first_two_vertices_top_sheet, - next_two_vertices_base_sheet, next_two_vertices_top_sheet + first_two_vertices_base_sheet, + first_two_vertices_top_sheet, + next_two_vertices_base_sheet, + next_two_vertices_top_sheet, ]: current_list_cube_vertices.extend(vertex_set) vertex_array = np.array(current_list_cube_vertices) - assert vertex_array.shape == (8, 3), "vertex_array has incorrect shape" - cube_centroid = np.average(np.array(current_list_cube_vertices), axis=0) + assert vertex_array.shape == ( + 8, + 3, + ), "vertex_array has incorrect shape" + cube_centroid = np.average( + np.array(current_list_cube_vertices), axis=0 + ) dictionary_cubes_centroids_indices[cube_counter] = { - 'centroid': cube_centroid, - 'vertex_list': current_list_cube_vertices} + "centroid": cube_centroid, + "vertex_list": current_list_cube_vertices, + } cube_counter += 1 current_index += 1 column_z_level += 1 - if column_z_level == num_z_values - 1: # the loop will break but I should also increment the + if ( + column_z_level == num_z_values - 1 + ): # the loop will break but I should also increment the # current_index current_index += 1 current_base_sheet += 1 total_cubes = len(dictionary_cubes_centroids_indices) - #produce an array of pseudo cube indices (actually the dictionary keys which are cube numbers in string format): + # produce an array of pseudo cube indices (actually the dictionary keys which are cube numbers in string format): pseudo_cube_indices = np.arange(0, total_cubes) - sublist_of_cube_indices_per_core = np.array_split(pseudo_cube_indices, num_cores) - #now, the split of pseudoindices seems to work well, and the above sublist_of_cube_indices_per_core is a list of + sublist_of_cube_indices_per_core = np.array_split( + pseudo_cube_indices, num_cores + ) + # now, the split of pseudoindices seems to work well, and the above sublist_of_cube_indices_per_core is a list of # arrays of cube numbers / keys in the original dictionary - #now I think I'll try to produce a list of dictionaries that each contain their assigned cubes based on the above + # now I think I'll try to produce a list of dictionaries that each contain their assigned cubes based on the above # per core split list_dictionaries_for_cores = [] subdictionary_counter = 0 @@ -224,11 +285,22 @@ def split_grid(grid, num_cores): items_popped += 1 list_dictionaries_for_cores.append(current_core_dictionary) subdictionary_counter += 1 - return list_dictionaries_for_cores, total_cubes, num_sheets, delta_array_shape - - -def per_core_work(start_frame_coord_array, end_frame_coord_array, dictionary_cube_data_this_core, MDA_selection, - start_frame, end_frame): + return ( + list_dictionaries_for_cores, + total_cubes, + num_sheets, + delta_array_shape, + ) + + +def per_core_work( + start_frame_coord_array, + end_frame_coord_array, + dictionary_cube_data_this_core, + MDA_selection, + start_frame, + end_frame, +): """Run the analysis on one core. The code to perform on a given core given the dictionary of cube data. @@ -237,82 +309,137 @@ def per_core_work(start_frame_coord_array, end_frame_coord_array, dictionary_cub list_previous_frame_indices = [] # define some utility functions for trajectory iteration: - def point_in_cube(array_point_coordinates, list_cube_vertices, cube_centroid): + def point_in_cube( + array_point_coordinates, list_cube_vertices, cube_centroid + ): """Determine if an array of coordinates are within a cube.""" - #the simulation particle point can't be more than half the cube side length away from the cube centroid in + # the simulation particle point can't be more than half the cube side length away from the cube centroid in # any given dimension: array_cube_vertices = np.array(list_cube_vertices) - cube_half_side_length = scipy.spatial.distance.pdist(array_cube_vertices, 'euclidean').min() / 2.0 - array_cube_vertex_distances_from_centroid = scipy.spatial.distance.cdist(array_cube_vertices, - cube_centroid[np.newaxis, :]) - np.testing.assert_allclose(array_cube_vertex_distances_from_centroid.min(), - array_cube_vertex_distances_from_centroid.max(), rtol=0, atol=1.5e-4, - err_msg="not all cube vertex to centroid distances are the same, " - "so not a true cube") - absolute_delta_coords = np.absolute(np.subtract(array_point_coordinates, cube_centroid)) + cube_half_side_length = ( + scipy.spatial.distance.pdist( + array_cube_vertices, "euclidean" + ).min() + / 2.0 + ) + array_cube_vertex_distances_from_centroid = ( + scipy.spatial.distance.cdist( + array_cube_vertices, cube_centroid[np.newaxis, :] + ) + ) + np.testing.assert_allclose( + array_cube_vertex_distances_from_centroid.min(), + array_cube_vertex_distances_from_centroid.max(), + rtol=0, + atol=1.5e-4, + err_msg="not all cube vertex to centroid distances are the same, " + "so not a true cube", + ) + absolute_delta_coords = np.absolute( + np.subtract(array_point_coordinates, cube_centroid) + ) absolute_delta_x_coords = absolute_delta_coords[..., 0] - indices_delta_x_acceptable = np.where(absolute_delta_x_coords <= cube_half_side_length) + indices_delta_x_acceptable = np.where( + absolute_delta_x_coords <= cube_half_side_length + ) absolute_delta_y_coords = absolute_delta_coords[..., 1] - indices_delta_y_acceptable = np.where(absolute_delta_y_coords <= cube_half_side_length) + indices_delta_y_acceptable = np.where( + absolute_delta_y_coords <= cube_half_side_length + ) absolute_delta_z_coords = absolute_delta_coords[..., 2] - indices_delta_z_acceptable = np.where(absolute_delta_z_coords <= cube_half_side_length) - intersection_xy_acceptable_arrays = np.intersect1d(indices_delta_x_acceptable[0], - indices_delta_y_acceptable[0]) - overall_indices_points_in_current_cube = np.intersect1d(intersection_xy_acceptable_arrays, - indices_delta_z_acceptable[0]) + indices_delta_z_acceptable = np.where( + absolute_delta_z_coords <= cube_half_side_length + ) + intersection_xy_acceptable_arrays = np.intersect1d( + indices_delta_x_acceptable[0], indices_delta_y_acceptable[0] + ) + overall_indices_points_in_current_cube = np.intersect1d( + intersection_xy_acceptable_arrays, indices_delta_z_acceptable[0] + ) return overall_indices_points_in_current_cube - def update_dictionary_point_in_cube_start_frame(array_simulation_particle_coordinates, - dictionary_cube_data_this_core): + def update_dictionary_point_in_cube_start_frame( + array_simulation_particle_coordinates, dictionary_cube_data_this_core + ): """Basically update the cube dictionary objects assigned to this core to contain a new key/value pair corresponding to the indices of the relevant particles that fall within a given cube. Also, for a given cube, - store a key/value pair for the centroid of the particles that fall within the cube.""" + store a key/value pair for the centroid of the particles that fall within the cube. + """ cube_counter = 0 for key, cube in dictionary_cube_data_this_core.items(): - index_list_in_cube = point_in_cube(array_simulation_particle_coordinates, cube['vertex_list'], - cube['centroid']) - cube['start_frame_index_list_in_cube'] = index_list_in_cube - if len(index_list_in_cube) > 0: # if there's at least one particle in this cube - centroid_particles_in_cube = np.average(array_simulation_particle_coordinates[index_list_in_cube], - axis=0) - cube['centroid_of_particles_first_frame'] = centroid_particles_in_cube + index_list_in_cube = point_in_cube( + array_simulation_particle_coordinates, + cube["vertex_list"], + cube["centroid"], + ) + cube["start_frame_index_list_in_cube"] = index_list_in_cube + if ( + len(index_list_in_cube) > 0 + ): # if there's at least one particle in this cube + centroid_particles_in_cube = np.average( + array_simulation_particle_coordinates[index_list_in_cube], + axis=0, + ) + cube["centroid_of_particles_first_frame"] = ( + centroid_particles_in_cube + ) else: # empty cube - cube['centroid_of_particles_first_frame'] = None + cube["centroid_of_particles_first_frame"] = None cube_counter += 1 - def update_dictionary_end_frame(array_simulation_particle_coordinates, dictionary_cube_data_this_core): + def update_dictionary_end_frame( + array_simulation_particle_coordinates, dictionary_cube_data_this_core + ): """Update the cube dictionary objects again as appropriate for the second and final frame.""" cube_counter = 0 for key, cube in dictionary_cube_data_this_core.items(): # if there were no particles in the cube in the first frame, then set dx,dy,dz each to 0 - if cube['centroid_of_particles_first_frame'] is None: - cube['dx'] = 0 - cube['dy'] = 0 - cube['dz'] = 0 + if cube["centroid_of_particles_first_frame"] is None: + cube["dx"] = 0 + cube["dy"] = 0 + cube["dz"] = 0 else: # there was at least one particle in the starting cube so we can get dx,dy,dz centroid values - new_coordinate_array_for_particles_starting_in_this_cube = array_simulation_particle_coordinates[ - cube['start_frame_index_list_in_cube']] + new_coordinate_array_for_particles_starting_in_this_cube = ( + array_simulation_particle_coordinates[ + cube["start_frame_index_list_in_cube"] + ] + ) new_centroid_for_particles_starting_in_this_cube = np.average( - new_coordinate_array_for_particles_starting_in_this_cube, axis=0) - cube['centroid_of_paticles_final_frame'] = new_centroid_for_particles_starting_in_this_cube - delta_centroid_array_this_cube = new_centroid_for_particles_starting_in_this_cube - cube[ - 'centroid_of_particles_first_frame'] - cube['dx'] = delta_centroid_array_this_cube[0] - cube['dy'] = delta_centroid_array_this_cube[1] - cube['dz'] = delta_centroid_array_this_cube[2] + new_coordinate_array_for_particles_starting_in_this_cube, + axis=0, + ) + cube["centroid_of_paticles_final_frame"] = ( + new_centroid_for_particles_starting_in_this_cube + ) + delta_centroid_array_this_cube = ( + new_centroid_for_particles_starting_in_this_cube + - cube["centroid_of_particles_first_frame"] + ) + cube["dx"] = delta_centroid_array_this_cube[0] + cube["dy"] = delta_centroid_array_this_cube[1] + cube["dz"] = delta_centroid_array_this_cube[2] cube_counter += 1 - #now that the parent process is dealing with the universe object & grabbing required coordinates, each child + # now that the parent process is dealing with the universe object & grabbing required coordinates, each child # process only needs to take the coordinate arrays & perform the operations with its assigned cubes (no more file # opening and trajectory iteration on each core--which I'm hoping will substantially reduce the physical memory # footprint of my 3D streamplot code) - update_dictionary_point_in_cube_start_frame(start_frame_coord_array, dictionary_cube_data_this_core) - update_dictionary_end_frame(end_frame_coord_array, dictionary_cube_data_this_core) + update_dictionary_point_in_cube_start_frame( + start_frame_coord_array, dictionary_cube_data_this_core + ) + update_dictionary_end_frame( + end_frame_coord_array, dictionary_cube_data_this_core + ) return dictionary_cube_data_this_core -def produce_coordinate_arrays_single_process(topology_file_path, trajectory_file_path, MDA_selection, start_frame, - end_frame): +def produce_coordinate_arrays_single_process( + topology_file_path, + trajectory_file_path, + MDA_selection, + start_frame, + end_frame, +): """Generate coordinate arrays. To reduce memory footprint produce only a single MDA selection and get @@ -321,24 +448,46 @@ def produce_coordinate_arrays_single_process(topology_file_path, trajectory_file waste memory. """ - universe_object = MDAnalysis.Universe(topology_file_path, trajectory_file_path) + universe_object = MDAnalysis.Universe( + topology_file_path, trajectory_file_path + ) relevant_particles = universe_object.select_atoms(MDA_selection) # pull out coordinate arrays from desired frames: for ts in universe_object.trajectory: if ts.frame > end_frame: break # stop here if ts.frame == start_frame: - start_frame_relevant_particle_coordinate_array_xyz = relevant_particles.positions + start_frame_relevant_particle_coordinate_array_xyz = ( + relevant_particles.positions + ) elif ts.frame == end_frame: - end_frame_relevant_particle_coordinate_array_xyz = relevant_particles.positions + end_frame_relevant_particle_coordinate_array_xyz = ( + relevant_particles.positions + ) else: continue - return (start_frame_relevant_particle_coordinate_array_xyz, end_frame_relevant_particle_coordinate_array_xyz) - - -def generate_streamlines_3d(topology_file_path, trajectory_file_path, grid_spacing, MDA_selection, start_frame, - end_frame, xmin, xmax, ymin, ymax, zmin, zmax, maximum_delta_magnitude=2.0, - num_cores='maximum'): + return ( + start_frame_relevant_particle_coordinate_array_xyz, + end_frame_relevant_particle_coordinate_array_xyz, + ) + + +def generate_streamlines_3d( + topology_file_path, + trajectory_file_path, + grid_spacing, + MDA_selection, + start_frame, + end_frame, + xmin, + xmax, + ymin, + ymax, + zmin, + zmax, + maximum_delta_magnitude=2.0, + num_cores="maximum", +): r"""Produce the x, y and z components of a 3D streamplot data set. Parameters @@ -439,68 +588,91 @@ def generate_streamlines_3d(topology_file_path, trajectory_file_path, grid_spaci .. _mayavi: http://docs.enthought.com/mayavi/mayavi/ """ # work out the number of cores to use: - if num_cores == 'maximum': + if num_cores == "maximum": num_cores = multiprocessing.cpu_count() # use all available cores else: num_cores = num_cores # use the value specified by the user # assert isinstance(num_cores,(int,long)), "The number of specified cores must (of course) be an integer." - np.seterr(all='warn', over='raise') + np.seterr(all="warn", over="raise") parent_cube_dictionary = {} # collect all data from child processes here def log_result_to_parent(process_dict): parent_cube_dictionary.update(process_dict) - #step 1: produce tuple of cartesian coordinate limits for the first frame - #tuple_of_limits = determine_container_limits(topology_file_path = topology_file_path,trajectory_file_path = + # step 1: produce tuple of cartesian coordinate limits for the first frame + # tuple_of_limits = determine_container_limits(topology_file_path = topology_file_path,trajectory_file_path = # trajectory_file_path,buffer_value=buffer_value) tuple_of_limits = (xmin, xmax, ymin, ymax, zmin, zmax) - #step 2: produce a suitable grid (will assume that grid size / container size does not vary during simulation--or + # step 2: produce a suitable grid (will assume that grid size / container size does not vary during simulation--or # at least not beyond the buffer limit, such that this grid can be used for all subsequent frames) - grid = produce_grid(tuple_of_limits=tuple_of_limits, grid_spacing=grid_spacing) - #step 3: split the grid into a dictionary of cube information that can be sent to each core for processing: - list_dictionaries_for_cores, total_cubes, num_sheets, delta_array_shape = split_grid(grid=grid, num_cores=num_cores) - #step 3b: produce required coordinate arrays on a single core to avoid making a universe object on each core: - start_frame_coord_array, end_frame_coord_array = produce_coordinate_arrays_single_process(topology_file_path, - trajectory_file_path, - MDA_selection, - start_frame, end_frame) - #step 4: per process work using the above grid data split + grid = produce_grid( + tuple_of_limits=tuple_of_limits, grid_spacing=grid_spacing + ) + # step 3: split the grid into a dictionary of cube information that can be sent to each core for processing: + list_dictionaries_for_cores, total_cubes, num_sheets, delta_array_shape = ( + split_grid(grid=grid, num_cores=num_cores) + ) + # step 3b: produce required coordinate arrays on a single core to avoid making a universe object on each core: + start_frame_coord_array, end_frame_coord_array = ( + produce_coordinate_arrays_single_process( + topology_file_path, + trajectory_file_path, + MDA_selection, + start_frame, + end_frame, + ) + ) + # step 4: per process work using the above grid data split pool = multiprocessing.Pool(num_cores) for sub_dictionary_of_cube_data in list_dictionaries_for_cores: - pool.apply_async(per_core_work, args=( - start_frame_coord_array, end_frame_coord_array, sub_dictionary_of_cube_data, MDA_selection, start_frame, - end_frame), callback=log_result_to_parent) + pool.apply_async( + per_core_work, + args=( + start_frame_coord_array, + end_frame_coord_array, + sub_dictionary_of_cube_data, + MDA_selection, + start_frame, + end_frame, + ), + callback=log_result_to_parent, + ) pool.close() pool.join() - #so, at this stage the parent process now has a single dictionary with all the cube objects updated from all + # so, at this stage the parent process now has a single dictionary with all the cube objects updated from all # available cores - #the 3D streamplot (i.e, mayavi flow() function) will require separate 3D np arrays for dx,dy,dz - #the shape of each 3D array will unfortunately have to match the mgrid data structure (bit of a pain): ( + # the 3D streamplot (i.e, mayavi flow() function) will require separate 3D np arrays for dx,dy,dz + # the shape of each 3D array will unfortunately have to match the mgrid data structure (bit of a pain): ( # num_sheets - 1, num_sheets - 1, cubes_per_column) cubes_per_sheet = int(float(total_cubes) / float(num_sheets - 1)) - #produce dummy zero arrays for dx,dy,dz of the appropriate shape: + # produce dummy zero arrays for dx,dy,dz of the appropriate shape: dx_array = np.zeros(delta_array_shape) dy_array = np.zeros(delta_array_shape) dz_array = np.zeros(delta_array_shape) - #now use the parent cube dictionary to correctly substitute in dx,dy,dz values + # now use the parent cube dictionary to correctly substitute in dx,dy,dz values current_sheet = 0 # which is also the current row y_index_current_sheet = 0 # sub row z_index_current_column = 0 # column total_cubes_current_sheet = 0 for cube_number in range(0, total_cubes): - dx_array[current_sheet, y_index_current_sheet, z_index_current_column] = parent_cube_dictionary[cube_number][ - 'dx'] - dy_array[current_sheet, y_index_current_sheet, z_index_current_column] = parent_cube_dictionary[cube_number][ - 'dy'] - dz_array[current_sheet, y_index_current_sheet, z_index_current_column] = parent_cube_dictionary[cube_number][ - 'dz'] + dx_array[ + current_sheet, y_index_current_sheet, z_index_current_column + ] = parent_cube_dictionary[cube_number]["dx"] + dy_array[ + current_sheet, y_index_current_sheet, z_index_current_column + ] = parent_cube_dictionary[cube_number]["dy"] + dz_array[ + current_sheet, y_index_current_sheet, z_index_current_column + ] = parent_cube_dictionary[cube_number]["dz"] z_index_current_column += 1 total_cubes_current_sheet += 1 if z_index_current_column == delta_array_shape[2]: # done building current y-column so iterate y value and reset z z_index_current_column = 0 y_index_current_sheet += 1 - if y_index_current_sheet == delta_array_shape[1]: # current sheet is complete + if ( + y_index_current_sheet == delta_array_shape[1] + ): # current sheet is complete current_sheet += 1 y_index_current_sheet = 0 # restart for new sheet z_index_current_column = 0 diff --git a/package/pyproject.toml b/package/pyproject.toml index 4dc2275df49..c27a92c0296 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -130,6 +130,9 @@ include = ''' ( tables\.py | due\.py +| setup\.py +| visualization/.*\.py ) ''' +extend-exclude = '__pycache__' required-version = '24' diff --git a/package/setup.py b/package/setup.py index 3e9bf27f85e..b19a4c0fbde 100755 --- a/package/setup.py +++ b/package/setup.py @@ -60,7 +60,7 @@ # NOTE: keep in sync with MDAnalysis.__version__ in version.py RELEASE = "2.8.0" -is_release = 'dev' not in RELEASE +is_release = "dev" not in RELEASE # Handle cython modules try: @@ -68,18 +68,22 @@ # minimum cython version now set to 0.28 to match pyproject.toml import Cython from Cython.Build import cythonize + cython_found = True required_version = "0.28" if not Version(Cython.__version__) >= Version(required_version): # We don't necessarily die here. Maybe we already have # the cythonized '.c' files. - print("Cython version {0} was found but won't be used: version {1} " - "or greater is required because it offers a handy " - "parallelization module".format( - Cython.__version__, required_version)) + print( + "Cython version {0} was found but won't be used: version {1} " + "or greater is required because it offers a handy " + "parallelization module".format( + Cython.__version__, required_version + ) + ) cython_found = False - cython_linetrace = bool(os.environ.get('CYTHON_TRACE_NOGIL', False)) + cython_linetrace = bool(os.environ.get("CYTHON_TRACE_NOGIL", False)) except ImportError: cython_found = False if not is_release: @@ -88,9 +92,10 @@ sys.exit(1) cython_linetrace = False + def abspath(file): - return os.path.join(os.path.dirname(os.path.abspath(__file__)), - file) + return os.path.join(os.path.dirname(os.path.abspath(__file__)), file) + class Config(object): """Config wrapper class to get build options @@ -109,31 +114,31 @@ class Config(object): """ - def __init__(self, fname='setup.cfg'): + def __init__(self, fname="setup.cfg"): fname = abspath(fname) if os.path.exists(fname): self.config = configparser.ConfigParser() self.config.read(fname) def get(self, option_name, default=None): - environ_name = 'MDA_' + option_name.upper() + environ_name = "MDA_" + option_name.upper() if environ_name in os.environ: val = os.environ[environ_name] - if val.upper() in ('1', 'TRUE'): + if val.upper() in ("1", "TRUE"): return True - elif val.upper() in ('0', 'FALSE'): + elif val.upper() in ("0", "FALSE"): return False return val try: - option = self.config.get('options', option_name) + option = self.config.get("options", option_name) return option except configparser.NoOptionError: return default class MDAExtension(Extension, object): - """Derived class to cleanly handle setup-time (numpy) dependencies. - """ + """Derived class to cleanly handle setup-time (numpy) dependencies.""" + # The only setup-time numpy dependency comes when setting up its # include dir. # The actual numpy import and call can be delayed until after pip @@ -151,7 +156,7 @@ def include_dirs(self): if not self._mda_include_dirs: for item in self._mda_include_dir_args: try: - self._mda_include_dirs.append(item()) #The numpy callable + self._mda_include_dirs.append(item()) # The numpy callable except TypeError: item = abspath(item) self._mda_include_dirs.append((item)) @@ -174,9 +179,13 @@ def get_numpy_include(): import numpy as np except ImportError: print('*** package "numpy" not found ***') - print('MDAnalysis requires a version of NumPy (>=1.21.0), even for setup.') - print('Please get it from http://numpy.scipy.org/ or install it through ' - 'your package manager.') + print( + "MDAnalysis requires a version of NumPy (>=1.21.0), even for setup." + ) + print( + "Please get it from http://numpy.scipy.org/ or install it through " + "your package manager." + ) sys.exit(-1) return np.get_include() @@ -184,26 +193,27 @@ def get_numpy_include(): def hasfunction(cc, funcname, include=None, extra_postargs=None): # From http://stackoverflow.com/questions/ # 7018879/disabling-output-when-compiling-with-distutils - tmpdir = tempfile.mkdtemp(prefix='hasfunction-') + tmpdir = tempfile.mkdtemp(prefix="hasfunction-") devnull = oldstderr = None try: try: - fname = os.path.join(tmpdir, 'funcname.c') - with open(fname, 'w') as f: + fname = os.path.join(tmpdir, "funcname.c") + with open(fname, "w") as f: if include is not None: - f.write('#include {0!s}\n'.format(include)) - f.write('int main(void) {\n') - f.write(' {0!s};\n'.format(funcname)) - f.write('}\n') + f.write("#include {0!s}\n".format(include)) + f.write("int main(void) {\n") + f.write(" {0!s};\n".format(funcname)) + f.write("}\n") # Redirect stderr to /dev/null to hide any error messages # from the compiler. # This will have to be changed if we ever have to check # for a function on Windows. - devnull = open('/dev/null', 'w') + devnull = open("/dev/null", "w") oldstderr = os.dup(sys.stderr.fileno()) os.dup2(devnull.fileno(), sys.stderr.fileno()) - objects = cc.compile([fname], output_dir=tmpdir, - extra_postargs=extra_postargs) + objects = cc.compile( + [fname], output_dir=tmpdir, extra_postargs=extra_postargs + ) cc.link_executable(objects, os.path.join(tmpdir, "a.out")) except Exception: return False @@ -221,11 +231,15 @@ def detect_openmp(): print("Attempting to autodetect OpenMP support... ", end="") compiler = new_compiler() customize_compiler(compiler) - compiler.add_library('gomp') - include = '' - extra_postargs = ['-fopenmp'] - hasopenmp = hasfunction(compiler, 'omp_get_num_threads()', include=include, - extra_postargs=extra_postargs) + compiler.add_library("gomp") + include = "" + extra_postargs = ["-fopenmp"] + hasopenmp = hasfunction( + compiler, + "omp_get_num_threads()", + include=include, + extra_postargs=extra_postargs, + ) if hasopenmp: print("Compiler supports OpenMP") else: @@ -238,12 +252,12 @@ def using_clang(): compiler = new_compiler() customize_compiler(compiler) compiler_ver = getoutput("{0} -v".format(compiler.compiler[0])) - if 'Spack GCC' in compiler_ver: + if "Spack GCC" in compiler_ver: # when gcc toolchain is built from source with spack # using clang, the 'clang' string may be present in # the compiler metadata, but it is not clang is_clang = False - elif 'clang' in compiler_ver: + elif "clang" in compiler_ver: # by default, Apple will typically alias gcc to # clang, with some mention of 'clang' in the # metadata @@ -255,196 +269,252 @@ def using_clang(): def extensions(config): # usually (except coming from release tarball) cython files must be generated - use_cython = config.get('use_cython', default=cython_found) - use_openmp = config.get('use_openmp', default=True) - annotate_cython = config.get('annotate_cython', default=False) - - extra_compile_args = ['-std=c11', '-O3', '-funroll-loops', - '-fsigned-zeros'] # see #2722 + use_cython = config.get("use_cython", default=cython_found) + use_openmp = config.get("use_openmp", default=True) + annotate_cython = config.get("annotate_cython", default=False) + + extra_compile_args = [ + "-std=c11", + "-O3", + "-funroll-loops", + "-fsigned-zeros", + ] # see #2722 define_macros = [] - if config.get('debug_cflags', default=False): - extra_compile_args.extend(['-Wall', '-pedantic']) - define_macros.extend([('DEBUG', '1')]) + if config.get("debug_cflags", default=False): + extra_compile_args.extend(["-Wall", "-pedantic"]) + define_macros.extend([("DEBUG", "1")]) # encore is sensitive to floating point accuracy, especially on non-x86 # to avoid reducing optimisations on everything, we make a set of compile # args specific to encore see #2997 for an example of this. - encore_compile_args = [a for a in extra_compile_args if 'O3' not in a] - if platform.machine() == 'aarch64' or platform.machine() == 'ppc64le': - encore_compile_args.append('-O1') + encore_compile_args = [a for a in extra_compile_args if "O3" not in a] + if platform.machine() == "aarch64" or platform.machine() == "ppc64le": + encore_compile_args.append("-O1") else: - encore_compile_args.append('-O3') + encore_compile_args.append("-O3") # allow using custom c/c++ flags and architecture specific instructions. # This allows people to build optimized versions of MDAnalysis. # Do here so not included in encore - extra_cflags = config.get('extra_cflags', default=False) + extra_cflags = config.get("extra_cflags", default=False) if extra_cflags: flags = extra_cflags.split() extra_compile_args.extend(flags) - cpp_extra_compile_args = [a for a in extra_compile_args if 'std' not in a] - cpp_extra_compile_args.append('-std=c++11') - cpp_extra_link_args=[] + cpp_extra_compile_args = [a for a in extra_compile_args if "std" not in a] + cpp_extra_compile_args.append("-std=c++11") + cpp_extra_link_args = [] # needed to specify c++ runtime library on OSX - if platform.system() == 'Darwin' and using_clang(): - cpp_extra_compile_args.append('-stdlib=libc++') - cpp_extra_compile_args.append('-mmacosx-version-min=10.9') - cpp_extra_link_args.append('-stdlib=libc++') - cpp_extra_link_args.append('-mmacosx-version-min=10.9') + if platform.system() == "Darwin" and using_clang(): + cpp_extra_compile_args.append("-stdlib=libc++") + cpp_extra_compile_args.append("-mmacosx-version-min=10.9") + cpp_extra_link_args.append("-stdlib=libc++") + cpp_extra_link_args.append("-mmacosx-version-min=10.9") # Needed for large-file seeking under 32bit systems (for xtc/trr indexing # and access). largefile_macros = [ - ('_LARGEFILE_SOURCE', None), - ('_LARGEFILE64_SOURCE', None), - ('_FILE_OFFSET_BITS', '64') + ("_LARGEFILE_SOURCE", None), + ("_LARGEFILE64_SOURCE", None), + ("_FILE_OFFSET_BITS", "64"), ] has_openmp = detect_openmp() if use_openmp and not has_openmp: - print('No openmp compatible compiler found default to serial build.') + print("No openmp compatible compiler found default to serial build.") - parallel_args = ['-fopenmp'] if has_openmp and use_openmp else [] - parallel_libraries = ['gomp'] if has_openmp and use_openmp else [] - parallel_macros = [('PARALLEL', None)] if has_openmp and use_openmp else [] + parallel_args = ["-fopenmp"] if has_openmp and use_openmp else [] + parallel_libraries = ["gomp"] if has_openmp and use_openmp else [] + parallel_macros = [("PARALLEL", None)] if has_openmp and use_openmp else [] if use_cython: - print('Will attempt to use Cython.') + print("Will attempt to use Cython.") if not cython_found: - print("Couldn't find a Cython installation. " - "Not recompiling cython extensions.") + print( + "Couldn't find a Cython installation. " + "Not recompiling cython extensions." + ) use_cython = False else: - print('Will not attempt to use Cython.') + print("Will not attempt to use Cython.") - source_suffix = '.pyx' if use_cython else '.c' - cpp_source_suffix = '.pyx' if use_cython else '.cpp' + source_suffix = ".pyx" if use_cython else ".c" + cpp_source_suffix = ".pyx" if use_cython else ".cpp" # The callable is passed so that it is only evaluated at install time. include_dirs = [get_numpy_include] # Windows automatically handles math library linking # and will not build MDAnalysis if we try to specify one - if os.name == 'nt': + if os.name == "nt": mathlib = [] else: - mathlib = ['m'] + mathlib = ["m"] if cython_linetrace: extra_compile_args.append("-DCYTHON_TRACE_NOGIL") cpp_extra_compile_args.append("-DCYTHON_TRACE_NOGIL") - libdcd = MDAExtension('MDAnalysis.lib.formats.libdcd', - ['MDAnalysis/lib/formats/libdcd' + source_suffix], - include_dirs=include_dirs + ['MDAnalysis/lib/formats/include'], - define_macros=define_macros, - extra_compile_args=extra_compile_args) - distances = MDAExtension('MDAnalysis.lib.c_distances', - ['MDAnalysis/lib/c_distances' + source_suffix], - include_dirs=include_dirs + ['MDAnalysis/lib/include'], - libraries=mathlib, - define_macros=define_macros, - extra_compile_args=extra_compile_args) - distances_omp = MDAExtension('MDAnalysis.lib.c_distances_openmp', - ['MDAnalysis/lib/c_distances_openmp' + source_suffix], - include_dirs=include_dirs + ['MDAnalysis/lib/include'], - libraries=mathlib + parallel_libraries, - define_macros=define_macros + parallel_macros, - extra_compile_args=parallel_args + extra_compile_args, - extra_link_args=parallel_args) - qcprot = MDAExtension('MDAnalysis.lib.qcprot', - ['MDAnalysis/lib/qcprot' + source_suffix], - include_dirs=include_dirs, - define_macros=define_macros, - extra_compile_args=extra_compile_args) - transformation = MDAExtension('MDAnalysis.lib._transformations', - ['MDAnalysis/lib/src/transformations/transformations.c'], - libraries=mathlib, - define_macros=define_macros, - include_dirs=include_dirs, - extra_compile_args=extra_compile_args) - libmdaxdr = MDAExtension('MDAnalysis.lib.formats.libmdaxdr', - sources=['MDAnalysis/lib/formats/libmdaxdr' + source_suffix, - 'MDAnalysis/lib/formats/src/xdrfile.c', - 'MDAnalysis/lib/formats/src/xdrfile_xtc.c', - 'MDAnalysis/lib/formats/src/xdrfile_trr.c', - 'MDAnalysis/lib/formats/src/trr_seek.c', - 'MDAnalysis/lib/formats/src/xtc_seek.c', - ], - include_dirs=include_dirs + ['MDAnalysis/lib/formats/include', - 'MDAnalysis/lib/formats'], - define_macros=largefile_macros + define_macros, - extra_compile_args=extra_compile_args) - util = MDAExtension('MDAnalysis.lib.formats.cython_util', - sources=['MDAnalysis/lib/formats/cython_util' + source_suffix], - include_dirs=include_dirs, - define_macros=define_macros, - extra_compile_args=extra_compile_args) - cutil = MDAExtension('MDAnalysis.lib._cutil', - sources=['MDAnalysis/lib/_cutil' + cpp_source_suffix], - language='c++', - libraries=mathlib, - include_dirs=include_dirs + ['MDAnalysis/lib/include'], - define_macros=define_macros, - extra_compile_args=cpp_extra_compile_args, - extra_link_args= cpp_extra_link_args) - augment = MDAExtension('MDAnalysis.lib._augment', - sources=['MDAnalysis/lib/_augment' + cpp_source_suffix], - language='c++', - include_dirs=include_dirs, - define_macros=define_macros, - extra_compile_args=cpp_extra_compile_args, - extra_link_args= cpp_extra_link_args) - timestep = MDAExtension('MDAnalysis.coordinates.timestep', - sources=['MDAnalysis/coordinates/timestep' + cpp_source_suffix], - language='c++', - include_dirs=include_dirs, - define_macros=define_macros, - extra_compile_args=cpp_extra_compile_args, - extra_link_args= cpp_extra_link_args) - - - encore_utils = MDAExtension('MDAnalysis.analysis.encore.cutils', - sources=['MDAnalysis/analysis/encore/cutils' + source_suffix], - include_dirs=include_dirs, - define_macros=define_macros, - extra_compile_args=encore_compile_args) - ap_clustering = MDAExtension('MDAnalysis.analysis.encore.clustering.affinityprop', - sources=['MDAnalysis/analysis/encore/clustering/affinityprop' + source_suffix, - 'MDAnalysis/analysis/encore/clustering/src/ap.c'], - include_dirs=include_dirs+['MDAnalysis/analysis/encore/clustering/include'], - libraries=mathlib, - define_macros=define_macros, - extra_compile_args=encore_compile_args) - spe_dimred = MDAExtension('MDAnalysis.analysis.encore.dimensionality_reduction.stochasticproxembed', - sources=['MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed' + source_suffix, - 'MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c'], - include_dirs=include_dirs+['MDAnalysis/analysis/encore/dimensionality_reduction/include'], - libraries=mathlib, - define_macros=define_macros, - extra_compile_args=encore_compile_args) - nsgrid = MDAExtension('MDAnalysis.lib.nsgrid', - ['MDAnalysis/lib/nsgrid' + cpp_source_suffix], - include_dirs=include_dirs + ['MDAnalysis/lib/include'], - language='c++', - define_macros=define_macros, - extra_compile_args=cpp_extra_compile_args, - extra_link_args= cpp_extra_link_args) - pre_exts = [libdcd, distances, distances_omp, qcprot, - transformation, libmdaxdr, util, encore_utils, - ap_clustering, spe_dimred, cutil, augment, nsgrid, timestep] + libdcd = MDAExtension( + "MDAnalysis.lib.formats.libdcd", + ["MDAnalysis/lib/formats/libdcd" + source_suffix], + include_dirs=include_dirs + ["MDAnalysis/lib/formats/include"], + define_macros=define_macros, + extra_compile_args=extra_compile_args, + ) + distances = MDAExtension( + "MDAnalysis.lib.c_distances", + ["MDAnalysis/lib/c_distances" + source_suffix], + include_dirs=include_dirs + ["MDAnalysis/lib/include"], + libraries=mathlib, + define_macros=define_macros, + extra_compile_args=extra_compile_args, + ) + distances_omp = MDAExtension( + "MDAnalysis.lib.c_distances_openmp", + ["MDAnalysis/lib/c_distances_openmp" + source_suffix], + include_dirs=include_dirs + ["MDAnalysis/lib/include"], + libraries=mathlib + parallel_libraries, + define_macros=define_macros + parallel_macros, + extra_compile_args=parallel_args + extra_compile_args, + extra_link_args=parallel_args, + ) + qcprot = MDAExtension( + "MDAnalysis.lib.qcprot", + ["MDAnalysis/lib/qcprot" + source_suffix], + include_dirs=include_dirs, + define_macros=define_macros, + extra_compile_args=extra_compile_args, + ) + transformation = MDAExtension( + "MDAnalysis.lib._transformations", + ["MDAnalysis/lib/src/transformations/transformations.c"], + libraries=mathlib, + define_macros=define_macros, + include_dirs=include_dirs, + extra_compile_args=extra_compile_args, + ) + libmdaxdr = MDAExtension( + "MDAnalysis.lib.formats.libmdaxdr", + sources=[ + "MDAnalysis/lib/formats/libmdaxdr" + source_suffix, + "MDAnalysis/lib/formats/src/xdrfile.c", + "MDAnalysis/lib/formats/src/xdrfile_xtc.c", + "MDAnalysis/lib/formats/src/xdrfile_trr.c", + "MDAnalysis/lib/formats/src/trr_seek.c", + "MDAnalysis/lib/formats/src/xtc_seek.c", + ], + include_dirs=include_dirs + + ["MDAnalysis/lib/formats/include", "MDAnalysis/lib/formats"], + define_macros=largefile_macros + define_macros, + extra_compile_args=extra_compile_args, + ) + util = MDAExtension( + "MDAnalysis.lib.formats.cython_util", + sources=["MDAnalysis/lib/formats/cython_util" + source_suffix], + include_dirs=include_dirs, + define_macros=define_macros, + extra_compile_args=extra_compile_args, + ) + cutil = MDAExtension( + "MDAnalysis.lib._cutil", + sources=["MDAnalysis/lib/_cutil" + cpp_source_suffix], + language="c++", + libraries=mathlib, + include_dirs=include_dirs + ["MDAnalysis/lib/include"], + define_macros=define_macros, + extra_compile_args=cpp_extra_compile_args, + extra_link_args=cpp_extra_link_args, + ) + augment = MDAExtension( + "MDAnalysis.lib._augment", + sources=["MDAnalysis/lib/_augment" + cpp_source_suffix], + language="c++", + include_dirs=include_dirs, + define_macros=define_macros, + extra_compile_args=cpp_extra_compile_args, + extra_link_args=cpp_extra_link_args, + ) + timestep = MDAExtension( + "MDAnalysis.coordinates.timestep", + sources=["MDAnalysis/coordinates/timestep" + cpp_source_suffix], + language="c++", + include_dirs=include_dirs, + define_macros=define_macros, + extra_compile_args=cpp_extra_compile_args, + extra_link_args=cpp_extra_link_args, + ) + encore_utils = MDAExtension( + "MDAnalysis.analysis.encore.cutils", + sources=["MDAnalysis/analysis/encore/cutils" + source_suffix], + include_dirs=include_dirs, + define_macros=define_macros, + extra_compile_args=encore_compile_args, + ) + ap_clustering = MDAExtension( + "MDAnalysis.analysis.encore.clustering.affinityprop", + sources=[ + "MDAnalysis/analysis/encore/clustering/affinityprop" + + source_suffix, + "MDAnalysis/analysis/encore/clustering/src/ap.c", + ], + include_dirs=include_dirs + + ["MDAnalysis/analysis/encore/clustering/include"], + libraries=mathlib, + define_macros=define_macros, + extra_compile_args=encore_compile_args, + ) + spe_dimred = MDAExtension( + "MDAnalysis.analysis.encore.dimensionality_reduction.stochasticproxembed", + sources=[ + "MDAnalysis/analysis/encore/dimensionality_reduction/stochasticproxembed" + + source_suffix, + "MDAnalysis/analysis/encore/dimensionality_reduction/src/spe.c", + ], + include_dirs=include_dirs + + ["MDAnalysis/analysis/encore/dimensionality_reduction/include"], + libraries=mathlib, + define_macros=define_macros, + extra_compile_args=encore_compile_args, + ) + nsgrid = MDAExtension( + "MDAnalysis.lib.nsgrid", + ["MDAnalysis/lib/nsgrid" + cpp_source_suffix], + include_dirs=include_dirs + ["MDAnalysis/lib/include"], + language="c++", + define_macros=define_macros, + extra_compile_args=cpp_extra_compile_args, + extra_link_args=cpp_extra_link_args, + ) + pre_exts = [ + libdcd, + distances, + distances_omp, + qcprot, + transformation, + libmdaxdr, + util, + encore_utils, + ap_clustering, + spe_dimred, + cutil, + augment, + nsgrid, + timestep, + ] cython_generated = [] if use_cython: extensions = cythonize( pre_exts, annotate=annotate_cython, - compiler_directives={'linetrace': cython_linetrace, - 'embedsignature': False, - 'language_level': '3'}, + compiler_directives={ + "linetrace": cython_linetrace, + "embedsignature": False, + "language_level": "3", + }, ) if cython_linetrace: print("Cython coverage will be enabled") @@ -453,15 +523,16 @@ def extensions(config): if source not in pre_ext.sources: cython_generated.append(source) else: - #Let's check early for missing .c files + # Let's check early for missing .c files extensions = pre_exts for ext in extensions: for source in ext.sources: - if not (os.path.isfile(source) and - os.access(source, os.R_OK)): - raise IOError("Source file '{}' not found. This might be " - "caused by a missing Cython install, or a " - "failed/disabled Cython build.".format(source)) + if not (os.path.isfile(source) and os.access(source, os.R_OK)): + raise IOError( + "Source file '{}' not found. This might be " + "caused by a missing Cython install, or a " + "failed/disabled Cython build.".format(source) + ) return extensions, cython_generated @@ -477,7 +548,7 @@ def dynamic_author_list(): "Chronological list of authors" title. """ authors = [] - with codecs.open(abspath('AUTHORS'), encoding='utf-8') as infile: + with codecs.open(abspath("AUTHORS"), encoding="utf-8") as infile: # An author is a bullet point under the title "Chronological list of # authors". We first want move the cursor down to the title of # interest. @@ -486,21 +557,23 @@ def dynamic_author_list(): break else: # If we did not break, it means we did not find the authors. - raise IOError('EOF before the list of authors') + raise IOError("EOF before the list of authors") # Skip the next line as it is the title underlining line = next(infile) line_no += 1 - if line[:4] != '----': - raise IOError('Unexpected content on line {0}, ' - 'should be a string of "-".'.format(line_no)) + if line[:4] != "----": + raise IOError( + "Unexpected content on line {0}, " + 'should be a string of "-".'.format(line_no) + ) # Add each bullet point as an author until the next title underlining for line in infile: - if line[:4] in ('----', '====', '~~~~'): + if line[:4] in ("----", "====", "~~~~"): # The previous line was a title, hopefully it did not start as # a bullet point so it got ignored. Since we hit a title, we # are done reading the list of authors. break - elif line.strip()[:2] == '- ': + elif line.strip()[:2] == "- ": # This is a bullet point, so it should be an author name. name = line.strip()[2:].strip() authors.append(name) @@ -509,28 +582,32 @@ def dynamic_author_list(): # sorted alphabetically of the last name. authors.sort(key=lambda name: name.split()[-1]) # Move Naveen and Elizabeth first, and Oliver last. - authors.remove('Naveen Michaud-Agrawal') - authors.remove('Elizabeth J. Denning') - authors.remove('Oliver Beckstein') - authors = (['Naveen Michaud-Agrawal', 'Elizabeth J. Denning'] - + authors + ['Oliver Beckstein']) + authors.remove("Naveen Michaud-Agrawal") + authors.remove("Elizabeth J. Denning") + authors.remove("Oliver Beckstein") + authors = ( + ["Naveen Michaud-Agrawal", "Elizabeth J. Denning"] + + authors + + ["Oliver Beckstein"] + ) # Write the authors.py file. - out_path = abspath('MDAnalysis/authors.py') - with codecs.open(out_path, 'w', encoding='utf-8') as outfile: + out_path = abspath("MDAnalysis/authors.py") + with codecs.open(out_path, "w", encoding="utf-8") as outfile: # Write the header - header = '''\ + header = """\ #-*- coding:utf-8 -*- # This file is generated from the AUTHORS file during the installation process. # Do not edit it as your changes will be overwritten. -''' +""" print(header, file=outfile) # Write the list of authors as a python list - template = u'__authors__ = [\n{}\n]' - author_string = u',\n'.join(u' u"{}"'.format(name) - for name in authors) + template = "__authors__ = [\n{}\n]" + author_string = ",\n".join( + ' u"{}"'.format(name) for name in authors + ) print(template.format(author_string), file=outfile) @@ -540,17 +617,18 @@ def long_description(readme): with open(abspath(readme)) as summary: buffer = summary.read() # remove top heading that messes up pypi display - m = re.search('====*\n[^\n]*README[^\n]*\n=====*\n', buffer, - flags=re.DOTALL) + m = re.search( + "====*\n[^\n]*README[^\n]*\n=====*\n", buffer, flags=re.DOTALL + ) assert m, "README.rst does not contain a level-1 heading" - return buffer[m.end():] + return buffer[m.end() :] -if __name__ == '__main__': +if __name__ == "__main__": try: dynamic_author_list() except (OSError, IOError): - warnings.warn('Cannot write the list of authors.') + warnings.warn("Cannot write the list of authors.") try: # when building from repository for creating the distribution @@ -563,24 +641,30 @@ def long_description(readme): config = Config() exts, cythonfiles = extensions(config) - setup(name='MDAnalysis', - version=RELEASE, - long_description=LONG_DESCRIPTION, - long_description_content_type='text/x-rst', - # currently unused & may become obsolte see setuptools #1569 - provides=['MDAnalysis'], - ext_modules=exts, - test_suite="MDAnalysisTests", - tests_require=[ - 'MDAnalysisTests=={0!s}'.format(RELEASE), # same as this release! - ], + setup( + name="MDAnalysis", + version=RELEASE, + long_description=LONG_DESCRIPTION, + long_description_content_type="text/x-rst", + # currently unused & may become obsolte see setuptools #1569 + provides=["MDAnalysis"], + ext_modules=exts, + test_suite="MDAnalysisTests", + tests_require=[ + "MDAnalysisTests=={0!s}".format(RELEASE), # same as this release! + ], ) # Releases keep their cythonized stuff for shipping. - if not config.get('keep_cythonized', default=is_release) and not cython_linetrace: + if ( + not config.get("keep_cythonized", default=is_release) + and not cython_linetrace + ): for cythonized in cythonfiles: try: os.unlink(cythonized) except OSError as err: - print("Warning: failed to delete cythonized file {0}: {1}. " - "Moving on.".format(cythonized, err.strerror)) + print( + "Warning: failed to delete cythonized file {0}: {1}. " + "Moving on.".format(cythonized, err.strerror) + ) From 7e521debfc1e2a0f66adb6da1b8e189c6b9970e2 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Tue, 26 Nov 2024 22:54:47 +0000 Subject: [PATCH 42/57] Start 2.9.0-dev0 (#4803) --- package/CHANGELOG | 13 +++++++++++++ package/MDAnalysis/version.py | 2 +- package/setup.py | 2 +- testsuite/MDAnalysisTests/__init__.py | 2 +- testsuite/setup.py | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 9a6026971df..5bce69eaec0 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,6 +14,19 @@ The rules for this file: ------------------------------------------------------------------------------- +??/??/?? IAlibay + + * 2.9.0 + +Fixes + +Enhancements + +Changes + +Deprecations + + 11/11/24 IAlibay, HeetVekariya, marinegor, lilyminium, RMeli, ljwoods2, aditya292002, pstaerk, PicoCentauri, BFedder, tyler.je.reddy, SampurnaM, leonwehrhan, kainszs, orionarcher, diff --git a/package/MDAnalysis/version.py b/package/MDAnalysis/version.py index 66476ef9b3e..e8384ed3470 100644 --- a/package/MDAnalysis/version.py +++ b/package/MDAnalysis/version.py @@ -67,4 +67,4 @@ # e.g. with lib.log #: Release of MDAnalysis as a string, using `semantic versioning`_. -__version__ = "2.8.0" # NOTE: keep in sync with RELEASE in setup.py +__version__ = "2.9.0-dev0" # NOTE: keep in sync with RELEASE in setup.py diff --git a/package/setup.py b/package/setup.py index b19a4c0fbde..1158330e7a7 100755 --- a/package/setup.py +++ b/package/setup.py @@ -58,7 +58,7 @@ from subprocess import getoutput # NOTE: keep in sync with MDAnalysis.__version__ in version.py -RELEASE = "2.8.0" +RELEASE = "2.9.0-dev0" is_release = "dev" not in RELEASE diff --git a/testsuite/MDAnalysisTests/__init__.py b/testsuite/MDAnalysisTests/__init__.py index 5752ec98588..ea7b2d55706 100644 --- a/testsuite/MDAnalysisTests/__init__.py +++ b/testsuite/MDAnalysisTests/__init__.py @@ -97,7 +97,7 @@ logger = logging.getLogger("MDAnalysisTests.__init__") # keep in sync with RELEASE in setup.py -__version__ = "2.8.0" +__version__ = "2.9.0-dev0" # Do NOT import MDAnalysis at this level. Tests should do it themselves. diff --git a/testsuite/setup.py b/testsuite/setup.py index 5f786f94d90..228bfd1fd0a 100755 --- a/testsuite/setup.py +++ b/testsuite/setup.py @@ -74,7 +74,7 @@ def run(self): if __name__ == '__main__': # this must be in-sync with MDAnalysis - RELEASE = "2.8.0" + RELEASE = "2.9.0-dev0" setup( version=RELEASE, From abc9806392f4935fb2b85b05b6479036c6e51b9f Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:52:28 +0100 Subject: [PATCH 43/57] Addition of pytest case coverage of `backend` and `AnalysisBase.run()` using different `n_workers` values (#4768) --- testsuite/MDAnalysisTests/analysis/test_base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 90887b2ad0b..e2fe428376e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -121,6 +121,17 @@ def test_incompatible_n_workers(u): with pytest.raises(ValueError): FrameAnalysis(u).run(backend=backend, n_workers=3) + +def test_n_workers_conflict_raises_value_error(u): + backend_instance = ManyWorkersBackend(n_workers=4) + + with pytest.raises(ValueError, match="n_workers specified twice"): + FrameAnalysis(u.trajectory).run( + backend=backend_instance, + n_workers=1, + unsupported_backend=True + ) + @pytest.mark.parametrize('run_class,backend,n_workers', [ (Parallelizable, 'not-existing-backend', 2), (Parallelizable, 'not-existing-backend', None), From 441e2c67abdb8a0a5ffac4a0bc5e88bc7c329aa8 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Wed, 27 Nov 2024 22:57:15 +0100 Subject: [PATCH 44/57] fmt auxiliary (#4802) --- package/MDAnalysis/auxiliary/EDR.py | 62 ++-- package/MDAnalysis/auxiliary/XVG.py | 69 +++-- package/MDAnalysis/auxiliary/base.py | 289 +++++++++++------- package/MDAnalysis/auxiliary/core.py | 7 +- package/pyproject.toml | 1 + testsuite/MDAnalysisTests/auxiliary/base.py | 247 +++++++++------ .../MDAnalysisTests/auxiliary/test_core.py | 18 +- .../MDAnalysisTests/auxiliary/test_edr.py | 254 ++++++++------- .../MDAnalysisTests/auxiliary/test_xvg.py | 65 ++-- testsuite/pyproject.toml | 8 +- testsuite/setup.py | 19 +- 11 files changed, 630 insertions(+), 409 deletions(-) diff --git a/package/MDAnalysis/auxiliary/EDR.py b/package/MDAnalysis/auxiliary/EDR.py index 37f4394c24d..8b9149690eb 100644 --- a/package/MDAnalysis/auxiliary/EDR.py +++ b/package/MDAnalysis/auxiliary/EDR.py @@ -231,11 +231,15 @@ class EDRStep(base.AuxStep): :class:`MDAnalysis.auxiliary.base.AuxStep` """ - def __init__(self, time_selector: str = "Time", - data_selector: Optional[str] = None, **kwargs): - super(EDRStep, self).__init__(time_selector=time_selector, - data_selector=data_selector, - **kwargs) + def __init__( + self, + time_selector: str = "Time", + data_selector: Optional[str] = None, + **kwargs, + ): + super(EDRStep, self).__init__( + time_selector=time_selector, data_selector=data_selector, **kwargs + ) def _select_time(self, key: str) -> np.float64: """'Time' is one of the entries in the dict returned by pyedr. @@ -249,12 +253,14 @@ def _select_data(self, key: Union[str, None]) -> np.float64: try: return self._data[key] except KeyError: - raise KeyError(f"'{key}' is not a key in the data_dict dictionary." - " Check the EDRReader.terms attribute") + raise KeyError( + f"'{key}' is not a key in the data_dict dictionary." + " Check the EDRReader.terms attribute" + ) class EDRReader(base.AuxReader): - """ Auxiliary reader to read data from an .edr file. + """Auxiliary reader to read data from an .edr file. `EDR files`_ are created by GROMACS during a simulation. They are binary files which @@ -310,8 +316,9 @@ class EDRReader(base.AuxReader): def __init__(self, filename: str, convert_units: bool = True, **kwargs): if not HAS_PYEDR: - raise ImportError("EDRReader: To read EDR files please install " - "pyedr.") + raise ImportError( + "EDRReader: To read EDR files please install " "pyedr." + ) self._auxdata = Path(filename).resolve() self.data_dict = pyedr.edr_to_dict(filename) self.unit_dict = pyedr.get_unit_dictionary(filename) @@ -340,8 +347,10 @@ def _convert_units(self): self.data_dict[term] = units.convert(data, unit, target_unit) self.unit_dict[term] = units.MDANALYSIS_BASE_UNITS[unit_type] if unknown_units: - warnings.warn("Could not find unit type for the following " - f"units: {unknown_units}") + warnings.warn( + "Could not find unit type for the following " + f"units: {unknown_units}" + ) def _memory_usage(self): size = 0 @@ -365,8 +374,10 @@ def _read_next_step(self) -> EDRStep: auxstep = self.auxstep new_step = self.step + 1 if new_step < self.n_steps: - auxstep._data = {term: self.data_dict[term][self.step + 1] - for term in self.terms} + auxstep._data = { + term: self.data_dict[term][self.step + 1] + for term in self.terms + } auxstep.step = new_step return auxstep else: @@ -375,7 +386,7 @@ def _read_next_step(self) -> EDRStep: raise StopIteration def _go_to_step(self, i: int) -> EDRStep: - """ Move to and read i-th auxiliary step. + """Move to and read i-th auxiliary step. Parameters ---------- @@ -392,14 +403,16 @@ def _go_to_step(self, i: int) -> EDRStep: If step index not in valid range. """ if i >= self.n_steps or i < 0: - raise ValueError("Step index {0} is not valid for auxiliary " - "(num. steps {1})".format(i, self.n_steps)) + raise ValueError( + "Step index {0} is not valid for auxiliary " + "(num. steps {1})".format(i, self.n_steps) + ) self.auxstep.step = i - 1 self.next() return self.auxstep def read_all_times(self) -> np.ndarray: - """ Get list of time at each step. + """Get list of time at each step. Returns ------- @@ -408,9 +421,10 @@ def read_all_times(self) -> np.ndarray: """ return self.data_dict[self.time_selector] - def get_data(self, data_selector: Union[str, List[str], None] = None - ) -> Dict[str, np.ndarray]: - """ Returns the auxiliary data contained in the :class:`EDRReader`. + def get_data( + self, data_selector: Union[str, List[str], None] = None + ) -> Dict[str, np.ndarray]: + """Returns the auxiliary data contained in the :class:`EDRReader`. Returns either all data or data specified as `data_selector` in form of a str or a list of any of :attr:`EDRReader.terms`. `Time` is always returned to allow easy plotting. @@ -438,8 +452,10 @@ def _get_data_term(term, datadict): try: return datadict[term] except KeyError: - raise KeyError(f"data selector {term} is invalid. Check the " - "EDRReader's `terms` attribute.") + raise KeyError( + f"data selector {term} is invalid. Check the " + "EDRReader's `terms` attribute." + ) data_dict = {"Time": self.data_dict["Time"]} diff --git a/package/MDAnalysis/auxiliary/XVG.py b/package/MDAnalysis/auxiliary/XVG.py index c690b414059..f68d2de1c76 100644 --- a/package/MDAnalysis/auxiliary/XVG.py +++ b/package/MDAnalysis/auxiliary/XVG.py @@ -74,7 +74,7 @@ def uncomment(lines): - """ Remove comments from lines in an .xvg file + """Remove comments from lines in an .xvg file Parameters ---------- @@ -92,10 +92,10 @@ def uncomment(lines): if not stripped_line: continue # '@' must be at the beginning of a line to be a grace instruction - if stripped_line[0] == '@': + if stripped_line[0] == "@": continue # '#' can be anywhere in the line, everything after is a comment - comment_position = stripped_line.find('#') + comment_position = stripped_line.find("#") if comment_position > 0 and stripped_line[:comment_position]: yield stripped_line[:comment_position] elif comment_position < 0 and stripped_line: @@ -104,7 +104,7 @@ def uncomment(lines): class XVGStep(base.AuxStep): - """ AuxStep class for .xvg file format. + """AuxStep class for .xvg file format. Extends the base AuxStep class to allow selection of time and data-of-interest fields (by column index) from the full set of data read @@ -127,9 +127,9 @@ class XVGStep(base.AuxStep): """ def __init__(self, time_selector=0, data_selector=None, **kwargs): - super(XVGStep, self).__init__(time_selector=time_selector, - data_selector=data_selector, - **kwargs) + super(XVGStep, self).__init__( + time_selector=time_selector, data_selector=data_selector, **kwargs + ) def _select_time(self, key): if key is None: @@ -138,7 +138,7 @@ def _select_time(self, key): if isinstance(key, numbers.Integral): return self._select_data(key) else: - raise ValueError('Time selector must be single index') + raise ValueError("Time selector must be single index") def _select_data(self, key): if key is None: @@ -148,15 +148,17 @@ def _select_data(self, key): try: return self._data[key] except IndexError: - errmsg = (f'{key} not a valid index for data with ' - f'{len(self._data)} columns') + errmsg = ( + f"{key} not a valid index for data with " + f"{len(self._data)} columns" + ) raise ValueError(errmsg) from None else: return np.array([self._select_data(i) for i in key]) class XVGReader(base.AuxReader): - """ Auxiliary reader to read data from an .xvg file. + """Auxiliary reader to read data from an .xvg file. Default reader for .xvg files. All data from the file will be read and stored on initialisation. @@ -188,24 +190,25 @@ def __init__(self, filename, **kwargs): auxdata_values = [] # remove comments before storing for i, line in enumerate(uncomment(lines)): - if line.lstrip()[0] == '&': + if line.lstrip()[0] == "&": # multiple data sets not supported; stop at end of first set break auxdata_values.append([float(val) for val in line.split()]) # check the number of columns is consistent if len(auxdata_values[i]) != len(auxdata_values[0]): - raise ValueError('Step {0} has {1} columns instead of ' - '{2}'.format(i, auxdata_values[i], - auxdata_values[0])) + raise ValueError( + "Step {0} has {1} columns instead of " + "{2}".format(i, auxdata_values[i], auxdata_values[0]) + ) self._auxdata_values = np.array(auxdata_values) self._n_steps = len(self._auxdata_values) super(XVGReader, self).__init__(**kwargs) def _memory_usage(self): - return(self._auxdata_values.nbytes) + return self._auxdata_values.nbytes def _read_next_step(self): - """ Read next auxiliary step and update ``auxstep``. + """Read next auxiliary step and update ``auxstep``. Returns ------- @@ -228,7 +231,7 @@ def _read_next_step(self): raise StopIteration def _go_to_step(self, i): - """ Move to and read i-th auxiliary step. + """Move to and read i-th auxiliary step. Parameters ---------- @@ -245,14 +248,16 @@ def _go_to_step(self, i): If step index not in valid range. """ if i >= self.n_steps or i < 0: - raise ValueError("Step index {0} is not valid for auxiliary " - "(num. steps {1})".format(i, self.n_steps)) - self.auxstep.step = i-1 + raise ValueError( + "Step index {0} is not valid for auxiliary " + "(num. steps {1})".format(i, self.n_steps) + ) + self.auxstep.step = i - 1 self.next() return self.auxstep def read_all_times(self): - """ Get NumPy array of time at each step. + """Get NumPy array of time at each step. Returns ------- @@ -263,7 +268,7 @@ def read_all_times(self): class XVGFileReader(base.AuxFileReader): - """ Auxiliary reader to read (one step at a time) from an .xvg file. + """Auxiliary reader to read (one step at a time) from an .xvg file. An alternative XVG reader which reads each step from the .xvg file as needed (rather than reading and storing all from the start), for a lower @@ -286,14 +291,14 @@ class XVGFileReader(base.AuxFileReader): The default reader for .xvg files is :class:`XVGReader`. """ - format = 'XVG-F' + format = "XVG-F" _Auxstep = XVGStep def __init__(self, filename, **kwargs): super(XVGFileReader, self).__init__(filename, **kwargs) def _read_next_step(self): - """ Read next recorded step in xvg file and update ``auxstep``. + """Read next recorded step in xvg file and update ``auxstep``. Returns ------- @@ -307,7 +312,7 @@ def _read_next_step(self): """ line = next(self.auxfile) while True: - if not line or (line.strip() and line.strip()[0] == '&'): + if not line or (line.strip() and line.strip()[0] == "&"): # at end of file or end of first set of data (multiple sets # currently not supported) self.rewind() @@ -325,15 +330,17 @@ def _read_next_step(self): # haven't set n_cols yet; set now auxstep._n_cols = len(auxstep._data) if len(auxstep._data) != auxstep._n_cols: - raise ValueError(f'Step {self.step} has ' - f'{len(auxstep._data)} columns instead ' - f'of {auxstep._n_cols}') + raise ValueError( + f"Step {self.step} has " + f"{len(auxstep._data)} columns instead " + f"of {auxstep._n_cols}" + ) return auxstep # line is comment only - move to next line = next(self.auxfile) def _count_n_steps(self): - """ Iterate through all steps to count total number. + """Iterate through all steps to count total number. Returns ------- @@ -358,7 +365,7 @@ def _count_n_steps(self): return count def read_all_times(self): - """ Iterate through all steps to build times list. + """Iterate through all steps to build times list. Returns ------- diff --git a/package/MDAnalysis/auxiliary/base.py b/package/MDAnalysis/auxiliary/base.py index 58f9219c002..29fbaf999a2 100644 --- a/package/MDAnalysis/auxiliary/base.py +++ b/package/MDAnalysis/auxiliary/base.py @@ -61,7 +61,7 @@ class _AuxReaderMeta(type): def __init__(cls, name, bases, classdict): type.__init__(type, name, bases, classdict) try: - fmt = asiterable(classdict['format']) + fmt = asiterable(classdict["format"]) except KeyError: pass else: @@ -114,8 +114,15 @@ class AuxStep(object): Number of the current auxiliary step (0-based). """ - def __init__(self, dt=1, initial_time=0, time_selector=None, - data_selector=None, constant_dt=True, memory_limit=None): + def __init__( + self, + dt=1, + initial_time=0, + time_selector=None, + data_selector=None, + constant_dt=True, + memory_limit=None, + ): self.step = -1 self._initial_time = initial_time self._dt = dt @@ -128,7 +135,7 @@ def __init__(self, dt=1, initial_time=0, time_selector=None, @property def time(self): - """ Time in ps of current auxiliary step (as float). + """Time in ps of current auxiliary step (as float). Read from the set of auxiliary data read each step if time selection is enabled and a valid ``time_selector`` is specified; otherwise @@ -140,13 +147,13 @@ def time(self): # default to calculting time... return self.step * self._dt + self._initial_time else: - raise ValueError("If dt is not constant, must have a valid " - "time selector") - + raise ValueError( + "If dt is not constant, must have a valid " "time selector" + ) @property def data(self): - """ Auxiliary values of interest for the current step (as ndarray). + """Auxiliary values of interest for the current step (as ndarray). Read from the full set of data read for each step if data selection is enabled and a valid ``data_selector`` is specified; otherwise @@ -159,7 +166,7 @@ def data(self): @property def _time_selector(self): - """ 'Key' to select time from the full set of data read in each step. + """'Key' to select time from the full set of data read in each step. Will be passed to ``_select_time()``, defined separately for each auxiliary format, when returning the time of the current step. @@ -172,8 +179,10 @@ def _time_selector(self): try: self._select_time except AttributeError: - warnings.warn("{} does not support time selection. Reverting to " - "default".format(self.__class__.__name__)) + warnings.warn( + "{} does not support time selection. Reverting to " + "default".format(self.__class__.__name__) + ) return None return self._time_selector_ @@ -183,8 +192,11 @@ def _time_selector(self, new): try: select = self._select_time except AttributeError: - warnings.warn("{} does not support time selection".format( - self.__class__.__name__)) + warnings.warn( + "{} does not support time selection".format( + self.__class__.__name__ + ) + ) else: # check *new* is valid before setting; _select_time should raise # an error if not @@ -193,7 +205,7 @@ def _time_selector(self, new): @property def _data_selector(self): - """ 'Key' to select values of interest from full set of auxiliary data. + """'Key' to select values of interest from full set of auxiliary data. These are the values that will be stored in ``data`` (and ``frame_data`` and ``frame_rep``). @@ -208,8 +220,10 @@ def _data_selector(self): try: self._select_data except AttributeError: - warnings.warn("{} does not support data selection. Reverting to " - "default".format(self.__class__.__name__)) + warnings.warn( + "{} does not support data selection. Reverting to " + "default".format(self.__class__.__name__) + ) return None return self._data_selector_ @@ -220,7 +234,9 @@ def _data_selector(self, new): select = self._select_data except AttributeError: warnings.warn( - "{} does not support data selection".format(self.__class__.__name__) + "{} does not support data selection".format( + self.__class__.__name__ + ) ) else: # check *new* is valid before setting; _select_data should raise an @@ -229,7 +245,7 @@ def _data_selector(self, new): self._data_selector_ = new def _empty_data(self): - """ Create an 'empty' ``data``-like placeholder. + """Create an 'empty' ``data``-like placeholder. Returns an ndarray in the format of ``data`` with all values set to np.nan; to use at the 'representative value' when no auxiliary steps @@ -297,15 +313,25 @@ class AuxReader(metaclass=_AuxReaderMeta): _Auxstep = AuxStep # update when add new options - represent_options = ['closest', 'average'] + represent_options = ["closest", "average"] # list of attributes required to recreate the auxiliary - required_attrs = ['represent_ts_as', 'cutoff', 'dt', 'initial_time', - 'time_selector', 'data_selector', 'constant_dt', 'auxname', - 'format', '_auxdata'] - - def __init__(self, represent_ts_as='closest', auxname=None, cutoff=None, - **kwargs): + required_attrs = [ + "represent_ts_as", + "cutoff", + "dt", + "initial_time", + "time_selector", + "data_selector", + "constant_dt", + "auxname", + "format", + "_auxdata", + ] + + def __init__( + self, represent_ts_as="closest", auxname=None, cutoff=None, **kwargs + ): # allow auxname to be optional for when using reader separate from # trajectory. self.auxname = auxname @@ -334,45 +360,46 @@ def copy(self): return new_reader def __len__(self): - """ Number of steps in auxiliary data. """ + """Number of steps in auxiliary data.""" return self.n_steps def next(self): - """ Move to next step of auxiliary data. """ + """Move to next step of auxiliary data.""" return self._read_next_step() def __next__(self): - """ Move to next step of auxiliary data. """ + """Move to next step of auxiliary data.""" return self.next() def __iter__(self): - """ Iterate over all auxiliary steps. """ + """Iterate over all auxiliary steps.""" self._restart() return self def _restart(self): - """ Reset back to start; calling next() should read first step. """ + """Reset back to start; calling next() should read first step.""" # Overwrite as appropriate self.auxstep.step = -1 def rewind(self): - """ Return to and read first step. """ + """Return to and read first step.""" # Overwrite as appropriate # could also use _go_to_step(0) self._restart() return self._read_next_step() def _read_next_step(self): - """ Move to next step and update auxstep. + """Move to next step and update auxstep. Should return the AuxStep instance corresponding to the next step. """ # Define in each auxiliary reader raise NotImplementedError( - "BUG: Override _read_next_step() in auxiliary reader!") + "BUG: Override _read_next_step() in auxiliary reader!" + ) def update_ts(self, ts): - """ Read auxiliary steps corresponding to and update the trajectory + """Read auxiliary steps corresponding to and update the trajectory timestep *ts*. Calls :meth:`read_ts`, then updates *ts* with the representative value. @@ -401,15 +428,17 @@ def update_ts(self, ts): :meth:`read_ts` """ if not self.auxname: - raise ValueError("Auxiliary name not set, cannot set representative " - "value in timestep. Name auxiliary or use read_ts " - "instead") + raise ValueError( + "Auxiliary name not set, cannot set representative " + "value in timestep. Name auxiliary or use read_ts " + "instead" + ) self.read_ts(ts) setattr(ts.aux, self.auxname, self.frame_rep) return ts def read_ts(self, ts): - """ Read auxiliary steps corresponding to the trajectory timestep *ts*. + """Read auxiliary steps corresponding to the trajectory timestep *ts*. Read the auxiliary steps 'assigned' to *ts* (the steps that are within ``ts.dt/2`` of of the trajectory timestep/frame - ie. closer to *ts* @@ -441,7 +470,7 @@ def read_ts(self, ts): # previous frame, and the next step to either the frame being read or a # following frame. Move to right position if not. frame_for_step = self.step_to_frame(self.step, ts) - frame_for_next_step = self.step_to_frame(self.step+1, ts) + frame_for_next_step = self.step_to_frame(self.step + 1, ts) if frame_for_step is not None: if frame_for_next_step is None: # self.step is the last auxiliary step in memory. @@ -450,18 +479,20 @@ def read_ts(self, ts): elif not (frame_for_step < ts.frame <= frame_for_next_step): self.move_to_ts(ts) - self._reset_frame_data() # clear previous frame data + self._reset_frame_data() # clear previous frame data # read through all the steps 'assigned' to ts.frame + add to frame_data - while self.step_to_frame(self.step+1, ts) == ts.frame: + while self.step_to_frame(self.step + 1, ts) == ts.frame: self._read_next_step() self._add_step_to_frame_data(ts.time) self.frame_rep = self.calc_representative() - def attach_auxiliary(self, - coord_parent, - aux_spec: Optional[Union[str, Dict[str, str]]] = None, - format: Optional[str] = None, - **kwargs) -> None: + def attach_auxiliary( + self, + coord_parent, + aux_spec: Optional[Union[str, Dict[str, str]]] = None, + format: Optional[str] = None, + **kwargs, + ) -> None: """Attaches the data specified in `aux_spec` to the `coord_parent` This method is called from within @@ -516,12 +547,15 @@ def attach_auxiliary(self, for auxname in aux_spec: if auxname in coord_parent.aux_list: - raise ValueError(f"Auxiliary data with name {auxname} already " - "exists") + raise ValueError( + f"Auxiliary data with name {auxname} already " "exists" + ) if " " in auxname: - warnings.warn(f"Auxiliary name '{auxname}' contains a space. " - "Only dictionary style accession, not attribute " - f"style accession of '{auxname}' will work.") + warnings.warn( + f"Auxiliary name '{auxname}' contains a space. " + "Only dictionary style accession, not attribute " + f"style accession of '{auxname}' will work." + ) description = self.get_description() # make sure kwargs are also used description_kwargs = {**description, **kwargs} @@ -538,22 +572,25 @@ def attach_auxiliary(self, aux_memory_usage = 0 # Check if testing, which needs lower memory limit - memory_limit = kwargs.get("memory_limit", 1e+09) + memory_limit = kwargs.get("memory_limit", 1e09) for reader in coord_parent._auxs.values(): aux_memory_usage += reader._memory_usage() if aux_memory_usage > memory_limit: - conv = 1e+09 # convert to GB - warnings.warn("AuxReader: memory usage warning! " - f"Auxiliary data takes up {aux_memory_usage/conv} " - f"GB of memory (Warning limit: {memory_limit/conv} " - "GB).") + conv = 1e09 # convert to GB + warnings.warn( + "AuxReader: memory usage warning! " + f"Auxiliary data takes up {aux_memory_usage/conv} " + f"GB of memory (Warning limit: {memory_limit/conv} " + "GB)." + ) def _memory_usage(self): - raise NotImplementedError("BUG: Override _memory_usage() " - "in auxiliary reader!") + raise NotImplementedError( + "BUG: Override _memory_usage() " "in auxiliary reader!" + ) def step_to_frame(self, step, ts, return_time_diff=False): - """ Calculate closest trajectory frame for auxiliary step *step*. + """Calculate closest trajectory frame for auxiliary step *step*. Calculated given dt, time and frame from *ts*:: @@ -592,18 +629,20 @@ def step_to_frame(self, step, ts, return_time_diff=False): """ if step >= self.n_steps or step < 0: return None - time_frame_0 = ts.time - ts.frame*ts.dt # assumes ts.dt is constant + time_frame_0 = ts.time - ts.frame * ts.dt # assumes ts.dt is constant time_step = self.step_to_time(step) - frame_index = int(math.floor((time_step-time_frame_0+ts.dt/2.)/ts.dt)) + frame_index = int( + math.floor((time_step - time_frame_0 + ts.dt / 2.0) / ts.dt) + ) if not return_time_diff: return frame_index else: - time_frame = time_frame_0 + frame_index*ts.dt + time_frame = time_frame_0 + frame_index * ts.dt time_diff = abs(time_frame - time_step) return frame_index, time_diff def move_to_ts(self, ts): - """ Position auxiliary reader just before trajectory timestep *ts*. + """Position auxiliary reader just before trajectory timestep *ts*. Calling ``next()`` should read the first auxiliary step 'assigned' to the trajectory timestep *ts* or, if no auxiliary steps are @@ -619,23 +658,25 @@ def move_to_ts(self, ts): # figure out what step we want to end up at if self.constant_dt: # if dt constant, calculate from dt/offset/etc - step = int(math.floor((ts.time-ts.dt/2-self.initial_time)/self.dt)) + step = int( + math.floor((ts.time - ts.dt / 2 - self.initial_time) / self.dt) + ) # if we're out of range of the number of steps, reset back - step = max(min(step, self.n_steps-1), -1) + step = max(min(step, self.n_steps - 1), -1) else: # otherwise, go through steps till we find the right one - for i in range(self.n_steps+1): + for i in range(self.n_steps + 1): if self.step_to_frame(i) >= ts.frame: break # we want the step before this - step = i-1 + step = i - 1 if step == -1: self._restart() else: self._go_to_step(step) def next_nonempty_frame(self, ts): - """ Find the next trajectory frame for which a representative auxiliary + """Find the next trajectory frame for which a representative auxiliary value can be calculated. That is, the next trajectory frame to which one or more auxiliary steps @@ -660,9 +701,10 @@ def next_nonempty_frame(self, ts): The returned index may be out of range for the trajectory. """ step = self.step - while step < self.n_steps-1: - next_frame, time_diff = self.step_to_frame(self.step+1, ts, - return_time_diff=True) + while step < self.n_steps - 1: + next_frame, time_diff = self.step_to_frame( + self.step + 1, ts, return_time_diff=True + ) if self.cutoff is not None and time_diff > self.cutoff: # 'representative values' will be NaN; check next step step = step + 1 @@ -672,7 +714,7 @@ def next_nonempty_frame(self, ts): return None def __getitem__(self, i): - """ Return the AuxStep corresponding to the *i*-th auxiliary step(s) + """Return the AuxStep corresponding to the *i*-th auxiliary step(s) (0-based). Negative numbers are counted from the end. *i* may be an integer (in which case the corresponding AuxStep is @@ -704,15 +746,21 @@ def __getitem__(self, i): # default stop to after last frame (i.e. n_steps) # n_steps is a valid stop index but will fail _check_index; # deal with separately - stop = (i.stop if i.stop == self.n_steps - else self._check_index(i.stop) if i.stop is not None - else self.n_steps) + stop = ( + i.stop + if i.stop == self.n_steps + else ( + self._check_index(i.stop) + if i.stop is not None + else self.n_steps + ) + ) step = i.step or 1 if not isinstance(step, numbers.Integral) or step < 1: - raise ValueError("Step must be positive integer") # allow -ve? + raise ValueError("Step must be positive integer") # allow -ve? if start > stop: raise IndexError("Stop frame is lower than start frame") - return self._slice_iter(slice(start,stop,step)) + return self._slice_iter(slice(start, stop, step)) else: raise TypeError("Index must be integer, list of integers or slice") @@ -722,8 +770,10 @@ def _check_index(self, i): if i < 0: i = i + self.n_steps if i < 0 or i >= self.n_steps: - raise IndexError("{} is out of range of auxiliary (num. steps " - "{})".format(i, self.n_steps)) + raise IndexError( + "{} is out of range of auxiliary (num. steps " + "{})".format(i, self.n_steps) + ) return i def _list_iter(self, i): @@ -735,16 +785,17 @@ def _slice_iter(self, i): yield self._go_to_step(j) def _go_to_step(self, i): - """ Move to and read i-th auxiliary step. """ + """Move to and read i-th auxiliary step.""" # Need to define in each auxiliary reader raise NotImplementedError( - "BUG: Override _go_to_step() in auxiliary reader!") + "BUG: Override _go_to_step() in auxiliary reader!" + ) def _reset_frame_data(self): self.frame_data = {} def _add_step_to_frame_data(self, ts_time): - """ Update ``frame_data`` with values for the current step. + """Update ``frame_data`` with values for the current step. Parameters ---------- @@ -756,7 +807,7 @@ def _add_step_to_frame_data(self, ts_time): self.frame_data[time_diff] = self.auxstep.data def calc_representative(self): - """ Calculate representative auxiliary value(s) from the data in + """Calculate representative auxiliary value(s) from the data in *frame_data*. Currently implemented options for calculating representative value are: @@ -782,14 +833,17 @@ def calc_representative(self): if self.cutoff is None: cutoff_data = self.frame_data else: - cutoff_data = {key: val for key, val in self.frame_data.items() - if abs(key) <= self.cutoff} + cutoff_data = { + key: val + for key, val in self.frame_data.items() + if abs(key) <= self.cutoff + } if len(cutoff_data) == 0: # no steps are 'assigned' to this trajectory frame, so return # values of ``np.nan`` value = self.auxstep._empty_data() - elif self.represent_ts_as == 'closest': + elif self.represent_ts_as == "closest": min_diff = min([abs(i) for i in cutoff_data]) # we don't know the original sign, and might have two equally-spaced # steps; check the earlier time first @@ -797,11 +851,11 @@ def calc_representative(self): value = cutoff_data[-min_diff] except KeyError: value = cutoff_data[min_diff] - elif self.represent_ts_as == 'average': + elif self.represent_ts_as == "average": try: - value = np.mean(np.array( - [val for val in cutoff_data.values()] - ), axis=0) + value = np.mean( + np.array([val for val in cutoff_data.values()]), axis=0 + ) except TypeError: # for readers like EDRReader, the above does not work # because each step contains a dictionary of numpy arrays @@ -827,7 +881,7 @@ def close(self): @property def n_steps(self): - """ Total number of steps in the auxiliary data. """ + """Total number of steps in the auxiliary data.""" try: return self._n_steps except AttributeError: @@ -835,7 +889,7 @@ def n_steps(self): return self._n_steps def step_to_time(self, i): - """ Return time of auxiliary step *i*. + """Return time of auxiliary step *i*. Calculated using ``dt`` and ``initial_time`` if ``constant_dt`` is True; otherwise from the list of times as read from the auxiliary data for @@ -857,10 +911,12 @@ def step_to_time(self, i): When *i* not in valid range """ if i >= self.n_steps: - raise ValueError("{0} is not a valid step index (total number of " - "steps is {1})".format(i, self.n_steps)) + raise ValueError( + "{0} is not a valid step index (total number of " + "steps is {1})".format(i, self.n_steps) + ) if self.constant_dt: - return i*self.dt+self.initial_time + return i * self.dt + self.initial_time else: try: return self._times[i] @@ -870,7 +926,7 @@ def step_to_time(self, i): @property def represent_ts_as(self): - """ Method by which 'representative' timestep values of auxiliary data + """Method by which 'representative' timestep values of auxiliary data will be calculated. """ return self._represent_ts_as @@ -878,17 +934,18 @@ def represent_ts_as(self): @represent_ts_as.setter def represent_ts_as(self, new): if new not in self.represent_options: - raise ValueError("{0} is not a valid option for calculating " - "representative value(s). Enabled options are: " - "{1}".format(new, self.represent_options)) + raise ValueError( + "{0} is not a valid option for calculating " + "representative value(s). Enabled options are: " + "{1}".format(new, self.represent_options) + ) self._represent_ts_as = new - def __del__(self): self.close() def get_description(self): - """ Get the values of the parameters necessary for replicating the + """Get the values of the parameters necessary for replicating the AuxReader. An AuxReader can be duplicated using @@ -909,8 +966,10 @@ def get_description(self): Key-word arguments and values that can be used to replicate the AuxReader. """ - description = {attr.strip('_'): getattr(self, attr) - for attr in self.required_attrs} + description = { + attr.strip("_"): getattr(self, attr) + for attr in self.required_attrs + } return description def __eq__(self, other): @@ -963,7 +1022,7 @@ def time_selector(self, new): # if constant_dt is False and so we're using a _times list, this will # now be made invalid try: - del(self._times) + del self._times except AttributeError: pass @@ -988,8 +1047,8 @@ def data_selector(self, new): @property def constant_dt(self): - """ True if ``dt`` is constant throughout the auxiliary (as stored in - ``auxstep``) """ + """True if ``dt`` is constant throughout the auxiliary (as stored in + ``auxstep``)""" return self.auxstep._constant_dt @constant_dt.setter @@ -998,7 +1057,7 @@ def constant_dt(self, new): class AuxFileReader(AuxReader): - """ Base class for auxiliary readers that read from file. + """Base class for auxiliary readers that read from file. Extends AuxReader with attributes and methods particular to reading auxiliary data from an open file, for use when auxiliary files may be too @@ -1028,26 +1087,26 @@ def __init__(self, filename, **kwargs): super(AuxFileReader, self).__init__(**kwargs) def close(self): - """ Close *auxfile*. """ + """Close *auxfile*.""" if self.auxfile is None: return self.auxfile.close() self.auxfile = None def _restart(self): - """ Reposition to just before first step. """ + """Reposition to just before first step.""" self.auxfile.seek(0) self.auxstep.step = -1 def _reopen(self): - """ Close and then reopen *auxfile*. """ + """Close and then reopen *auxfile*.""" if self.auxfile is not None: self.auxfile.close() self.auxfile = open(self._auxdata) self.auxstep.step = -1 def _go_to_step(self, i): - """ Move to and read i-th auxiliary step. + """Move to and read i-th auxiliary step. Parameters ---------- @@ -1066,8 +1125,10 @@ def _go_to_step(self, i): """ ## could seek instead? if i >= self.n_steps: - raise ValueError("Step index {0} is not valid for auxiliary " - "(num. steps {1}!".format(i, self.n_steps)) + raise ValueError( + "Step index {0} is not valid for auxiliary " + "(num. steps {1}!".format(i, self.n_steps) + ) value = self.rewind() while self.step != i: value = self.next() diff --git a/package/MDAnalysis/auxiliary/core.py b/package/MDAnalysis/auxiliary/core.py index e62109e1517..03e65b7c4a7 100644 --- a/package/MDAnalysis/auxiliary/core.py +++ b/package/MDAnalysis/auxiliary/core.py @@ -59,7 +59,7 @@ def get_auxreader_for(auxdata=None, format=None): """ if not auxdata and not format: - raise ValueError('Must provide either auxdata or format') + raise ValueError("Must provide either auxdata or format") if format is None: if isinstance(auxdata, str): @@ -81,10 +81,11 @@ def get_auxreader_for(auxdata=None, format=None): errmsg = f"Unknown auxiliary data format {format}" raise ValueError(errmsg) from None + def auxreader(auxdata, format=None, **kwargs): - """ Return an auxiliary reader instance for *auxdata*. + """Return an auxiliary reader instance for *auxdata*. - An appropriate reader class is first obtained using + An appropriate reader class is first obtained using :func:`get_auxreader_for`, and an auxiliary reader instance for *auxdata* then created and returned. diff --git a/package/pyproject.toml b/package/pyproject.toml index c27a92c0296..97217c60e5f 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -131,6 +131,7 @@ include = ''' tables\.py | due\.py | setup\.py +| MDAnalysis/auxiliary/.*\.py | visualization/.*\.py ) ''' diff --git a/testsuite/MDAnalysisTests/auxiliary/base.py b/testsuite/MDAnalysisTests/auxiliary/base.py index 7394de17806..e3e82a01610 100644 --- a/testsuite/MDAnalysisTests/auxiliary/base.py +++ b/testsuite/MDAnalysisTests/auxiliary/base.py @@ -23,43 +23,50 @@ import MDAnalysis as mda import numpy as np import pytest -from MDAnalysisTests.datafiles import (COORDINATES_XTC, COORDINATES_TOPOLOGY) +from MDAnalysisTests.datafiles import COORDINATES_XTC, COORDINATES_TOPOLOGY from numpy.testing import assert_almost_equal, assert_equal def test_get_bad_auxreader_format_raises_ValueError(): # should raise a ValueError when no AuxReaders with match the specified format with pytest.raises(ValueError): - mda.auxiliary.core.get_auxreader_for(format='bad-format') + mda.auxiliary.core.get_auxreader_for(format="bad-format") class BaseAuxReference(object): - ## assumes the reference auxiliary data has 5 steps, with three values + ## assumes the reference auxiliary data has 5 steps, with three values ## for each step: i, 2*i and 2^i, where i is the step number. - ## If a particular AuxReader is such that auxiliary data is read in a - ## format other than np.array([i, 2*i, 2**i]), format_data() should be + ## If a particular AuxReader is such that auxiliary data is read in a + ## format other than np.array([i, 2*i, 2**i]), format_data() should be ## overwritten tp return the appropriate format def __init__(self): self.n_steps = 5 self.dt = 1 self.initial_time = 0 - self.name = 'test' + self.name = "test" # reference description of the (basic) auxiliary reader. Will # have to add 'format' and 'auxdata' when creating the reference # for each particular reader - self.description= {'dt':self.dt, 'represent_ts_as':'closest', - 'initial_time':self.initial_time, 'time_selector':None, - 'data_selector':None, 'constant_dt':True, - 'cutoff': None, 'auxname': self.name} + self.description = { + "dt": self.dt, + "represent_ts_as": "closest", + "initial_time": self.initial_time, + "time_selector": None, + "data_selector": None, + "constant_dt": True, + "cutoff": None, + "auxname": self.name, + } def reference_auxstep(i): # create a reference AuxStep for step i - auxstep = mda.auxiliary.base.AuxStep(dt=self.dt, - initial_time=self.initial_time) + auxstep = mda.auxiliary.base.AuxStep( + dt=self.dt, initial_time=self.initial_time + ) auxstep.step = i - auxstep._data = self.format_data([i, 2*i, 2**i]) + auxstep._data = self.format_data([i, 2 * i, 2**i]) return auxstep self.auxsteps = [reference_auxstep(i) for i in range(self.n_steps)] @@ -68,15 +75,17 @@ def reference_auxstep(i): ## through the specified auxiliary steps... self.iter_list = [1, -2] self.iter_list_auxsteps = [self.auxsteps[1], self.auxsteps[3]] - self.iter_slice = slice(None, None, 2) # every second step - self.iter_slice_auxsteps = [self.auxsteps[0], self.auxsteps[2], - self.auxsteps[4]] + self.iter_slice = slice(None, None, 2) # every second step + self.iter_slice_auxsteps = [ + self.auxsteps[0], + self.auxsteps[2], + self.auxsteps[4], + ] def reference_timestep(dt=1, offset=0): - # return a trajectory timestep with specified dt, offset + move to - # frame 1; for use in auxiliary reading of different timesteps - ts = mda.coordinates.base.Timestep(0, dt=dt, - time_offset=offset) + # return a trajectory timestep with specified dt, offset + move to + # frame 1; for use in auxiliary reading of different timesteps + ts = mda.coordinates.base.Timestep(0, dt=dt, time_offset=offset) ts.frame = 1 return ts @@ -85,11 +94,11 @@ def reference_timestep(dt=1, offset=0): ## step 1 (1 ps) and step 2 (2 ps). self.lower_freq_ts = reference_timestep(dt=2, offset=0) # 'closest' representative value will match step 2 - self.lowf_closest_rep = self.format_data([2, 2*2, 2**2]) + self.lowf_closest_rep = self.format_data([2, 2 * 2, 2**2]) # 'average' representative value self.lowf_average_rep = self.format_data([1.5, 3, 3]) - ## test reading a timestep with higher frequency. Auxiliart steps with + ## test reading a timestep with higher frequency. Auxiliart steps with ## times between [0.25ps, 0.75ps) will be assigned to this timestep, i.e. ## no auxiliary steps self.higher_freq_ts = reference_timestep(dt=0.5, offset=0) @@ -100,27 +109,30 @@ def reference_timestep(dt=1, offset=0): ## step 1 (1 ps) self.offset_ts = reference_timestep(dt=1, offset=0.25) # 'closest' representative value will match step 1 data - self.offset_closest_rep = self.format_data([1, 2*1, 2**1]) + self.offset_closest_rep = self.format_data([1, 2 * 1, 2**1]) ## testing cutoff for representative values self.cutoff = 0 # for 'average': use low frequenct timestep, only step 2 within 0ps cutoff - self.lowf_cutoff_average_rep = self.format_data([2, 2*2, 2**2]) + self.lowf_cutoff_average_rep = self.format_data([2, 2 * 2, 2**2]) # for 'closest': use offset timestep; no timestep within 0ps cutoff - self.offset_cutoff_closest_rep = self.format_data([np.nan, np.nan, np.nan]) + self.offset_cutoff_closest_rep = self.format_data( + [np.nan, np.nan, np.nan] + ) - ## testing selection of time/data. Overload for each auxilairy format + ## testing selection of time/data. Overload for each auxilairy format ## as appropraite. - # default None behavior set here so won't get errors when time/data - # selection not implemented. - self.time_selector = None + # default None behavior set here so won't get errors when time/data + # selection not implemented. + self.time_selector = None self.select_time_ref = np.arange(self.n_steps) - self.data_selector = None - self.select_data_ref = [self.format_data([2*i, 2**i]) for i in range(self.n_steps)] - + self.data_selector = None + self.select_data_ref = [ + self.format_data([2 * i, 2**i]) for i in range(self.n_steps) + ] def format_data(self, data): - ## overload if auxiliary reader will read data with a format different + ## overload if auxiliary reader will read data with a format different ## to e.g. np.array([0, 0, 1]) return np.array(data) @@ -138,7 +150,9 @@ def test_dt(self, ref, reader): assert reader.dt == ref.dt, "dt does not match" def test_initial_time(self, ref, reader): - assert reader.initial_time == ref.initial_time, "initial time does not match" + assert ( + reader.initial_time == ref.initial_time + ), "initial time does not match" def test_first_step(self, ref, reader): # on first loading we should start at step 0 @@ -185,7 +199,7 @@ def test_invalid_step_to_time_raises_ValueError(self, reader): with pytest.raises(ValueError): reader.step_to_time(reader.n_steps) - def test_iter(self,ref, reader): + def test_iter(self, ref, reader): for i, val in enumerate(reader): assert_auxstep_equal(val, ref.auxsteps[i]) @@ -200,64 +214,74 @@ def test_iter_slice(self, ref, reader): assert_auxstep_equal(val, ref.iter_slice_auxsteps[i]) def test_slice_start_after_stop_raises_IndexError(self, reader): - #should raise IndexError if start frame after end frame + # should raise IndexError if start frame after end frame with pytest.raises(IndexError): reader[2:1] def test_slice_out_of_range_raises_IndexError(self, ref, reader): # should raise IndexError if indices our of range with pytest.raises(IndexError): - reader[ref.n_steps:] + reader[ref.n_steps :] def test_slice_non_int_raises_TypeError(self, reader): # should raise TypeError if try pass in non-integer to slice with pytest.raises(TypeError): - reader['a':] + reader["a":] def test_bad_represent_raises_ValueError(self, reader): # if we try to set represent_ts_as to something not listed as a # valid option, we should get a ValueError with pytest.raises(ValueError): - reader.represent_ts_as = 'invalid-option' + reader.represent_ts_as = "invalid-option" def test_time_selector(self, ref): # reload the reader, passing a time selector - reader = ref.reader(ref.testdata, - time_selector = ref.time_selector) + reader = ref.reader(ref.testdata, time_selector=ref.time_selector) # time should still match reference time for each step for i, val in enumerate(reader): - assert val.time == ref.select_time_ref[i], "time for step {} does not match".format(i) + assert ( + val.time == ref.select_time_ref[i] + ), "time for step {} does not match".format(i) def test_time_non_constant_dt(self, reader): reader.constant_dt = False - with pytest.raises(ValueError, match="If dt is not constant, must have a valid time selector"): + with pytest.raises( + ValueError, + match="If dt is not constant, must have a valid time selector", + ): reader.time def test_time_selector_manual(self, ref): - reader = ref.reader(ref.testdata, - time_selector = ref.time_selector) + reader = ref.reader(ref.testdata, time_selector=ref.time_selector) # Manually set time selector reader.time_selector = ref.time_selector for i, val in enumerate(reader): - assert val.time == ref.select_time_ref[i], "time for step {} does not match".format(i) + assert ( + val.time == ref.select_time_ref[i] + ), "time for step {} does not match".format(i) def test_data_selector(self, ref): # reload reader, passing in a data selector - reader = ref.reader(ref.testdata, - data_selector=ref.data_selector) + reader = ref.reader(ref.testdata, data_selector=ref.data_selector) # data should match reference data for each step for i, val in enumerate(reader): - assert_equal(val.data, ref.select_data_ref[i], "data for step {0} does not match".format(i)) + assert_equal( + val.data, + ref.select_data_ref[i], + "data for step {0} does not match".format(i), + ) def test_no_constant_dt(self, ref): ## assume we can select time... # reload reader, without assuming constant dt - reader = ref.reader(ref.testdata, - time_selector=ref.time_selector, - constant_dt=False) + reader = ref.reader( + ref.testdata, time_selector=ref.time_selector, constant_dt=False + ) # time should match reference for selecting time, for each step for i, val in enumerate(reader): - assert val.time == ref.select_time_ref[i], "data for step {} does not match".format(i) + assert ( + val.time == ref.select_time_ref[i] + ), "data for step {} does not match".format(i) def test_update_ts_without_auxname_raises_ValueError(self, ref): # reload reader without auxname @@ -271,20 +295,26 @@ def test_read_lower_freq_timestep(self, ref, reader): ts = ref.lower_freq_ts reader.update_ts(ts) # check the value set in ts is as we expect - assert_almost_equal(ts.aux.test, ref.lowf_closest_rep, - err_msg="Representative value in ts.aux does not match") + assert_almost_equal( + ts.aux.test, + ref.lowf_closest_rep, + err_msg="Representative value in ts.aux does not match", + ) def test_represent_as_average(self, ref, reader): # test the 'average' option for 'represent_ts_as' # reset the represent method to 'average'... - reader.represent_ts_as = 'average' + reader.represent_ts_as = "average" # read timestep; use the low freq timestep ts = ref.lower_freq_ts reader.update_ts(ts) # check the representative value set in ts is as expected - assert_almost_equal(ts.aux.test, ref.lowf_average_rep, - err_msg="Representative value does not match when " - "using with option 'average'") + assert_almost_equal( + ts.aux.test, + ref.lowf_average_rep, + err_msg="Representative value does not match when " + "using with option 'average'", + ) def test_represent_as_average_with_cutoff(self, ref, reader): # test the 'represent_ts_as' 'average' option when we have a cutoff set @@ -294,16 +324,22 @@ def test_represent_as_average_with_cutoff(self, ref, reader): ts = ref.lower_freq_ts reader.update_ts(ts) # check representative value set in ts is as expected - assert_almost_equal(ts.aux.test, ref.lowf_cutoff_average_rep, - err_msg="Representative value does not match when " - "applying cutoff") + assert_almost_equal( + ts.aux.test, + ref.lowf_cutoff_average_rep, + err_msg="Representative value does not match when " + "applying cutoff", + ) def test_read_offset_timestep(self, ref, reader): # try reading a timestep offset from auxiliary ts = ref.offset_ts reader.update_ts(ts) - assert_almost_equal(ts.aux.test, ref.offset_closest_rep, - err_msg="Representative value in ts.aux does not match") + assert_almost_equal( + ts.aux.test, + ref.offset_closest_rep, + err_msg="Representative value in ts.aux does not match", + ) def test_represent_as_closest_with_cutoff(self, ref, reader): # test the 'represent_ts_as' 'closest' option when we have a cutoff set @@ -313,16 +349,22 @@ def test_represent_as_closest_with_cutoff(self, ref, reader): ts = ref.offset_ts reader.update_ts(ts) # check representative value set in ts is as expected - assert_almost_equal(ts.aux.test, ref.offset_cutoff_closest_rep, - err_msg="Representative value does not match when " - "applying cutoff") + assert_almost_equal( + ts.aux.test, + ref.offset_cutoff_closest_rep, + err_msg="Representative value does not match when " + "applying cutoff", + ) def test_read_higher_freq_timestep(self, ref, reader): # try reading a timestep with higher frequency ts = ref.higher_freq_ts reader.update_ts(ts) - assert_almost_equal(ts.aux.test, ref.highf_rep, - err_msg="Representative value in ts.aux does not match") + assert_almost_equal( + ts.aux.test, + ref.highf_rep, + err_msg="Representative value in ts.aux does not match", + ) def test_get_auxreader_for(self, ref, reader): # check guesser gives us right reader @@ -334,18 +376,24 @@ def test_iterate_through_trajectory(self, ref, ref_universe): # trajectory here has same dt, offset; so there's a direct correspondence # between frames and steps for i, ts in enumerate(ref_universe.trajectory): - assert_equal(ts.aux.test, ref.auxsteps[i].data, - "representative value does not match when " - "iterating through all trajectory timesteps") + assert_equal( + ts.aux.test, + ref.auxsteps[i].data, + "representative value does not match when " + "iterating through all trajectory timesteps", + ) def test_iterate_as_auxiliary_from_trajectory(self, ref, ref_universe): # check representative values of aux for each frame are as expected # trajectory here has same dt, offset, so there's a direct correspondence # between frames and steps, and iter_as_aux will run through all frames - for i, ts in enumerate(ref_universe.trajectory.iter_as_aux('test')): - assert_equal(ts.aux.test, ref.auxsteps[i].data, - "representative value does not match when " - "iterating through all trajectory timesteps") + for i, ts in enumerate(ref_universe.trajectory.iter_as_aux("test")): + assert_equal( + ts.aux.test, + ref.auxsteps[i].data, + "representative value does not match when " + "iterating through all trajectory timesteps", + ) def test_auxiliary_read_ts_rewind(self, ref_universe): # AuxiliaryBase.read_ts() should retrieve the correct step after @@ -354,19 +402,26 @@ def test_auxiliary_read_ts_rewind(self, ref_universe): aux_info_0 = ref_universe.trajectory[0].aux.test ref_universe.trajectory[-1] aux_info_0_rewind = ref_universe.trajectory[0].aux.test - assert_equal(aux_info_0, aux_info_0_rewind, - "aux info was retrieved incorrectly " - "after reading the last step") + assert_equal( + aux_info_0, + aux_info_0_rewind, + "aux info was retrieved incorrectly " + "after reading the last step", + ) def test_get_description(self, ref, reader): description = reader.get_description() for attr in ref.description: - assert description[attr] == ref.description[attr], "'Description' does not match for {}".format(attr) + assert ( + description[attr] == ref.description[attr] + ), "'Description' does not match for {}".format(attr) def test_load_from_description(self, reader): description = reader.get_description() new = mda.auxiliary.core.auxreader(**description) - assert new == reader, "AuxReader reloaded from description does not match" + assert ( + new == reader + ), "AuxReader reloaded from description does not match" def test_step_to_frame_out_of_bounds(self, reader, ref): @@ -391,14 +446,18 @@ def test_step_to_frame_time_diff(self, reader, ref): # Test all 5 frames for idx in range(5): - frame, time_diff = reader.step_to_frame(idx, ts, return_time_diff=True) + frame, time_diff = reader.step_to_frame( + idx, ts, return_time_diff=True + ) assert frame == idx np.testing.assert_almost_equal(time_diff, idx * 0.1) def test_go_to_step_fail(self, reader): - with pytest.raises(ValueError, match="Step index [0-9]* is not valid for auxiliary"): + with pytest.raises( + ValueError, match="Step index [0-9]* is not valid for auxiliary" + ): reader._go_to_step(reader.n_steps) @pytest.mark.parametrize("constant", [True, False]) @@ -416,20 +475,26 @@ def test_copy(self, reader): def assert_auxstep_equal(A, B): if not isinstance(A, mda.auxiliary.base.AuxStep): - raise AssertionError('A is not of type AuxStep') + raise AssertionError("A is not of type AuxStep") if not isinstance(B, mda.auxiliary.base.AuxStep): - raise AssertionError('B is not of type AuxStep') + raise AssertionError("B is not of type AuxStep") if A.step != B.step: - raise AssertionError('A and B refer to different steps: A.step = {}, ' - 'B.step = {}'.format(A.step, B.step)) + raise AssertionError( + "A and B refer to different steps: A.step = {}, " + "B.step = {}".format(A.step, B.step) + ) if A.time != B.time: - raise AssertionError('A and B have different times: A.time = {}, ' - 'B.time = {}'.format(A.time, B.time)) + raise AssertionError( + "A and B have different times: A.time = {}, " + "B.time = {}".format(A.time, B.time) + ) if isinstance(A.data, dict): for term in A.data: assert_almost_equal(A.data[term], B.data[term]) else: if any(A.data != B.data): # e.g. XVGReader - raise AssertionError('A and B have different data: A.data = {}, ' - 'B.data = {}'.format(A.data, B.data)) + raise AssertionError( + "A and B have different data: A.data = {}, " + "B.data = {}".format(A.data, B.data) + ) diff --git a/testsuite/MDAnalysisTests/auxiliary/test_core.py b/testsuite/MDAnalysisTests/auxiliary/test_core.py index f06320af411..8a630c6e48d 100644 --- a/testsuite/MDAnalysisTests/auxiliary/test_core.py +++ b/testsuite/MDAnalysisTests/auxiliary/test_core.py @@ -28,12 +28,16 @@ def test_get_auxreader_for_none(): - with pytest.raises(ValueError, match="Must provide either auxdata or format"): + with pytest.raises( + ValueError, match="Must provide either auxdata or format" + ): mda.auxiliary.core.get_auxreader_for() def test_get_auxreader_for_wrong_auxdata(): - with pytest.raises(ValueError, match="Unknown auxiliary data format for auxdata:"): + with pytest.raises( + ValueError, match="Unknown auxiliary data format for auxdata:" + ): mda.auxiliary.core.get_auxreader_for(auxdata="test.none") @@ -43,8 +47,9 @@ def test_get_auxreader_for_wrong_format(): def test_notimplemented_read_next_timestep(): - with pytest.raises(NotImplementedError, match="BUG: Override " - "_read_next_step()"): + with pytest.raises( + NotImplementedError, match="BUG: Override " "_read_next_step()" + ): reader = mda.auxiliary.base.AuxReader() @@ -54,7 +59,8 @@ def _read_next_step(self): def test_notimplemented_memory_usage(): - with pytest.raises(NotImplementedError, match="BUG: Override " - "_memory_usage()"): + with pytest.raises( + NotImplementedError, match="BUG: Override " "_memory_usage()" + ): reader = No_Memory_Usage() reader._memory_usage() diff --git a/testsuite/MDAnalysisTests/auxiliary/test_edr.py b/testsuite/MDAnalysisTests/auxiliary/test_edr.py index 9aa6e762b07..8668c8a056b 100644 --- a/testsuite/MDAnalysisTests/auxiliary/test_edr.py +++ b/testsuite/MDAnalysisTests/auxiliary/test_edr.py @@ -32,14 +32,18 @@ from MDAnalysis import units from MDAnalysis.auxiliary.EDR import HAS_PYEDR -from MDAnalysisTests.datafiles import (AUX_EDR, - AUX_EDR_TPR, - AUX_EDR_XTC, - AUX_EDR_RAW, - AUX_EDR_SINGLE_FRAME) -from MDAnalysisTests.auxiliary.base import (BaseAuxReaderTest, - BaseAuxReference, - assert_auxstep_equal) +from MDAnalysisTests.datafiles import ( + AUX_EDR, + AUX_EDR_TPR, + AUX_EDR_XTC, + AUX_EDR_RAW, + AUX_EDR_SINGLE_FRAME, +) +from MDAnalysisTests.auxiliary.base import ( + BaseAuxReaderTest, + BaseAuxReference, + assert_auxstep_equal, +) def read_raw_data_file(step): @@ -48,7 +52,7 @@ def read_raw_data_file(step): with open(AUX_EDR_RAW) as f: rawdata = f.readlines() n_entries = 52 # number of aux terms per step - stepdata = rawdata[step * n_entries: (step + 1) * n_entries] + stepdata = rawdata[step * n_entries : (step + 1) * n_entries] aux_dict = {} edr_units = {} for line in stepdata: @@ -87,17 +91,23 @@ def __init__(self): self.reader = mda.auxiliary.EDR.EDRReader self.n_steps = 4 self.dt = 0.02 - self.description = {'dt': self.dt, 'represent_ts_as': 'closest', - 'initial_time': self.initial_time, - 'time_selector': "Time", 'data_selector': None, - 'constant_dt': True, 'cutoff': None, - 'auxname': self.name} + self.description = { + "dt": self.dt, + "represent_ts_as": "closest", + "initial_time": self.initial_time, + "time_selector": "Time", + "data_selector": None, + "constant_dt": True, + "cutoff": None, + "auxname": self.name, + } def reference_auxstep(i): # create a reference AuxStep for step i t_init = self.initial_time - auxstep = mda.auxiliary.EDR.EDRStep(dt=self.dt, - initial_time=t_init) + auxstep = mda.auxiliary.EDR.EDRStep( + dt=self.dt, initial_time=t_init + ) auxstep.step = i auxstep._data = get_auxstep_data(i) return auxstep @@ -105,13 +115,14 @@ def reference_auxstep(i): self.auxsteps = [reference_auxstep(i) for i in range(self.n_steps)] # add the auxdata and format for .edr to the reference description - self.description['auxdata'] = Path(self.testdata).resolve() - self.description['format'] = self.reader.format + self.description["auxdata"] = Path(self.testdata).resolve() + self.description["format"] = self.reader.format # for testing the selection of data/time self.time_selector = "Time" - self.select_time_ref = [step._data[self.time_selector] - for step in self.auxsteps] + self.select_time_ref = [ + step._data[self.time_selector] for step in self.auxsteps + ] self.data_selector = "Bond" # selects all data self.select_data_ref = [step._data["Bond"] for step in self.auxsteps] @@ -125,8 +136,7 @@ def reference_auxstep(i): def reference_timestep(dt=0.02, offset=0): # return a trajectory timestep with specified dt, offset + move to # frame 1; for use in auxiliary reading of different timesteps - ts = mda.coordinates.base.Timestep(0, dt=dt, - time_offset=offset) + ts = mda.coordinates.base.Timestep(0, dt=dt, time_offset=offset) ts.frame = 1 return ts @@ -167,16 +177,18 @@ def reference_timestep(dt=0.02, offset=0): self.offset_cutoff_closest_rep = np.array(np.nan) # for testing EDRReader.get_data() - self.times = np.array([0., 0.02, 0.04, 0.06]) - self.bonds = np.array([1374.82324219, 1426.22521973, - 1482.0098877, 1470.33752441]) - self.angles = np.array([3764.52734375, 3752.83032227, - 3731.59179688, 3683.40942383]) + self.times = np.array([0.0, 0.02, 0.04, 0.06]) + self.bonds = np.array( + [1374.82324219, 1426.22521973, 1482.0098877, 1470.33752441] + ) + self.angles = np.array( + [3764.52734375, 3752.83032227, 3731.59179688, 3683.40942383] + ) @pytest.mark.skipif(not HAS_PYEDR, reason="pyedr not installed") class TestEDRReader(BaseAuxReaderTest): - """ Class to conduct tests for the auxiliary EDRReader + """Class to conduct tests for the auxiliary EDRReader Normally, it would be desirable to use the tests from :class:`BaseAuxReaderTest`, but this is not possible for some of the tests @@ -195,7 +207,7 @@ def ref(): @pytest.fixture def ref_universe(ref): u = mda.Universe(AUX_EDR_TPR, AUX_EDR_XTC) -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 u.trajectory.add_auxiliary({"test": "Bond"}, ref.testdata) return u @@ -205,9 +217,10 @@ def reader(ref): reader = ref.reader( ref.testdata, initial_time=ref.initial_time, - dt=ref.dt, auxname=ref.name, + dt=ref.dt, + auxname=ref.name, time_selector="Time", - data_selector=None + data_selector=None, ) ref_units = get_edr_unit_dict(0) if reader.unit_dict != ref_units: @@ -215,9 +228,9 @@ def reader(ref): data = reader.data_dict[term] reader_unit = reader.unit_dict[term] try: - reader.data_dict[term] = units.convert(data, - reader_unit, - ref_unit) + reader.data_dict[term] = units.convert( + data, reader_unit, ref_unit + ) except ValueError: continue # some units not supported yet reader.rewind() @@ -226,9 +239,12 @@ def reader(ref): def test_time_non_constant_dt(self, reader): reader.constant_dt = False reader.time_selector = None - with pytest.raises(ValueError, match="If dt is not constant, " - "must have a valid time " - "selector"): + with pytest.raises( + ValueError, + match="If dt is not constant, " + "must have a valid time " + "selector", + ): reader.time def test_iterate_through_trajectory(self, ref, ref_universe): @@ -243,7 +259,7 @@ def test_iterate_as_auxiliary_from_trajectory(self, ref, ref_universe): # trajectory here has same dt, offset, so there's a direct # correspondence between frames and steps, and iter_as_aux will run # through all frames - for i, ts in enumerate(ref_universe.trajectory.iter_as_aux('test')): + for i, ts in enumerate(ref_universe.trajectory.iter_as_aux("test")): assert_allclose(ts.aux.test, ref.auxsteps[i].data["Bond"]) def test_step_to_frame_time_diff(self, reader, ref): @@ -253,8 +269,9 @@ def test_step_to_frame_time_diff(self, reader, ref): # Test all 4 frames for idx in range(4): - frame, time_diff = reader.step_to_frame(idx, ts, - return_time_diff=True) + frame, time_diff = reader.step_to_frame( + idx, ts, return_time_diff=True + ) assert frame == idx assert_allclose(time_diff, idx * 0.002) @@ -264,38 +281,47 @@ def test_read_lower_freq_timestep(self, ref, reader): ts = ref.lower_freq_ts reader.update_ts(ts) # check the value set in ts is as we expect - assert_allclose(ts.aux.test["Bond"], ref.lowf_closest_rep, - err_msg="Representative value in ts.aux " - "does not match") + assert_allclose( + ts.aux.test["Bond"], + ref.lowf_closest_rep, + err_msg="Representative value in ts.aux " "does not match", + ) def test_read_higher_freq_timestep(self, ref, reader): # try reading a timestep with higher frequency ts = ref.higher_freq_ts reader.update_ts(ts) - assert_allclose(ts.aux.test, ref.highf_rep, - err_msg="Representative value in ts.aux " - "does not match") + assert_allclose( + ts.aux.test, + ref.highf_rep, + err_msg="Representative value in ts.aux " "does not match", + ) def test_read_offset_timestep(self, ref, reader): # try reading a timestep offset from auxiliary ts = ref.offset_ts reader.update_ts(ts) - assert_allclose(ts.aux.test["Bond"], ref.offset_closest_rep, - err_msg="Representative value in ts.aux " - "does not match") + assert_allclose( + ts.aux.test["Bond"], + ref.offset_closest_rep, + err_msg="Representative value in ts.aux " "does not match", + ) def test_represent_as_average(self, ref, reader): # test the 'average' option for 'represent_ts_as' # reset the represent method to 'average'... - reader.represent_ts_as = 'average' + reader.represent_ts_as = "average" # read timestep; use the low freq timestep ts = ref.lower_freq_ts reader.update_ts(ts) # check the representative value set in ts is as expected test_value = [ts.aux.test["Time"], ts.aux.test["Bond"]] - assert_allclose(test_value, ref.lowf_average_rep, - err_msg="Representative value does not match when " - "using with option 'average'") + assert_allclose( + test_value, + ref.lowf_average_rep, + err_msg="Representative value does not match when " + "using with option 'average'", + ) def test_represent_as_average_with_cutoff(self, ref, reader): # test the 'represent_ts_as' 'average' option when we have a cutoff set @@ -305,11 +331,14 @@ def test_represent_as_average_with_cutoff(self, ref, reader): ts = ref.lower_freq_ts reader.update_ts(ts) # check representative value set in ts is as expected - assert_allclose(ts.aux.test["Bond"], ref.lowf_cutoff_average_rep, - err_msg="Representative value does not match when " - "applying cutoff") + assert_allclose( + ts.aux.test["Bond"], + ref.lowf_cutoff_average_rep, + err_msg="Representative value does not match when " + "applying cutoff", + ) -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_add_all_terms_from_file(self, ref, ref_universe): ref_universe.trajectory.add_auxiliary(auxdata=ref.testdata) # adding "test" manually to match above addition of test term @@ -317,57 +346,61 @@ def test_add_all_terms_from_file(self, ref, ref_universe): terms = [key for key in ref_universe.trajectory._auxs] assert ref_terms == terms -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_add_all_terms_from_reader(self, ref_universe, reader): ref_universe.trajectory.add_auxiliary(auxdata=reader) ref_terms = ["test"] + [key for key in get_auxstep_data(0).keys()] terms = [key for key in ref_universe.trajectory._auxs] assert ref_terms == terms -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_add_term_list_custom_names_from_file(self, ref, ref_universe): - ref_universe.trajectory.add_auxiliary({"bond": "Bond", - "temp": "Temperature"}, - ref.testdata) + ref_universe.trajectory.add_auxiliary( + {"bond": "Bond", "temp": "Temperature"}, ref.testdata + ) ref_dict = get_auxstep_data(0) assert ref_universe.trajectory.ts.aux.bond == ref_dict["Bond"] assert ref_universe.trajectory.ts.aux.temp == ref_dict["Temperature"] -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 - def test_add_term_list_custom_names_from_reader(self, ref_universe, - reader): - ref_universe.trajectory.add_auxiliary({"bond": "Bond", - "temp": "Temperature"}, - reader) + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + def test_add_term_list_custom_names_from_reader( + self, ref_universe, reader + ): + ref_universe.trajectory.add_auxiliary( + {"bond": "Bond", "temp": "Temperature"}, reader + ) ref_dict = get_auxstep_data(0) assert ref_universe.trajectory.ts.aux.bond == ref_dict["Bond"] assert ref_universe.trajectory.ts.aux.temp == ref_dict["Temperature"] -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 - def test_raise_error_if_auxname_already_assigned(self, ref_universe, - reader): + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + def test_raise_error_if_auxname_already_assigned( + self, ref_universe, reader + ): with pytest.raises(ValueError, match="Auxiliary data with name"): ref_universe.trajectory.add_auxiliary("test", reader, "Bond") -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_add_single_term_custom_name_from_file(self, ref, ref_universe): - ref_universe.trajectory.add_auxiliary({"temp": "Temperature"}, - ref.testdata) + ref_universe.trajectory.add_auxiliary( + {"temp": "Temperature"}, ref.testdata + ) ref_dict = get_auxstep_data(0) assert ref_universe.trajectory.ts.aux.temp == ref_dict["Temperature"] -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 - def test_add_single_term_custom_name_from_reader(self, ref_universe, - reader): + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + def test_add_single_term_custom_name_from_reader( + self, ref_universe, reader + ): ref_universe.trajectory.add_auxiliary({"temp": "Temperature"}, reader) ref_dict = get_auxstep_data(0) assert ref_universe.trajectory.ts.aux.temp == ref_dict["Temperature"] -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_terms_update_on_iter(self, ref_universe, reader): - ref_universe.trajectory.add_auxiliary({"bond": "Bond", - "temp": "Temperature"}, - reader) + ref_universe.trajectory.add_auxiliary( + {"bond": "Bond", "temp": "Temperature"}, reader + ) ref_dict = get_auxstep_data(0) assert ref_universe.trajectory.ts.aux.bond == ref_dict["Bond"] assert ref_universe.trajectory.ts.aux.temp == ref_dict["Temperature"] @@ -376,14 +409,15 @@ def test_terms_update_on_iter(self, ref_universe, reader): assert ref_universe.trajectory.ts.aux.bond == ref_dict["Bond"] assert ref_universe.trajectory.ts.aux.temp == ref_dict["Temperature"] -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_invalid_data_selector(self, ref, ref_universe): with pytest.raises(KeyError, match="'Nonsense' is not a key"): - ref_universe.trajectory.add_auxiliary({"something": "Nonsense"}, - AUX_EDR) + ref_universe.trajectory.add_auxiliary( + {"something": "Nonsense"}, AUX_EDR + ) def test_read_all_times(self, reader): - all_times_expected = np.array([0., 0.02, 0.04, 0.06]) + all_times_expected = np.array([0.0, 0.02, 0.04, 0.06]) assert_allclose(all_times_expected, reader.read_all_times()) def test_get_data_from_string(self, ref, reader): @@ -407,27 +441,32 @@ def test_get_data_everything(self, ref, reader): assert ref_terms == reader.terms assert_allclose(ref.bonds, returned["Bond"]) - @pytest.mark.parametrize("get_data_input", (42, - "Not a valid term", - ["Bond", "Not a valid term"])) + @pytest.mark.parametrize( + "get_data_input", + (42, "Not a valid term", ["Bond", "Not a valid term"]), + ) def test_get_data_invalid_selections(self, reader, get_data_input): with pytest.raises(KeyError, match="data selector"): reader.get_data(get_data_input) -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_warning_when_space_in_aux_spec(self, ref_universe, reader): with pytest.warns(UserWarning, match="Auxiliary name"): - ref_universe.trajectory.add_auxiliary({"Pres. DC": "Pres. DC"}, - reader) + ref_universe.trajectory.add_auxiliary( + {"Pres. DC": "Pres. DC"}, reader + ) -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_warn_too_much_memory_usage(self, ref_universe, reader): - with pytest.warns(UserWarning, match="AuxReader: memory usage " - "warning! Auxiliary data takes up 3[0-9.]*e-06 GB of" - r" memory \(Warning limit: 1e-08 GB\)"): - ref_universe.trajectory.add_auxiliary({"temp": "Temperature"}, - reader, - memory_limit=10) + with pytest.warns( + UserWarning, + match="AuxReader: memory usage " + "warning! Auxiliary data takes up 3[0-9.]*e-06 GB of" + r" memory \(Warning limit: 1e-08 GB\)", + ): + ref_universe.trajectory.add_auxiliary( + {"temp": "Temperature"}, reader, memory_limit=10 + ) def test_auxreader_picklable(self, reader): new_reader = pickle.loads(pickle.dumps(reader)) @@ -441,20 +480,22 @@ def test_units_are_converted_by_EDRReader(self, reader): for term in ["Box-X", "Box-Vel-XX"]: assert original_units[term] != reader_units[term] -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 def test_warning_when_unknown_unit(self, ref_universe, reader): with pytest.warns(UserWarning, match="Could not find"): - ref_universe.trajectory.add_auxiliary({"temp": "Temperature"}, - reader) + ref_universe.trajectory.add_auxiliary( + {"temp": "Temperature"}, reader + ) def test_unit_conversion_is_optional(self, ref): reader = ref.reader( ref.testdata, initial_time=ref.initial_time, - dt=ref.dt, auxname=ref.name, + dt=ref.dt, + auxname=ref.name, time_selector="Time", data_selector=None, - convert_units=False + convert_units=False, ) ref_units = get_edr_unit_dict(0) # The units from AUX_EDR match the ones from the reference @@ -466,9 +507,10 @@ def test_unit_conversion_is_optional(self, ref): @pytest.mark.skipif(not HAS_PYEDR, reason="pyedr not installed") def test_single_frame_input_file(): """Previously, EDRReader could not handle EDR input files with only one - frame. See Issue #3999.""" - reader = mda.auxiliary.EDR.EDRReader(AUX_EDR_SINGLE_FRAME, - convert_units=False) + frame. See Issue #3999.""" + reader = mda.auxiliary.EDR.EDRReader( + AUX_EDR_SINGLE_FRAME, convert_units=False + ) ref_dict = get_auxstep_data(0) reader_data_dict = reader.auxstep.data assert ref_dict == reader_data_dict diff --git a/testsuite/MDAnalysisTests/auxiliary/test_xvg.py b/testsuite/MDAnalysisTests/auxiliary/test_xvg.py index 0afaa2ce423..8926cabe320 100644 --- a/testsuite/MDAnalysisTests/auxiliary/test_xvg.py +++ b/testsuite/MDAnalysisTests/auxiliary/test_xvg.py @@ -28,9 +28,14 @@ import MDAnalysis as mda -from MDAnalysisTests.datafiles import (AUX_XVG, XVG_BAD_NCOL, XVG_BZ2, - COORDINATES_XTC, COORDINATES_TOPOLOGY) -from MDAnalysisTests.auxiliary.base import (BaseAuxReaderTest, BaseAuxReference) +from MDAnalysisTests.datafiles import ( + AUX_XVG, + XVG_BAD_NCOL, + XVG_BZ2, + COORDINATES_XTC, + COORDINATES_TOPOLOGY, +) +from MDAnalysisTests.auxiliary.base import BaseAuxReaderTest, BaseAuxReference from MDAnalysis.auxiliary.XVG import XVGStep @@ -41,17 +46,22 @@ def __init__(self): self.reader = mda.auxiliary.XVG.XVGReader # add the auxdata and format for .xvg to the reference description - self.description['auxdata'] = os.path.abspath(self.testdata) - self.description['format'] = self.reader.format + self.description["auxdata"] = os.path.abspath(self.testdata) + self.description["format"] = self.reader.format # for testing the selection of data/time - self.time_selector = 0 # take time as first value in auxilairy + self.time_selector = 0 # take time as first value in auxilairy self.select_time_ref = np.arange(self.n_steps) - self.data_selector = [1,2] # select the second/third columns from auxiliary - self.select_data_ref = [self.format_data([2*i, 2**i]) for i in range(self.n_steps)] + self.data_selector = [ + 1, + 2, + ] # select the second/third columns from auxiliary + self.select_data_ref = [ + self.format_data([2 * i, 2**i]) for i in range(self.n_steps) + ] -class TestXVGStep(): +class TestXVGStep: @staticmethod @pytest.fixture() @@ -65,7 +75,9 @@ def test_select_time_none(self, step): assert st is None def test_select_time_invalid_index(self, step): - with pytest.raises(ValueError, match="Time selector must be single index"): + with pytest.raises( + ValueError, match="Time selector must be single index" + ): step._select_time([0]) def test_select_data_none(self, step): @@ -74,6 +86,7 @@ def test_select_data_none(self, step): assert st is None + class TestXVGReader(BaseAuxReaderTest): @staticmethod @pytest.fixture() @@ -84,8 +97,8 @@ def ref(): @pytest.fixture def ref_universe(ref): u = mda.Universe(COORDINATES_TOPOLOGY, COORDINATES_XTC) -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 - u.trajectory.add_auxiliary('test', ref.testdata) + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + u.trajectory.add_auxiliary("test", ref.testdata) return u @staticmethod @@ -94,9 +107,10 @@ def reader(ref): return ref.reader( ref.testdata, initial_time=ref.initial_time, - dt=ref.dt, auxname=ref.name, + dt=ref.dt, + auxname=ref.name, time_selector=None, - data_selector=None + data_selector=None, ) def test_changing_n_col_raises_ValueError(self, ref, reader): @@ -107,13 +121,13 @@ def test_changing_n_col_raises_ValueError(self, ref, reader): next(reader) def test_time_selector_out_of_range_raises_ValueError(self, ref, reader): - # if time_selector is not a valid index of _data, a ValueError + # if time_selector is not a valid index of _data, a ValueError # should be raised with pytest.raises(ValueError): reader.time_selector = len(reader.auxstep._data) def test_data_selector_out_of_range_raises_ValueError(self, ref, reader): - # if data_selector is not a valid index of _data, a ValueError + # if data_selector is not a valid index of _data, a ValueError # should be raised with pytest.raises(ValueError): reader.data_selector = [len(reader.auxstep._data)] @@ -124,7 +138,7 @@ def __init__(self): super(XVGFileReference, self).__init__() self.reader = mda.auxiliary.XVG.XVGFileReader self.format = "XVG-F" - self.description['format'] = self.format + self.description["format"] = self.format class TestXVGFileReader(TestXVGReader): @@ -137,8 +151,8 @@ def ref(): @pytest.fixture def ref_universe(ref): u = mda.Universe(COORDINATES_TOPOLOGY, COORDINATES_XTC) -# TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 - u.trajectory.add_auxiliary('test', ref.testdata) + # TODO: Change order of aux_spec and auxdata for 3.0 release, cf. Issue #3811 + u.trajectory.add_auxiliary("test", ref.testdata) return u @staticmethod @@ -150,14 +164,15 @@ def reader(ref): dt=ref.dt, auxname=ref.name, time_selector=None, - data_selector=None + data_selector=None, ) def test_get_auxreader_for(self, ref, reader): # Default reader of .xvg files is intead XVGReader, not XVGFileReader - # so test specifying format - reader = mda.auxiliary.core.get_auxreader_for(ref.testdata, - format=ref.format) + # so test specifying format + reader = mda.auxiliary.core.get_auxreader_for( + ref.testdata, format=ref.format + ) assert reader == ref.reader def test_reopen(self, reader): @@ -169,9 +184,9 @@ def test_reopen(self, reader): def test_xvg_bz2(): reader = mda.auxiliary.XVG.XVGReader(XVG_BZ2) - assert_array_equal(reader.read_all_times(), np.array([0., 50., 100.])) + assert_array_equal(reader.read_all_times(), np.array([0.0, 50.0, 100.0])) def test_xvg_file_bz2(): reader = mda.auxiliary.XVG.XVGFileReader(XVG_BZ2) - assert_array_equal(reader.read_all_times(), np.array([0., 50., 100.])) + assert_array_equal(reader.read_all_times(), np.array([0.0, 50.0, 100.0])) diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 8f23629b706..7f42a28fefd 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -156,5 +156,11 @@ filterwarnings = [ [tool.black] line-length = 79 target-version = ['py310', 'py311', 'py312', 'py313'] -extend-exclude = '.' +include = ''' +( +setup\.py +| MDAnalysisTests/auxiliary/.*\.py +) +''' +extend-exclude = '__pycache__' required-version = '24' diff --git a/testsuite/setup.py b/testsuite/setup.py index 228bfd1fd0a..63629d88791 100755 --- a/testsuite/setup.py +++ b/testsuite/setup.py @@ -50,7 +50,7 @@ class MDA_SDist(sdist.sdist): # To avoid having duplicate AUTHORS file... def run(self): here = os.path.dirname(os.path.abspath(__file__)) - has_authors = os.path.exists(os.path.join(here, 'AUTHORS')) + has_authors = os.path.exists(os.path.join(here, "AUTHORS")) if not has_authors: # If there is no AUTHORS file here, lets hope we're in @@ -59,8 +59,9 @@ def run(self): repo_root = os.path.split(here)[0] try: shutil.copyfile( - os.path.join(repo_root, 'package', 'AUTHORS'), - os.path.join(here, 'AUTHORS')) + os.path.join(repo_root, "package", "AUTHORS"), + os.path.join(here, "AUTHORS"), + ) except: raise IOError("Couldn't grab AUTHORS file") else: @@ -69,19 +70,19 @@ def run(self): super(MDA_SDist, self).run() finally: if not has_authors and copied_authors: - os.remove(os.path.join(here, 'AUTHORS')) + os.remove(os.path.join(here, "AUTHORS")) -if __name__ == '__main__': +if __name__ == "__main__": # this must be in-sync with MDAnalysis RELEASE = "2.9.0-dev0" setup( version=RELEASE, install_requires=[ - 'MDAnalysis=={0!s}'.format(RELEASE), # same as this release! - 'pytest>=3.3.0', # Raised to 3.3.0 due to Issue 2329 - 'hypothesis', + "MDAnalysis=={0!s}".format(RELEASE), # same as this release! + "pytest>=3.3.0", # Raised to 3.3.0 due to Issue 2329 + "hypothesis", ], - cmdclass={'sdist': MDA_SDist}, + cmdclass={"sdist": MDA_SDist}, ) From 905f197146d2223b2e4ffac8a581124d31b190c3 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:30:22 +0100 Subject: [PATCH 45/57] Addition of `pytest` case for not `None` values for `frames` and `start`/`stop`/`step` (#4769) --- testsuite/MDAnalysisTests/analysis/test_base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index e2fe428376e..377d70602ba 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -122,6 +122,19 @@ def test_incompatible_n_workers(u): FrameAnalysis(u).run(backend=backend, n_workers=3) +def test_frame_values_incompatability(u): + start, stop, step = 0, 4, 1 + frames = [1, 2, 3, 4] + + with pytest.raises(ValueError, + match="start/stop/step cannot be combined with frames"): + FrameAnalysis(u.trajectory).run( + frames=frames, + start=start, + stop=stop, + step=step + ) + def test_n_workers_conflict_raises_value_error(u): backend_instance = ManyWorkersBackend(n_workers=4) From 557f27d658ff0d4011bbe0efa03495f18aa2c1ce Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Sat, 30 Nov 2024 23:03:39 +0100 Subject: [PATCH 46/57] [fmt] transformations (#4809) --- package/MDAnalysis/transformations/base.py | 4 +- .../transformations/boxdimensions.py | 29 ++- package/MDAnalysis/transformations/fit.py | 98 +++++--- package/MDAnalysis/transformations/nojump.py | 13 +- .../transformations/positionaveraging.py | 66 +++--- package/MDAnalysis/transformations/rotate.py | 87 ++++--- .../MDAnalysis/transformations/translate.py | 55 +++-- package/MDAnalysis/transformations/wrap.py | 49 ++-- package/pyproject.toml | 1 + .../transformations/test_base.py | 23 +- .../transformations/test_boxdimensions.py | 93 +++++--- .../transformations/test_fit.py | 212 ++++++++++-------- .../transformations/test_nojump.py | 89 +++++--- .../transformations/test_positionaveraging.py | 120 +++++----- .../transformations/test_rotate.py | 194 +++++++++------- .../transformations/test_translate.py | 92 ++++---- .../transformations/test_wrap.py | 91 ++++---- testsuite/pyproject.toml | 1 + 18 files changed, 775 insertions(+), 542 deletions(-) diff --git a/package/MDAnalysis/transformations/base.py b/package/MDAnalysis/transformations/base.py index 59ad37e7fa6..ab0f6ea8990 100644 --- a/package/MDAnalysis/transformations/base.py +++ b/package/MDAnalysis/transformations/base.py @@ -104,8 +104,8 @@ def __init__(self, **kwargs): analysis approach. Default is ``True``. """ - self.max_threads = kwargs.pop('max_threads', None) - self.parallelizable = kwargs.pop('parallelizable', True) + self.max_threads = kwargs.pop("max_threads", None) + self.parallelizable = kwargs.pop("parallelizable", True) def __call__(self, ts): """The function that makes transformation can be called as a function diff --git a/package/MDAnalysis/transformations/boxdimensions.py b/package/MDAnalysis/transformations/boxdimensions.py index 0f5ebbd3227..c18cdc36a7a 100644 --- a/package/MDAnalysis/transformations/boxdimensions.py +++ b/package/MDAnalysis/transformations/boxdimensions.py @@ -34,6 +34,7 @@ from .base import TransformationBase + class set_dimensions(TransformationBase): """ Set simulation box dimensions. @@ -85,33 +86,31 @@ class set_dimensions(TransformationBase): Added the option to set varying box dimensions (i.e. an NPT trajectory). """ - def __init__(self, - dimensions, - max_threads=None, - parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + def __init__(self, dimensions, max_threads=None, parallelizable=True): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.dimensions = dimensions try: self.dimensions = np.asarray(self.dimensions, np.float32) except ValueError: errmsg = ( - f'{self.dimensions} cannot be converted into ' - 'np.float32 numpy.ndarray' + f"{self.dimensions} cannot be converted into " + "np.float32 numpy.ndarray" ) raise ValueError(errmsg) try: self.dimensions = self.dimensions.reshape(-1, 6) except ValueError: errmsg = ( - f'{self.dimensions} array does not have valid box ' - 'dimension shape.\nSimulation box dimensions are ' - 'given by an float array of shape (6, 0), (1, 6), ' - 'or (N, 6) where N is the number of frames in the ' - 'trajectory and the dimension vector(s) containing ' - '3 lengths and 3 angles: ' - '[a, b, c, alpha, beta, gamma]' + f"{self.dimensions} array does not have valid box " + "dimension shape.\nSimulation box dimensions are " + "given by an float array of shape (6, 0), (1, 6), " + "or (N, 6) where N is the number of frames in the " + "trajectory and the dimension vector(s) containing " + "3 lengths and 3 angles: " + "[a, b, c, alpha, beta, gamma]" ) raise ValueError(errmsg) diff --git a/package/MDAnalysis/transformations/fit.py b/package/MDAnalysis/transformations/fit.py index 2356201c54a..89336128cbf 100644 --- a/package/MDAnalysis/transformations/fit.py +++ b/package/MDAnalysis/transformations/fit.py @@ -90,10 +90,19 @@ class fit_translation(TransformationBase): The transformation was changed to inherit from the base class for limiting threads and checking if it can be used in parallel analysis. """ - def __init__(self, ag, reference, plane=None, weights=None, - max_threads=None, parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + + def __init__( + self, + ag, + reference, + plane=None, + weights=None, + max_threads=None, + parallelizable=True, + ): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.ag = ag self.reference = reference @@ -101,34 +110,37 @@ def __init__(self, ag, reference, plane=None, weights=None, self.weights = weights if self.plane is not None: - axes = {'yz': 0, 'xz': 1, 'xy': 2} + axes = {"yz": 0, "xz": 1, "xy": 2} try: self.plane = axes[self.plane] except (TypeError, KeyError): - raise ValueError(f'{self.plane} is not a valid plane') \ - from None + raise ValueError( + f"{self.plane} is not a valid plane" + ) from None try: if self.ag.atoms.n_residues != self.reference.atoms.n_residues: errmsg = ( - f"{self.ag} and {self.reference} have mismatched" - f"number of residues" + f"{self.ag} and {self.reference} have mismatched" + f"number of residues" ) raise ValueError(errmsg) except AttributeError: errmsg = ( - f"{self.ag} or {self.reference} is not valid" - f"Universe/AtomGroup" + f"{self.ag} or {self.reference} is not valid" + f"Universe/AtomGroup" ) raise AttributeError(errmsg) from None - self.ref, self.mobile = align.get_matching_atoms(self.reference.atoms, - self.ag.atoms) + self.ref, self.mobile = align.get_matching_atoms( + self.reference.atoms, self.ag.atoms + ) self.weights = align.get_weights(self.ref.atoms, weights=self.weights) self.ref_com = self.ref.center(self.weights) def _transform(self, ts): - mobile_com = np.asarray(self.mobile.atoms.center(self.weights), - np.float32) + mobile_com = np.asarray( + self.mobile.atoms.center(self.weights), np.float32 + ) vector = self.ref_com - mobile_com if self.plane is not None: vector[self.plane] = 0 @@ -197,10 +209,19 @@ class fit_rot_trans(TransformationBase): The transformation was changed to inherit from the base class for limiting threads and checking if it can be used in parallel analysis. """ - def __init__(self, ag, reference, plane=None, weights=None, - max_threads=1, parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + + def __init__( + self, + ag, + reference, + plane=None, + weights=None, + max_threads=1, + parallelizable=True, + ): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.ag = ag self.reference = reference @@ -208,12 +229,13 @@ def __init__(self, ag, reference, plane=None, weights=None, self.weights = weights if self.plane is not None: - axes = {'yz': 0, 'xz': 1, 'xy': 2} + axes = {"yz": 0, "xz": 1, "xy": 2} try: self.plane = axes[self.plane] except (TypeError, KeyError): - raise ValueError(f'{self.plane} is not a valid plane') \ - from None + raise ValueError( + f"{self.plane} is not a valid plane" + ) from None try: if self.ag.atoms.n_residues != self.reference.atoms.n_residues: errmsg = ( @@ -223,12 +245,13 @@ def __init__(self, ag, reference, plane=None, weights=None, raise ValueError(errmsg) except AttributeError: errmsg = ( - f"{self.ag} or {self.reference} is not valid " - f"Universe/AtomGroup" + f"{self.ag} or {self.reference} is not valid " + f"Universe/AtomGroup" ) raise AttributeError(errmsg) from None - self.ref, self.mobile = align.get_matching_atoms(self.reference.atoms, - self.ag.atoms) + self.ref, self.mobile = align.get_matching_atoms( + self.reference.atoms, self.ag.atoms + ) self.weights = align.get_weights(self.ref.atoms, weights=self.weights) self.ref_com = self.ref.center(self.weights) self.ref_coordinates = self.ref.atoms.positions - self.ref_com @@ -236,22 +259,23 @@ def __init__(self, ag, reference, plane=None, weights=None, def _transform(self, ts): mobile_com = self.mobile.atoms.center(self.weights) mobile_coordinates = self.mobile.atoms.positions - mobile_com - rotation, dump = align.rotation_matrix(mobile_coordinates, - self.ref_coordinates, - weights=self.weights) + rotation, dump = align.rotation_matrix( + mobile_coordinates, self.ref_coordinates, weights=self.weights + ) vector = self.ref_com if self.plane is not None: matrix = np.r_[rotation, np.zeros(3).reshape(1, 3)] matrix = np.c_[matrix, np.zeros(4)] - euler_angs = np.asarray(euler_from_matrix(matrix, axes='sxyz'), - np.float32) + euler_angs = np.asarray( + euler_from_matrix(matrix, axes="sxyz"), np.float32 + ) for i in range(0, euler_angs.size): - euler_angs[i] = (euler_angs[self.plane] if i == self.plane - else 0) - rotation = euler_matrix(euler_angs[0], - euler_angs[1], - euler_angs[2], - axes='sxyz')[:3, :3] + euler_angs[i] = ( + euler_angs[self.plane] if i == self.plane else 0 + ) + rotation = euler_matrix( + euler_angs[0], euler_angs[1], euler_angs[2], axes="sxyz" + )[:3, :3] vector[self.plane] = mobile_com[self.plane] ts.positions = ts.positions - mobile_com ts.positions = np.dot(ts.positions, rotation.T) diff --git a/package/MDAnalysis/transformations/nojump.py b/package/MDAnalysis/transformations/nojump.py index fd6dc7703e4..d4a54f6f8d3 100644 --- a/package/MDAnalysis/transformations/nojump.py +++ b/package/MDAnalysis/transformations/nojump.py @@ -50,7 +50,7 @@ class NoJump(TransformationBase): across periodic boundary edges. The algorithm used is based on :footcite:p:`Kulke2022`, equation B6 for non-orthogonal systems, so it is general to most applications where molecule trajectories should not "jump" from one side of a periodic box to another. - + Note that this transformation depends on a periodic box dimension being set for every frame in the trajectory, and that this box dimension can be transformed to an orthonormal unit cell. If not, an error is emitted. Since it is typical to transform all frames @@ -133,7 +133,8 @@ def _transform(self, ts): if ( self.check_c and self.older_frame != "A" - and (self.old_frame - self.older_frame) != (ts.frame - self.old_frame) + and (self.old_frame - self.older_frame) + != (ts.frame - self.old_frame) ): warnings.warn( "NoJump detected that the interval between frames is unequal." @@ -155,7 +156,9 @@ def _transform(self, ts): ) # Convert into reduced coordinate space fcurrent = ts.positions @ Linverse - fprev = self.prev # Previous unwrapped coordinates in reduced box coordinates. + fprev = ( + self.prev + ) # Previous unwrapped coordinates in reduced box coordinates. # Calculate the new positions in reduced coordinate space (Equation B6 from # 10.1021/acs.jctc.2c00327). As it turns out, the displacement term can # be moved inside the round function in this coordinate space, as the @@ -164,7 +167,9 @@ def _transform(self, ts): # Convert back into real space ts.positions = newpositions @ L # Set things we need to save for the next frame. - self.prev = newpositions # Note that this is in reduced coordinate space. + self.prev = ( + newpositions # Note that this is in reduced coordinate space. + ) self.older_frame = self.old_frame self.old_frame = ts.frame diff --git a/package/MDAnalysis/transformations/positionaveraging.py b/package/MDAnalysis/transformations/positionaveraging.py index 13145b69c44..d091dd9bbf8 100644 --- a/package/MDAnalysis/transformations/positionaveraging.py +++ b/package/MDAnalysis/transformations/positionaveraging.py @@ -42,9 +42,9 @@ class PositionAverager(TransformationBase): """ Averages the coordinates of a given timestep so that the coordinates of the AtomGroup correspond to the average positions of the N previous - frames. + frames. For frames < N, the average of the frames iterated up to that point will - be returned. + be returned. Example ------- @@ -59,7 +59,7 @@ class PositionAverager(TransformationBase): N=3 transformation = PositionAverager(N, check_reset=True) - u.trajectory.add_transformations(transformation) + u.trajectory.add_transformations(transformation) for ts in u.trajectory: print(ts.positions) @@ -72,18 +72,18 @@ class PositionAverager(TransformationBase): manually reset before restarting an iteration. In this case, ``ts.positions`` will return the average coordinates of the last N iterated frames, despite them not being sequential - (``frames = [0, 7, 1, 6]``). + (``frames = [0, 7, 1, 6]``). .. code-block:: python - + N=3 transformation = PositionAverager(N, check_reset=False) u.trajectory.add_transformations(transformation) - frames = [0, 7, 1, 6] + frames = [0, 7, 1, 6] transformation.resetarrays() for ts in u.trajectory[frames]: print(ts.positions) - + If ``check_reset=True``, the ``PositionAverager`` would have automatically reset after detecting a non sequential iteration (i.e. when iterating from frame 7 to frame 1 or when resetting the iterator from frame 6 back to @@ -98,14 +98,14 @@ class PositionAverager(TransformationBase): these examples corresponds to ``N=3``. .. code-block:: python - + N=3 transformation = PositionAverager(N, check_reset=True) - u.trajectory.add_transformations(transformation) + u.trajectory.add_transformations(transformation) for ts in u.trajectory: if transformation.current_avg == transformation.avg_frames: print(ts.positions) - + In the case of ``N=3``, as the average is calculated with the frames iterated up to the current iteration, the first frame returned will not be averaged. During the first iteration no other frames are stored in @@ -115,21 +115,21 @@ class PositionAverager(TransformationBase): following iterations will ``ts.positions`` start returning the average of the last 3 frames and thus ``transformation.current_avg = 3`` These initial frames are typically not desired during analysis, but one can - easily avoid them, as seen in the previous example with + easily avoid them, as seen in the previous example with ``if transformation.current_avg == transformation.avg_frames:`` or by - simply removing the first ``avg_frames-1`` frames from the analysis. + simply removing the first ``avg_frames-1`` frames from the analysis. Parameters ---------- avg_frames: int - Determines the number of frames to be used for the position averaging. + Determines the number of frames to be used for the position averaging. check_reset: bool, optional If ``True``, position averaging will be reset and a warning raised when the trajectory iteration direction changes. If ``False``, position averaging will not reset, regardless of the iteration. - + Returns ------- @@ -141,11 +141,16 @@ class PositionAverager(TransformationBase): limiting threads and checking if it can be used in parallel analysis. """ - def __init__(self, avg_frames, check_reset=True, - max_threads=None, - parallelizable=False): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + def __init__( + self, + avg_frames, + check_reset=True, + max_threads=None, + parallelizable=False, + ): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.avg_frames = avg_frames self.check_reset = check_reset self.current_avg = 0 @@ -164,8 +169,11 @@ def rollposx(self, ts): try: self.coord_array.size except AttributeError: - size = (ts.positions.shape[0], ts.positions.shape[1], - self.avg_frames) + size = ( + ts.positions.shape[0], + ts.positions.shape[1], + self.avg_frames, + ) self.coord_array = np.empty(size) self.coord_array = np.roll(self.coord_array, 1, axis=2) @@ -175,9 +183,11 @@ def _transform(self, ts): # calling the same timestep will not add new data to coord_array # This can prevent from getting different values when # call `u.trajectory[i]` multiple times. - if (ts.frame == self.current_frame - and hasattr(self, 'coord_array') - and not np.isnan(self.idx_array).all()): + if ( + ts.frame == self.current_frame + and hasattr(self, "coord_array") + and not np.isnan(self.idx_array).all() + ): test = ~np.isnan(self.idx_array) ts.positions = np.mean(self.coord_array[..., test], axis=2) return ts @@ -190,9 +200,11 @@ def _transform(self, ts): if self.check_reset: sign = np.sign(np.diff(self.idx_array[test])) if not (np.all(sign == 1) or np.all(sign == -1)): - warnings.warn('Cannot average position for non sequential' - 'iterations. Averager will be reset.', - Warning) + warnings.warn( + "Cannot average position for non sequential" + "iterations. Averager will be reset.", + Warning, + ) self.resetarrays() return self(ts) diff --git a/package/MDAnalysis/transformations/rotate.py b/package/MDAnalysis/transformations/rotate.py index ddb730f0694..4d8fa71d0b1 100644 --- a/package/MDAnalysis/transformations/rotate.py +++ b/package/MDAnalysis/transformations/rotate.py @@ -41,7 +41,7 @@ class rotateby(TransformationBase): - ''' + """ Rotates the trajectory by a given angle on a given axis. The axis is defined by the user, combining the direction vector and a point. This point can be the center of geometry or the center of mass of a user defined AtomGroup, or an array defining @@ -86,7 +86,7 @@ class rotateby(TransformationBase): rotation angle in degrees direction: array-like vector that will define the direction of a custom axis of rotation from the - provided point. Expected shapes are (3, ) or (1, 3). + provided point. Expected shapes are (3, ) or (1, 3). ag: AtomGroup, optional use the weighted center of an AtomGroup as the point from where the rotation axis will be defined. If no AtomGroup is given, the `point` argument becomes mandatory @@ -98,12 +98,12 @@ class rotateby(TransformationBase): define the weights of the atoms when calculating the center of the AtomGroup. With ``"mass"`` uses masses as weights; with ``None`` weigh each atom equally. If a float array of the same length as `ag` is provided, use each element of - the `array_like` as a weight for the corresponding atom in `ag`. Default is + the `array_like` as a weight for the corresponding atom in `ag`. Default is None. wrap: bool, optional If `True`, all the atoms from the given AtomGroup will be moved to the unit cell before calculating the center of mass or geometry. Default is `False`, no changes - to the atom coordinates are done before calculating the center of the AtomGroup. + to the atom coordinates are done before calculating the center of the AtomGroup. Returns ------- @@ -111,7 +111,7 @@ class rotateby(TransformationBase): Warning ------- - Wrapping/unwrapping the trajectory or performing PBC corrections may not be possible + Wrapping/unwrapping the trajectory or performing PBC corrections may not be possible after rotating the trajectory. @@ -121,18 +121,22 @@ class rotateby(TransformationBase): .. versionchanged:: 2.0.0 The transformation was changed to inherit from the base class for limiting threads and checking if it can be used in parallel analysis. - ''' - def __init__(self, - angle, - direction, - point=None, - ag=None, - weights=None, - wrap=False, - max_threads=1, - parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + """ + + def __init__( + self, + angle, + direction, + point=None, + ag=None, + weights=None, + wrap=False, + max_threads=1, + parallelizable=True, + ): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.angle = angle self.direction = direction @@ -144,38 +148,47 @@ def __init__(self, self.angle = np.deg2rad(self.angle) try: self.direction = np.asarray(self.direction, np.float32) - if self.direction.shape != (3, ) and \ - self.direction.shape != (1, 3): - raise ValueError('{} is not a valid direction' - .format(self.direction)) - self.direction = self.direction.reshape(3, ) + if self.direction.shape != (3,) and self.direction.shape != (1, 3): + raise ValueError( + "{} is not a valid direction".format(self.direction) + ) + self.direction = self.direction.reshape( + 3, + ) except ValueError: - raise ValueError(f'{self.direction} is not a valid direction') \ - from None + raise ValueError( + f"{self.direction} is not a valid direction" + ) from None if self.point is not None: self.point = np.asarray(self.point, np.float32) - if self.point.shape != (3, ) and self.point.shape != (1, 3): - raise ValueError('{} is not a valid point'.format(self.point)) - self.point = self.point.reshape(3, ) + if self.point.shape != (3,) and self.point.shape != (1, 3): + raise ValueError("{} is not a valid point".format(self.point)) + self.point = self.point.reshape( + 3, + ) elif self.ag: try: self.atoms = self.ag.atoms except AttributeError: - raise ValueError(f'{self.ag} is not an AtomGroup object') \ - from None + raise ValueError( + f"{self.ag} is not an AtomGroup object" + ) from None else: try: - self.weights = get_weights(self.atoms, - weights=self.weights) + self.weights = get_weights( + self.atoms, weights=self.weights + ) except (ValueError, TypeError): - errmsg = ("weights must be {'mass', None} or an iterable " - "of the same size as the atomgroup.") + errmsg = ( + "weights must be {'mass', None} or an iterable " + "of the same size as the atomgroup." + ) raise TypeError(errmsg) from None - self.center_method = partial(self.atoms.center, - self.weights, - wrap=self.wrap) + self.center_method = partial( + self.atoms.center, self.weights, wrap=self.wrap + ) else: - raise ValueError('A point or an AtomGroup must be specified') + raise ValueError("A point or an AtomGroup must be specified") def _transform(self, ts): if self.point is None: diff --git a/package/MDAnalysis/transformations/translate.py b/package/MDAnalysis/transformations/translate.py index 6edf5d4692a..c28fffd404f 100644 --- a/package/MDAnalysis/transformations/translate.py +++ b/package/MDAnalysis/transformations/translate.py @@ -70,10 +70,11 @@ class translate(TransformationBase): The transformation was changed to inherit from the base class for limiting threads and checking if it can be used in parallel analysis. """ - def __init__(self, vector, - max_threads=None, parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + + def __init__(self, vector, max_threads=None, parallelizable=True): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.vector = vector @@ -130,10 +131,19 @@ class center_in_box(TransformationBase): The transformation was changed to inherit from the base class for limiting threads and checking if it can be used in parallel analysis. """ - def __init__(self, ag, center='geometry', point=None, wrap=False, - max_threads=None, parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + + def __init__( + self, + ag, + center="geometry", + point=None, + wrap=False, + max_threads=None, + parallelizable=True, + ): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.ag = ag self.center = center @@ -143,24 +153,27 @@ def __init__(self, ag, center='geometry', point=None, wrap=False, pbc_arg = self.wrap if self.point: self.point = np.asarray(self.point, np.float32) - if self.point.shape != (3, ) and self.point.shape != (1, 3): - raise ValueError('{} is not a valid point'.format(self.point)) + if self.point.shape != (3,) and self.point.shape != (1, 3): + raise ValueError("{} is not a valid point".format(self.point)) try: - if self.center == 'geometry': - self.center_method = partial(self.ag.center_of_geometry, - wrap=pbc_arg) - elif self.center == 'mass': - self.center_method = partial(self.ag.center_of_mass, - wrap=pbc_arg) + if self.center == "geometry": + self.center_method = partial( + self.ag.center_of_geometry, wrap=pbc_arg + ) + elif self.center == "mass": + self.center_method = partial( + self.ag.center_of_mass, wrap=pbc_arg + ) else: - raise ValueError(f'{self.center} is valid for center') + raise ValueError(f"{self.center} is valid for center") except AttributeError: - if self.center == 'mass': - errmsg = f'{self.ag} is not an AtomGroup object with masses' + if self.center == "mass": + errmsg = f"{self.ag} is not an AtomGroup object with masses" raise AttributeError(errmsg) from None else: - raise ValueError(f'{self.ag} is not an AtomGroup object') \ - from None + raise ValueError( + f"{self.ag} is not an AtomGroup object" + ) from None def _transform(self, ts): if self.point is None: diff --git a/package/MDAnalysis/transformations/wrap.py b/package/MDAnalysis/transformations/wrap.py index f077f5edc19..f8c1d8dbaeb 100644 --- a/package/MDAnalysis/transformations/wrap.py +++ b/package/MDAnalysis/transformations/wrap.py @@ -42,7 +42,7 @@ class wrap(TransformationBase): """ Shift the contents of a given AtomGroup back into the unit cell. :: - + +-----------+ +-----------+ | | | | | 3 | 6 | 6 3 | @@ -52,24 +52,24 @@ class wrap(TransformationBase): | 4 | 7 | 7 4 | | | | | +-----------+ +-----------+ - + Example ------- - + .. code-block:: python - - ag = u.atoms + + ag = u.atoms transform = mda.transformations.wrap(ag) u.trajectory.add_transformations(transform) - + Parameters ---------- - + ag: Atomgroup Atomgroup to be wrapped in the unit cell compound : {'atoms', 'group', 'residues', 'segments', 'fragments'}, optional The group which will be kept together through the shifting process. - + Notes ----- When specifying a `compound`, the translation is calculated based on @@ -77,7 +77,7 @@ class wrap(TransformationBase): within this compound, meaning it will not be broken by the shift. This might however mean that not all atoms from the compound are inside the unit cell, but rather the center of the compound is. - + Returns ------- MDAnalysis.coordinates.timestep.Timestep @@ -90,10 +90,13 @@ class wrap(TransformationBase): The transformation was changed to inherit from the base class for limiting threads and checking if it can be used in parallel analysis. """ - def __init__(self, ag, compound='atoms', - max_threads=None, parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + + def __init__( + self, ag, compound="atoms", max_threads=None, parallelizable=True + ): + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.ag = ag self.compound = compound @@ -113,7 +116,7 @@ class unwrap(TransformationBase): unit cell, causing breaks mid molecule, with the molecule then appearing on either side of the unit cell. This is problematic for operations such as calculating the center of mass of the molecule. :: - + +-----------+ +-----------+ | | | | | 6 3 | | 3 | 6 @@ -123,22 +126,22 @@ class unwrap(TransformationBase): | 7 4 | | 4 | 7 | | | | +-----------+ +-----------+ - + Example ------- - + .. code-block:: python - - ag = u.atoms + + ag = u.atoms transform = mda.transformations.unwrap(ag) u.trajectory.add_transformations(transform) - + Parameters ---------- atomgroup : AtomGroup The :class:`MDAnalysis.core.groups.AtomGroup` to work with. The positions of this are modified in place. - + Returns ------- MDAnalysis.coordinates.timestep.Timestep @@ -151,9 +154,11 @@ class unwrap(TransformationBase): The transformation was changed to inherit from the base class for limiting threads and checking if it can be used in parallel analysis. """ + def __init__(self, ag, max_threads=None, parallelizable=True): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) self.ag = ag diff --git a/package/pyproject.toml b/package/pyproject.toml index 97217c60e5f..72a372ccef2 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -133,6 +133,7 @@ tables\.py | setup\.py | MDAnalysis/auxiliary/.*\.py | visualization/.*\.py +| MDAnalysis/transformations/.*\.py ) ''' extend-exclude = '__pycache__' diff --git a/testsuite/MDAnalysisTests/transformations/test_base.py b/testsuite/MDAnalysisTests/transformations/test_base.py index 5aa170f5604..492c2825b44 100644 --- a/testsuite/MDAnalysisTests/transformations/test_base.py +++ b/testsuite/MDAnalysisTests/transformations/test_base.py @@ -1,4 +1,3 @@ - # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 # @@ -32,6 +31,7 @@ class DefaultTransformation(TransformationBase): """Default values for max_threads and parallelizable""" + def __init__(self): super().__init__() @@ -43,15 +43,18 @@ def _transform(self, ts): class NoTransform_Transformation(TransformationBase): """Default values for max_threads and parallelizable""" + def __init__(self): super().__init__() class CustomTransformation(TransformationBase): """Custom value for max_threads and parallelizable""" + def __init__(self, max_threads=1, parallelizable=False): - super().__init__(max_threads=max_threads, - parallelizable=parallelizable) + super().__init__( + max_threads=max_threads, parallelizable=parallelizable + ) def _transform(self, ts): self.runtime_info = threadpool_info() @@ -59,7 +62,7 @@ def _transform(self, ts): return ts -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return mda.Universe(PSF, DCD) @@ -89,16 +92,18 @@ def test_setting_thread_limit_value(): def test_thread_limit_apply(u): default_thread_info = threadpool_info() - default_num_thread_limit_list = [thread_info['num_threads'] - for thread_info in default_thread_info] + default_num_thread_limit_list = [ + thread_info["num_threads"] for thread_info in default_thread_info + ] new_trans = CustomTransformation(max_threads=2) _ = new_trans(u.trajectory.ts) for thread_info in new_trans.runtime_info: - assert thread_info['num_threads'] == 2 + assert thread_info["num_threads"] == 2 # test the thread limit is only applied locally. new_thread_info = threadpool_info() - new_num_thread_limit_list = [thread_info['num_threads'] - for thread_info in new_thread_info] + new_num_thread_limit_list = [ + thread_info["num_threads"] for thread_info in new_thread_info + ] assert_equal(default_num_thread_limit_list, new_num_thread_limit_list) diff --git a/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py b/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py index f8bb30a7f2c..a1d88b78405 100644 --- a/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py +++ b/testsuite/MDAnalysisTests/transformations/test_boxdimensions.py @@ -51,46 +51,56 @@ def variable_boxdimensions_universe(): def test_boxdimensions_dims(boxdimensions_universe): new_dims = np.float32([2, 2, 2, 90, 90, 90]) set_dimensions(new_dims)(boxdimensions_universe.trajectory.ts) - assert_array_almost_equal(boxdimensions_universe.dimensions, - new_dims, decimal=6) - - -@pytest.mark.parametrize('dim_vector_shapes', ( - [1, 1, 1, 90, 90], - [1, 1, 1, 1, 90, 90, 90], - np.array([[1], [1], [90], [90], [90]]), - np.array([1, 1, 1, 90, 90]), - np.array([1, 1, 1, 1, 90, 90, 90]), - [1, 1, 1, 90, 90], - 111909090) + assert_array_almost_equal( + boxdimensions_universe.dimensions, new_dims, decimal=6 ) + + +@pytest.mark.parametrize( + "dim_vector_shapes", + ( + [1, 1, 1, 90, 90], + [1, 1, 1, 1, 90, 90, 90], + np.array([[1], [1], [90], [90], [90]]), + np.array([1, 1, 1, 90, 90]), + np.array([1, 1, 1, 1, 90, 90, 90]), + [1, 1, 1, 90, 90], + 111909090, + ), +) def test_dimensions_vector(boxdimensions_universe, dim_vector_shapes): # wrong box dimension vector shape ts = boxdimensions_universe.trajectory.ts - with pytest.raises(ValueError, match='valid box dimension shape'): + with pytest.raises(ValueError, match="valid box dimension shape"): set_dimensions(dim_vector_shapes)(ts) -@pytest.mark.parametrize('dim_vector_forms_dtypes', ( - ['a', 'b', 'c', 'd', 'e', 'f'], - np.array(['a', 'b', 'c', 'd', 'e', 'f']), - 'abcd') - ) -def test_dimensions_vector_asarray(boxdimensions_universe, - dim_vector_forms_dtypes): +@pytest.mark.parametrize( + "dim_vector_forms_dtypes", + ( + ["a", "b", "c", "d", "e", "f"], + np.array(["a", "b", "c", "d", "e", "f"]), + "abcd", + ), +) +def test_dimensions_vector_asarray( + boxdimensions_universe, dim_vector_forms_dtypes +): # box dimension input type not convertible into array ts = boxdimensions_universe.trajectory.ts - with pytest.raises(ValueError, match='cannot be converted'): + with pytest.raises(ValueError, match="cannot be converted"): set_dimensions(dim_vector_forms_dtypes)(ts) + def test_dimensions_transformations_api(boxdimensions_universe): # test if transformation works with on-the-fly transformations API new_dims = np.float32([2, 2, 2, 90, 90, 90]) transform = set_dimensions(new_dims) boxdimensions_universe.trajectory.add_transformations(transform) for ts in boxdimensions_universe.trajectory: - assert_array_almost_equal(boxdimensions_universe.dimensions, - new_dims, decimal=6) + assert_array_almost_equal( + boxdimensions_universe.dimensions, new_dims, decimal=6 + ) def test_varying_dimensions_transformations_api( @@ -100,16 +110,21 @@ def test_varying_dimensions_transformations_api( Test if transformation works with on-the-fly transformations API when we have varying dimensions. """ - new_dims = np.float32([ - [2, 2, 2, 90, 90, 90], - [4, 4, 4, 90, 90, 90], - [8, 8, 8, 90, 90, 90], - ]) + new_dims = np.float32( + [ + [2, 2, 2, 90, 90, 90], + [4, 4, 4, 90, 90, 90], + [8, 8, 8, 90, 90, 90], + ] + ) transform = set_dimensions(new_dims) variable_boxdimensions_universe.trajectory.add_transformations(transform) for ts in variable_boxdimensions_universe.trajectory: - assert_array_almost_equal(variable_boxdimensions_universe.dimensions, - new_dims[ts.frame], decimal=6) + assert_array_almost_equal( + variable_boxdimensions_universe.dimensions, + new_dims[ts.frame], + decimal=6, + ) def test_varying_dimensions_no_data( @@ -120,10 +135,16 @@ def test_varying_dimensions_no_data( in a trajectory. """ # trjactory has three frames - new_dims = np.float32([ - [2, 2, 2, 90, 90, 90], - [4, 4, 4, 90, 90, 90], - ]) + new_dims = np.float32( + [ + [2, 2, 2, 90, 90, 90], + [4, 4, 4, 90, 90, 90], + ] + ) transform = set_dimensions(new_dims) - with pytest.raises(ValueError, match="Dimensions array has no data for frame 2"): - variable_boxdimensions_universe.trajectory.add_transformations(transform) + with pytest.raises( + ValueError, match="Dimensions array has no data for frame 2" + ): + variable_boxdimensions_universe.trajectory.add_transformations( + transform + ) diff --git a/testsuite/MDAnalysisTests/transformations/test_fit.py b/testsuite/MDAnalysisTests/transformations/test_fit.py index 9c44f88e0d1..ecd4a87dd7a 100644 --- a/testsuite/MDAnalysisTests/transformations/test_fit.py +++ b/testsuite/MDAnalysisTests/transformations/test_fit.py @@ -31,61 +31,62 @@ @pytest.fixture() def fit_universe(): # make a test universe - test = make_Universe(('masses', ), trajectory=True) - ref = make_Universe(('masses', ), trajectory=True) + test = make_Universe(("masses",), trajectory=True) + ref = make_Universe(("masses",), trajectory=True) ref.atoms.positions += np.asarray([10, 10, 10], np.float32) return test, ref -@pytest.mark.parametrize('universe', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]]), - 'thisisnotanag', - 1) +@pytest.mark.parametrize( + "universe", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + "thisisnotanag", + 1, + ), ) def test_fit_translation_bad_ag(fit_universe, universe): ts = fit_universe[0].trajectory.ts test_u = fit_universe[0] - ref_u = fit_universe[1] + ref_u = fit_universe[1] # what happens if something other than an AtomGroup or Universe is given? with pytest.raises(AttributeError): fit_translation(universe, ref_u)(ts) -@pytest.mark.parametrize('weights', ( - " ", - "totallynotmasses", - 123456789, - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])) +@pytest.mark.parametrize( + "weights", + ( + " ", + "totallynotmasses", + 123456789, + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + ), ) def test_fit_translation_bad_weights(fit_universe, weights): ts = fit_universe[0].trajectory.ts test_u = fit_universe[0] - ref_u = fit_universe[1] + ref_u = fit_universe[1] # what happens if a bad string for center is given? with pytest.raises(ValueError): fit_translation(test_u, ref_u, weights=weights)(ts) -@pytest.mark.parametrize('plane', ( - 1, - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - "xyz", - "notaplane") +@pytest.mark.parametrize( + "plane", (1, [0, 1], [0, 1, 2, 3, 4], np.array([0, 1]), "xyz", "notaplane") ) def test_fit_translation_bad_plane(fit_universe, plane): ts = fit_universe[0].trajectory.ts test_u = fit_universe[0] - ref_u = fit_universe[1] + ref_u = fit_universe[1] # what happens if a bad string for center is given? with pytest.raises(ValueError): fit_translation(test_u, ref_u, plane=plane)(ts) @@ -95,11 +96,11 @@ def test_fit_translation_no_masses(fit_universe): ts = fit_universe[0].trajectory.ts test_u = fit_universe[0] # create a universe without masses - ref_u = make_Universe() + ref_u = make_Universe() # what happens Universe without masses is given? with pytest.raises(TypeError) as exc: fit_translation(test_u, ref_u, weights="mass")(ts) - assert 'atoms.masses is missing' in str(exc.value) + assert "atoms.masses is missing" in str(exc.value) def test_fit_translation_no_options(fit_universe): @@ -107,31 +108,37 @@ def test_fit_translation_no_options(fit_universe): ref_u = fit_universe[1] fit_translation(test_u, ref_u)(test_u.trajectory.ts) # what happens when no options are passed? - assert_array_almost_equal(test_u.trajectory.ts.positions, ref_u.trajectory.ts.positions, decimal=6) + assert_array_almost_equal( + test_u.trajectory.ts.positions, + ref_u.trajectory.ts.positions, + decimal=6, + ) + def test_fit_translation_residue_mismatch(fit_universe): test_u = fit_universe[0] ref_u = fit_universe[1].residues[:-1].atoms - with pytest.raises(ValueError, match='number of residues'): + with pytest.raises(ValueError, match="number of residues"): fit_translation(test_u, ref_u)(test_u.trajectory.ts) + def test_fit_translation_com(fit_universe): test_u = fit_universe[0] ref_u = fit_universe[1] fit_translation(test_u, ref_u, weights="mass")(test_u.trajectory.ts) # what happens when the center o mass is used? - assert_array_almost_equal(test_u.trajectory.ts.positions, ref_u.trajectory.ts.positions, decimal=6) + assert_array_almost_equal( + test_u.trajectory.ts.positions, + ref_u.trajectory.ts.positions, + decimal=6, + ) -@pytest.mark.parametrize('plane', ( - "yz", - "xz", - "xy") -) +@pytest.mark.parametrize("plane", ("yz", "xz", "xy")) def test_fit_translation_plane(fit_universe, plane): test_u = fit_universe[0] ref_u = fit_universe[1] - axes = {'yz' : 0, 'xz' : 1, 'xy' : 2} + axes = {"yz": 0, "xz": 1, "xy": 2} idx = axes[plane] # translate the test universe on the plane coordinates only fit_translation(test_u, ref_u, plane=plane)(test_u.trajectory.ts) @@ -139,18 +146,24 @@ def test_fit_translation_plane(fit_universe, plane): shiftz = np.asanyarray([0, 0, 0], np.float32) shiftz[idx] = -10 ref_coordinates = ref_u.trajectory.ts.positions + shiftz - assert_array_almost_equal(test_u.trajectory.ts.positions, ref_coordinates, decimal=6) + assert_array_almost_equal( + test_u.trajectory.ts.positions, ref_coordinates, decimal=6 + ) def test_fit_translation_all_options(fit_universe): test_u = fit_universe[0] ref_u = fit_universe[1] # translate the test universe on the x and y coordinates only - fit_translation(test_u, ref_u, plane="xy", weights="mass")(test_u.trajectory.ts) + fit_translation(test_u, ref_u, plane="xy", weights="mass")( + test_u.trajectory.ts + ) # the reference is 10 angstrom in the z coordinate above the test universe shiftz = np.asanyarray([0, 0, -10], np.float32) ref_coordinates = ref_u.trajectory.ts.positions + shiftz - assert_array_almost_equal(test_u.trajectory.ts.positions, ref_coordinates, decimal=6) + assert_array_almost_equal( + test_u.trajectory.ts.positions, ref_coordinates, decimal=6 + ) def test_fit_translation_transformations_api(fit_universe): @@ -158,42 +171,52 @@ def test_fit_translation_transformations_api(fit_universe): ref_u = fit_universe[1] transform = fit_translation(test_u, ref_u) test_u.trajectory.add_transformations(transform) - assert_array_almost_equal(test_u.trajectory.ts.positions, ref_u.trajectory.ts.positions, decimal=6) - - -@pytest.mark.parametrize('universe', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]]), - 'thisisnotanag', - 1) + assert_array_almost_equal( + test_u.trajectory.ts.positions, + ref_u.trajectory.ts.positions, + decimal=6, + ) + + +@pytest.mark.parametrize( + "universe", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + "thisisnotanag", + 1, + ), ) def test_fit_rot_trans_bad_universe(fit_universe, universe): test_u = fit_universe[0] - ref_u= universe + ref_u = universe with pytest.raises(AttributeError): fit_rot_trans(test_u, ref_u)(test_u.trajectory.ts) def test_fit_rot_trans_shorter_universe(fit_universe): ref_u = fit_universe[1] - bad_u =fit_universe[0].atoms[0:5] - test_u= bad_u + bad_u = fit_universe[0].atoms[0:5] + test_u = bad_u with pytest.raises(ValueError): fit_rot_trans(test_u, ref_u)(test_u.trajectory.ts) -@pytest.mark.parametrize('weights', ( - " ", - "totallynotmasses", - 123456789, - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])) +@pytest.mark.parametrize( + "weights", + ( + " ", + "totallynotmasses", + 123456789, + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + ), ) def test_fit_rot_trans_bad_weights(fit_universe, weights): test_u = fit_universe[0] @@ -203,15 +226,18 @@ def test_fit_rot_trans_bad_weights(fit_universe, weights): fit_rot_trans(test_u, ref_u, weights=bad_weights)(test_u.trajectory.ts) -@pytest.mark.parametrize('plane', ( - " ", - "totallynotaplane", - "xyz", - 123456789, - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])) +@pytest.mark.parametrize( + "plane", + ( + " ", + "totallynotaplane", + "xyz", + 123456789, + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + ), ) def test_fit_rot_trans_bad_plane(fit_universe, plane): test_u = fit_universe[0] @@ -225,18 +251,18 @@ def test_fit_rot_trans_no_options(fit_universe): ref_u = fit_universe[1] ref_com = ref_u.atoms.center(None) ref_u.trajectory.ts.positions -= ref_com - R = rotation_matrix(np.pi/3, [1,0,0])[:3,:3] + R = rotation_matrix(np.pi / 3, [1, 0, 0])[:3, :3] ref_u.trajectory.ts.positions = np.dot(ref_u.trajectory.ts.positions, R) ref_u.trajectory.ts.positions += ref_com fit_rot_trans(test_u, ref_u)(test_u.trajectory.ts) - assert_array_almost_equal(test_u.trajectory.ts.positions, ref_u.trajectory.ts.positions, decimal=3) + assert_array_almost_equal( + test_u.trajectory.ts.positions, + ref_u.trajectory.ts.positions, + decimal=3, + ) -@pytest.mark.parametrize('plane', ( - "yz", - "xz", - "xy") -) +@pytest.mark.parametrize("plane", ("yz", "xz", "xy")) def test_fit_rot_trans_plane(fit_universe, plane): # the reference is rotated in the x axis so removing the translations and rotations # in the yz plane should return the same as the fitting without specifying a plane @@ -244,17 +270,21 @@ def test_fit_rot_trans_plane(fit_universe, plane): ref_u = fit_universe[1] ref_com = ref_u.atoms.center(None) mobile_com = test_u.atoms.center(None) - axes = {'yz' : 0, 'xz' : 1, 'xy' : 2} + axes = {"yz": 0, "xz": 1, "xy": 2} idx = axes[plane] - rotaxis = np.asarray([0,0,0]) - rotaxis[idx]=1 + rotaxis = np.asarray([0, 0, 0]) + rotaxis[idx] = 1 ref_u.trajectory.ts.positions -= ref_com - R = rotation_matrix(np.pi/3, rotaxis)[:3,:3] + R = rotation_matrix(np.pi / 3, rotaxis)[:3, :3] ref_u.trajectory.ts.positions = np.dot(ref_u.trajectory.ts.positions, R) ref_com[idx] = mobile_com[idx] ref_u.trajectory.ts.positions += ref_com fit_rot_trans(test_u, ref_u, plane=plane)(test_u.trajectory.ts) - assert_array_almost_equal(test_u.trajectory.ts.positions[:,idx], ref_u.trajectory.ts.positions[:,idx], decimal=3) + assert_array_almost_equal( + test_u.trajectory.ts.positions[:, idx], + ref_u.trajectory.ts.positions[:, idx], + decimal=3, + ) def test_fit_rot_trans_transformations_api(fit_universe): @@ -262,9 +292,13 @@ def test_fit_rot_trans_transformations_api(fit_universe): ref_u = fit_universe[1] ref_com = ref_u.atoms.center(None) ref_u.trajectory.ts.positions -= ref_com - R = rotation_matrix(np.pi/3, [1,0,0])[:3,:3] + R = rotation_matrix(np.pi / 3, [1, 0, 0])[:3, :3] ref_u.trajectory.ts.positions = np.dot(ref_u.trajectory.ts.positions, R) ref_u.trajectory.ts.positions += ref_com transform = fit_rot_trans(test_u, ref_u) test_u.trajectory.add_transformations(transform) - assert_array_almost_equal(test_u.trajectory.ts.positions, ref_u.trajectory.ts.positions, decimal=3) + assert_array_almost_equal( + test_u.trajectory.ts.positions, + ref_u.trajectory.ts.positions, + decimal=3, + ) diff --git a/testsuite/MDAnalysisTests/transformations/test_nojump.py b/testsuite/MDAnalysisTests/transformations/test_nojump.py index 6bbabe370f2..f295ec33a7b 100644 --- a/testsuite/MDAnalysisTests/transformations/test_nojump.py +++ b/testsuite/MDAnalysisTests/transformations/test_nojump.py @@ -9,9 +9,9 @@ @pytest.fixture() def nojump_universes_fromfile(): - ''' + """ Create the universe objects for the tests. - ''' + """ u = mda.Universe(data.PSF_TRICLINIC, data.DCD_TRICLINIC) transformation = NoJump() u.trajectory.add_transformations(transformation) @@ -113,18 +113,24 @@ def nojump_universe_npt_2nd_frame_from_file(tmp_path_factory): coordinates[2] = [2.5, 50.0, 50.0] coordinates[3] = [2.5, 50.0, 50.0] u.load_new(coordinates, order="fac") - dim = np.asarray([ - [100, 100, 100, 90, 90, 90], - [95, 100, 100, 90, 90, 90], # Box shrinks by 5 in the x-dimension - [95, 100, 100, 90, 90, 90], - [95, 100, 100, 90, 90, 90], - ]) + dim = np.asarray( + [ + [100, 100, 100, 90, 90, 90], + [95, 100, 100, 90, 90, 90], # Box shrinks by 5 in the x-dimension + [95, 100, 100, 90, 90, 90], + [95, 100, 100, 90, 90, 90], + ] + ) workflow = [ mda.transformations.boxdimensions.set_dimensions(dim), ] u.trajectory.add_transformations(*workflow) - tmp_pdb = (tmp_path_factory.getbasetemp() / "nojump_npt_2nd_frame.pdb").as_posix() - tmp_xtc = (tmp_path_factory.getbasetemp() / "nojump_npt_2nd_frame.xtc").as_posix() + tmp_pdb = ( + tmp_path_factory.getbasetemp() / "nojump_npt_2nd_frame.pdb" + ).as_posix() + tmp_xtc = ( + tmp_path_factory.getbasetemp() / "nojump_npt_2nd_frame.xtc" + ).as_posix() u.atoms.write(tmp_pdb) with mda.Writer(tmp_xtc) as f: for ts in u.trajectory: @@ -139,7 +145,10 @@ def test_nojump_orthogonal_fwd(nojump_universe): """ u = nojump_universe dim = np.asarray([1, 1, 1, 90, 90, 90], np.float32) - workflow = [mda.transformations.boxdimensions.set_dimensions(dim), NoJump()] + workflow = [ + mda.transformations.boxdimensions.set_dimensions(dim), + NoJump(), + ] u.trajectory.add_transformations(*workflow) transformed_coordinates = u.trajectory.timeseries()[0] # Step is 1 unit every 3 steps. After 99 steps from the origin, @@ -160,7 +169,10 @@ def test_nojump_nonorthogonal_fwd(nojump_universe): # [0. 1. 0. ] # [0.5 0. 0.8660254]] dim = np.asarray([1, 1, 1, 90, 60, 90], np.float32) - workflow = [mda.transformations.boxdimensions.set_dimensions(dim), NoJump()] + workflow = [ + mda.transformations.boxdimensions.set_dimensions(dim), + NoJump(), + ] u.trajectory.add_transformations(*workflow) transformed_coordinates = u.trajectory.timeseries()[0] # After the transformation, you should end up in a repeating pattern, since you are @@ -173,13 +185,15 @@ def test_nojump_nonorthogonal_fwd(nojump_universe): ) assert_allclose( transformed_coordinates[1::3], - np.outer(np.arange(32.5), np.array([0.5, 1, np.sqrt(3) / 2])) + 1 * np.ones(3) / 3, - rtol=1.2e-7 + np.outer(np.arange(32.5), np.array([0.5, 1, np.sqrt(3) / 2])) + + 1 * np.ones(3) / 3, + rtol=1.2e-7, ) assert_allclose( transformed_coordinates[2::3], - np.outer(np.arange(32.5), np.array([0.5, 1, np.sqrt(3) / 2])) + 2 * np.ones(3) / 3, - rtol=1.2e-7 + np.outer(np.arange(32.5), np.array([0.5, 1, np.sqrt(3) / 2])) + + 2 * np.ones(3) / 3, + rtol=1.2e-7, ) @@ -189,7 +203,9 @@ def test_nojump_constantvel(nojump_constantvel_universe): values when iterating forwards over the sample trajectory. """ ref = nojump_constantvel_universe - towrap = ref.copy() # This copy of the universe will be wrapped, then unwrapped, + towrap = ( + ref.copy() + ) # This copy of the universe will be wrapped, then unwrapped, # and should be equal to ref. dim = np.asarray([5, 5, 5, 54, 60, 90], np.float32) workflow = [ @@ -225,12 +241,14 @@ def test_nojump_2nd_frame(nojump_universe_npt_2nd_frame): unwrapped = [97.5, 50.0, 50.0] """ u = nojump_universe_npt_2nd_frame - dim = np.asarray([ - [100, 100, 100, 90, 90, 90], - [95, 100, 100, 90, 90, 90], # Box shrinks by 5 in the x-dimension - [95, 100, 100, 90, 90, 90], - [95, 100, 100, 90, 90, 90], - ]) + dim = np.asarray( + [ + [100, 100, 100, 90, 90, 90], + [95, 100, 100, 90, 90, 90], # Box shrinks by 5 in the x-dimension + [95, 100, 100, 90, 90, 90], + [95, 100, 100, 90, 90, 90], + ] + ) workflow = [ mda.transformations.boxdimensions.set_dimensions(dim), NoJump(), @@ -259,12 +277,14 @@ def test_nojump_3rd_frame(nojump_universe_npt_3rd_frame): unwrapped = [97.5, 50.0, 50.0] """ u = nojump_universe_npt_3rd_frame - dim = np.asarray([ - [100, 100, 100, 90, 90, 90], - [100, 100, 100, 90, 90, 90], - [95, 100, 100, 90, 90, 90], # Box shrinks by 5 in the x-dimension - [95, 100, 100, 90, 90, 90], - ]) + dim = np.asarray( + [ + [100, 100, 100, 90, 90, 90], + [100, 100, 100, 90, 90, 90], + [95, 100, 100, 90, 90, 90], # Box shrinks by 5 in the x-dimension + [95, 100, 100, 90, 90, 90], + ] + ) workflow = [ mda.transformations.boxdimensions.set_dimensions(dim), NoJump(), @@ -283,7 +303,9 @@ def test_nojump_iterate_twice(nojump_universe_npt_2nd_frame_from_file): u.trajectory.add_transformations(NoJump()) timeseries_first_iteration = u.trajectory.timeseries() timeseries_second_iteration = u.trajectory.timeseries() - np.testing.assert_allclose(timeseries_first_iteration, timeseries_second_iteration) + np.testing.assert_allclose( + timeseries_first_iteration, timeseries_second_iteration + ) def test_nojump_constantvel_skip(nojump_universes_fromfile): @@ -293,7 +315,7 @@ def test_nojump_constantvel_skip(nojump_universes_fromfile): with pytest.warns(UserWarning): u = nojump_universes_fromfile u.trajectory[0] - u.trajectory[9] #Exercises the warning. + u.trajectory[9] # Exercises the warning. def test_nojump_constantvel_stride_2(nojump_universes_fromfile): @@ -351,6 +373,9 @@ def test_notinvertible(nojump_universe): with pytest.raises(mda.exceptions.NoDataError): u = nojump_universe dim = [1, 0, 0, 90, 90, 90] - workflow = [mda.transformations.boxdimensions.set_dimensions(dim),NoJump()] + workflow = [ + mda.transformations.boxdimensions.set_dimensions(dim), + NoJump(), + ] u.trajectory.add_transformations(*workflow) transformed_coordinates = u.trajectory.timeseries()[0] diff --git a/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py b/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py index ba2c348bd06..71e4251366e 100644 --- a/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py +++ b/testsuite/MDAnalysisTests/transformations/test_positionaveraging.py @@ -8,104 +8,122 @@ from MDAnalysis.transformations import PositionAverager from MDAnalysisTests import datafiles + @pytest.fixture() def posaveraging_universes(): - ''' + """ Create the universe objects for the tests. - ''' + """ u = md.Universe(datafiles.XTC_multi_frame, to_guess=()) transformation = PositionAverager(3) u.trajectory.add_transformations(transformation) return u + @pytest.fixture() def posaveraging_universes_noreset(): - ''' + """ Create the universe objects for the tests. Position averaging reset is set to False. - ''' - u = md.Universe(datafiles.XTC_multi_frame, to_guess=()) + """ + u = md.Universe(datafiles.XTC_multi_frame, to_guess=()) transformation = PositionAverager(3, check_reset=False) u.trajectory.add_transformations(transformation) - return u + return u + def test_posavging_fwd(posaveraging_universes): - ''' + """ Test if the position averaging function is returning the correct values when iterating forwards over the trajectory. - ''' - ref_matrix_fwd = np.asarray([80., 80., 80.]) - size = (posaveraging_universes.trajectory.ts.positions.shape[0], - posaveraging_universes.trajectory.ts.positions.shape[1], - len(posaveraging_universes.trajectory)) - avgd = np.empty(size) + """ + ref_matrix_fwd = np.asarray([80.0, 80.0, 80.0]) + size = ( + posaveraging_universes.trajectory.ts.positions.shape[0], + posaveraging_universes.trajectory.ts.positions.shape[1], + len(posaveraging_universes.trajectory), + ) + avgd = np.empty(size) for ts in posaveraging_universes.trajectory: - avgd[...,ts.frame] = ts.positions.copy() - - assert_array_almost_equal(ref_matrix_fwd, avgd[1,:,-1], decimal=5) + avgd[..., ts.frame] = ts.positions.copy() + + assert_array_almost_equal(ref_matrix_fwd, avgd[1, :, -1], decimal=5) + def test_posavging_bwd(posaveraging_universes): - ''' + """ Test if the position averaging function is returning the correct values when iterating backwards over the trajectory. - ''' - ref_matrix_bwd = np.asarray([10., 10., 10.]) - size = (posaveraging_universes.trajectory.ts.positions.shape[0], - posaveraging_universes.trajectory.ts.positions.shape[1], - len(posaveraging_universes.trajectory)) + """ + ref_matrix_bwd = np.asarray([10.0, 10.0, 10.0]) + size = ( + posaveraging_universes.trajectory.ts.positions.shape[0], + posaveraging_universes.trajectory.ts.positions.shape[1], + len(posaveraging_universes.trajectory), + ) back_avgd = np.empty(size) for ts in posaveraging_universes.trajectory[::-1]: - back_avgd[...,9-ts.frame] = ts.positions.copy() - assert_array_almost_equal(ref_matrix_bwd, back_avgd[1,:,-1], decimal=5) + back_avgd[..., 9 - ts.frame] = ts.positions.copy() + assert_array_almost_equal(ref_matrix_bwd, back_avgd[1, :, -1], decimal=5) + def test_posavging_reset(posaveraging_universes): - ''' + """ Test if the automatic reset is working as intended. - ''' - size = (posaveraging_universes.trajectory.ts.positions.shape[0], - posaveraging_universes.trajectory.ts.positions.shape[1], - len(posaveraging_universes.trajectory)) - avgd = np.empty(size) + """ + size = ( + posaveraging_universes.trajectory.ts.positions.shape[0], + posaveraging_universes.trajectory.ts.positions.shape[1], + len(posaveraging_universes.trajectory), + ) + avgd = np.empty(size) for ts in posaveraging_universes.trajectory: - avgd[...,ts.frame] = ts.positions.copy() + avgd[..., ts.frame] = ts.positions.copy() after_reset = ts.positions.copy() - assert_array_almost_equal(avgd[...,0], after_reset, decimal=5) + assert_array_almost_equal(avgd[..., 0], after_reset, decimal=5) + def test_posavging_specific(posaveraging_universes): - ''' + """ Test if the position averaging function is returning the correct values when iterating over arbitrary non-sequential frames. check_reset=True - ''' - ref_matrix_specr = np.asarray([30., 30., 30.]) + """ + ref_matrix_specr = np.asarray([30.0, 30.0, 30.0]) fr_list = [0, 1, 7, 3] - size = (posaveraging_universes.trajectory.ts.positions.shape[0], - posaveraging_universes.trajectory.ts.positions.shape[1], - len(fr_list)) + size = ( + posaveraging_universes.trajectory.ts.positions.shape[0], + posaveraging_universes.trajectory.ts.positions.shape[1], + len(fr_list), + ) specr_avgd = np.empty(size) idx = 0 for ts in posaveraging_universes.trajectory[fr_list]: - specr_avgd[...,idx] = ts.positions.copy() + specr_avgd[..., idx] = ts.positions.copy() idx += 1 - assert_array_almost_equal(ref_matrix_specr, specr_avgd[1,:,-1], decimal=5) - + assert_array_almost_equal( + ref_matrix_specr, specr_avgd[1, :, -1], decimal=5 + ) + + def test_posavging_specific_noreset(posaveraging_universes_noreset): - ''' + """ Test if the position averaging function is returning the correct values when iterating over arbitrary non-sequential frames. check_reset=False - ''' + """ ref_matrix_specr = np.asarray([36.66667, 36.66667, 36.66667]) fr_list = [0, 1, 7, 3] - size = (posaveraging_universes_noreset.trajectory.ts.positions.shape[0], - posaveraging_universes_noreset.trajectory.ts.positions.shape[1], - len(fr_list)) + size = ( + posaveraging_universes_noreset.trajectory.ts.positions.shape[0], + posaveraging_universes_noreset.trajectory.ts.positions.shape[1], + len(fr_list), + ) specr_avgd = np.empty(size) idx = 0 for ts in posaveraging_universes_noreset.trajectory[fr_list]: - specr_avgd[...,idx] = ts.positions.copy() + specr_avgd[..., idx] = ts.positions.copy() idx += 1 - assert_array_almost_equal(ref_matrix_specr, specr_avgd[1,:,-1], decimal=5) - - - + assert_array_almost_equal( + ref_matrix_specr, specr_avgd[1, :, -1], decimal=5 + ) diff --git a/testsuite/MDAnalysisTests/transformations/test_rotate.py b/testsuite/MDAnalysisTests/transformations/test_rotate.py index 77ffd561647..4f8fd9867b5 100644 --- a/testsuite/MDAnalysisTests/transformations/test_rotate.py +++ b/testsuite/MDAnalysisTests/transformations/test_rotate.py @@ -21,21 +21,24 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # +import MDAnalysis as mda import numpy as np import pytest +from MDAnalysis.lib.transformations import rotation_matrix +from MDAnalysis.transformations import rotateby from numpy.testing import assert_array_almost_equal -import MDAnalysis as mda -from MDAnalysis.transformations import rotateby -from MDAnalysis.lib.transformations import rotation_matrix from MDAnalysisTests import make_Universe + @pytest.fixture() def rotate_universes(): # create the Universe objects for the tests reference = make_Universe(trajectory=True) - transformed = make_Universe(['masses'], trajectory=True) - transformed.trajectory.ts.dimensions = np.array([372., 373., 374., 90, 90, 90]) + transformed = make_Universe(["masses"], trajectory=True) + transformed.trajectory.ts.dimensions = np.array( + [372.0, 373.0, 374.0, 90, 90, 90] + ) return reference, transformed @@ -45,24 +48,34 @@ def test_rotation_matrix(): angle = 180 vector = [0, 0, 1] pos = [0, 0, 0] - ref_matrix = np.asarray([[-1, 0, 0], - [0, -1, 0], - [0, 0, 1]], np.float64) + ref_matrix = np.asarray( + [ + [-1, 0, 0], + [0, -1, 0], + [0, 0, 1], + ], + np.float64, + ) matrix = rotation_matrix(np.deg2rad(angle), vector, pos)[:3, :3] assert_array_almost_equal(matrix, ref_matrix, decimal=6) # another angle in a custom axis angle = 60 vector = [1, 2, 3] pos = [1, 2, 3] - ref_matrix = np.asarray([[ 0.53571429, -0.6229365 , 0.57005291], - [ 0.76579365, 0.64285714, -0.01716931], - [-0.35576719, 0.44574074, 0.82142857]], np.float64) + ref_matrix = np.asarray( + [ + [0.53571429, -0.6229365, 0.57005291], + [0.76579365, 0.64285714, -0.01716931], + [-0.35576719, 0.44574074, 0.82142857], + ], + np.float64, + ) matrix = rotation_matrix(np.deg2rad(angle), vector, pos)[:3, :3] assert_array_almost_equal(matrix, ref_matrix, decimal=6) - -@pytest.mark.parametrize('point', ( - np.asarray([0, 0, 0]), - np.asarray([[0, 0, 0]])) + + +@pytest.mark.parametrize( + "point", (np.asarray([0, 0, 0]), np.asarray([[0, 0, 0]])) ) def test_rotateby_custom_point(rotate_universes, point): # what happens when we use a custom point for the axis of rotation? @@ -71,7 +84,7 @@ def test_rotateby_custom_point(rotate_universes, point): trans = trans_u.trajectory.ts ref = ref_u.trajectory.ts vector = [1, 0, 0] - pos = point.reshape(3, ) + pos = point.reshape(3) angle = 90 matrix = rotation_matrix(np.deg2rad(angle), vector, pos) ref_u.atoms.transform(matrix) @@ -79,9 +92,8 @@ def test_rotateby_custom_point(rotate_universes, point): assert_array_almost_equal(transformed.positions, ref.positions, decimal=6) -@pytest.mark.parametrize('vector', ( - np.asarray([1, 0, 0]), - np.asarray([[1, 0, 0]])) +@pytest.mark.parametrize( + "vector", (np.asarray([1, 0, 0]), np.asarray([[1, 0, 0]])) ) def test_rotateby_vector(rotate_universes, vector): # what happens when we use a custom point for the axis of rotation? @@ -91,7 +103,7 @@ def test_rotateby_vector(rotate_universes, vector): ref = ref_u.trajectory.ts point = [0, 0, 0] angle = 90 - vec = vector.reshape(3, ) + vec = vector.reshape(3) matrix = rotation_matrix(np.deg2rad(angle), vec, point) ref_u.atoms.transform(matrix) transformed = rotateby(angle, vector, point=point)(trans) @@ -105,13 +117,13 @@ def test_rotateby_atomgroup_cog_nopbc(rotate_universes): trans_u = rotate_universes[1] trans = trans_u.trajectory.ts ref = ref_u.trajectory.ts - center_pos = [6,7,8] - vector = [1,0,0] + center_pos = [6, 7, 8] + vector = [1, 0, 0] angle = 90 matrix = rotation_matrix(np.deg2rad(angle), vector, center_pos) ref_u.atoms.transform(matrix) selection = trans_u.residues[0].atoms - transformed = rotateby(angle, vector, ag=selection, weights=None)(trans) + transformed = rotateby(angle, vector, ag=selection, weights=None)(trans) assert_array_almost_equal(transformed.positions, ref.positions, decimal=6) @@ -122,16 +134,16 @@ def test_rotateby_atomgroup_com_nopbc(rotate_universes): trans_u = rotate_universes[1] trans = trans_u.trajectory.ts ref = ref_u.trajectory.ts - vector = [1,0,0] + vector = [1, 0, 0] angle = 90 selection = trans_u.residues[0].atoms center_pos = selection.center_of_mass() matrix = rotation_matrix(np.deg2rad(angle), vector, center_pos) ref_u.atoms.transform(matrix) - transformed = rotateby(angle, vector, ag=selection, weights='mass')(trans) + transformed = rotateby(angle, vector, ag=selection, weights="mass")(trans) assert_array_almost_equal(transformed.positions, ref.positions, decimal=6) - + def test_rotateby_atomgroup_cog_pbc(rotate_universes): # what happens when we rotate arround the center of geometry of a residue # with pbc? @@ -139,13 +151,15 @@ def test_rotateby_atomgroup_cog_pbc(rotate_universes): trans_u = rotate_universes[1] trans = trans_u.trajectory.ts ref = ref_u.trajectory.ts - vector = [1,0,0] + vector = [1, 0, 0] angle = 90 selection = trans_u.residues[0].atoms center_pos = selection.center_of_geometry(pbc=True) matrix = rotation_matrix(np.deg2rad(angle), vector, center_pos) ref_u.atoms.transform(matrix) - transformed = rotateby(angle, vector, ag=selection, weights=None, wrap=True)(trans) + transformed = rotateby( + angle, vector, ag=selection, weights=None, wrap=True + )(trans) assert_array_almost_equal(transformed.positions, ref.positions, decimal=6) @@ -156,25 +170,30 @@ def test_rotateby_atomgroup_com_pbc(rotate_universes): trans_u = rotate_universes[1] trans = trans_u.trajectory.ts ref = ref_u.trajectory.ts - vector = [1,0,0] + vector = [1, 0, 0] angle = 90 selection = trans_u.residues[0].atoms center_pos = selection.center_of_mass(pbc=True) matrix = rotation_matrix(np.deg2rad(angle), vector, center_pos) ref_u.atoms.transform(matrix) - transformed = rotateby(angle, vector, ag=selection, weights='mass', wrap=True)(trans) + transformed = rotateby( + angle, vector, ag=selection, weights="mass", wrap=True + )(trans) assert_array_almost_equal(transformed.positions, ref.positions, decimal=6) -@pytest.mark.parametrize('ag', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]]), - 'thisisnotanag', - 1) +@pytest.mark.parametrize( + "ag", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + "thisisnotanag", + 1, + ), ) def test_rotateby_bad_ag(rotate_universes, ag): # this universe as a box size zero @@ -184,19 +203,22 @@ def test_rotateby_bad_ag(rotate_universes, ag): angle = 90 vector = [0, 0, 1] bad_ag = 1 - with pytest.raises(ValueError): - rotateby(angle, vector, ag = bad_ag)(ts) - - -@pytest.mark.parametrize('point', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]]), - 'thisisnotapoint', - 1) + with pytest.raises(ValueError): + rotateby(angle, vector, ag=bad_ag)(ts) + + +@pytest.mark.parametrize( + "point", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + "thisisnotapoint", + 1, + ), ) def test_rotateby_bad_point(rotate_universes, point): # this universe as a box size zero @@ -205,19 +227,22 @@ def test_rotateby_bad_point(rotate_universes, point): angle = 90 vector = [0, 0, 1] bad_position = point - with pytest.raises(ValueError): + with pytest.raises(ValueError): rotateby(angle, vector, point=bad_position)(ts) -@pytest.mark.parametrize('direction', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]]), - 'thisisnotadirection', - 1) +@pytest.mark.parametrize( + "direction", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + "thisisnotadirection", + 1, + ), ) def test_rotateby_bad_direction(rotate_universes, direction): # this universe as a box size zero @@ -225,11 +250,11 @@ def test_rotateby_bad_direction(rotate_universes, direction): # what if the box is in the wrong format? angle = 90 point = [0, 0, 0] - with pytest.raises(ValueError): + with pytest.raises(ValueError): rotateby(angle, direction, point=point)(ts) -def test_rotateby_bad_pbc(rotate_universes): +def test_rotateby_bad_pbc(rotate_universes): # this universe as a box size zero ts = rotate_universes[0].trajectory.ts ag = rotate_universes[0].residues[0].atoms @@ -237,18 +262,21 @@ def test_rotateby_bad_pbc(rotate_universes): # if yes it should raise an exception for boxes that are zero in size vector = [1, 0, 0] angle = 90 - with pytest.raises(ValueError): - rotateby(angle, vector, ag = ag, wrap=True)(ts) - - -@pytest.mark.parametrize('weights', ( - " ", - "totallynotmasses", - 123456789, - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])) + with pytest.raises(ValueError): + rotateby(angle, vector, ag=ag, wrap=True)(ts) + + +@pytest.mark.parametrize( + "weights", + ( + " ", + "totallynotmasses", + 123456789, + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + ), ) def test_rotateby_bad_weights(rotate_universes, weights): # this universe as a box size zero @@ -258,11 +286,11 @@ def test_rotateby_bad_weights(rotate_universes, weights): angle = 90 vector = [0, 0, 1] bad_weights = " " - with pytest.raises(TypeError): - rotateby(angle, vector, ag = ag, weights=bad_weights)(ts) + with pytest.raises(TypeError): + rotateby(angle, vector, ag=ag, weights=bad_weights)(ts) + - -def test_rotateby_no_masses(rotate_universes): +def test_rotateby_no_masses(rotate_universes): # this universe as a box size zero ts = rotate_universes[0].trajectory.ts ag = rotate_universes[0].residues[0].atoms @@ -270,8 +298,8 @@ def test_rotateby_no_masses(rotate_universes): angle = 90 vector = [0, 0, 1] bad_center = "mass" - with pytest.raises(TypeError): - rotateby(angle, vector, ag = ag, weights=bad_center)(ts) + with pytest.raises(TypeError): + rotateby(angle, vector, ag=ag, weights=bad_center)(ts) def test_rotateby_no_args(rotate_universes): @@ -281,5 +309,5 @@ def test_rotateby_no_args(rotate_universes): vector = [0, 0, 1] # if no point or AtomGroup are passed to the function # it should raise a ValueError - with pytest.raises(ValueError): + with pytest.raises(ValueError): rotateby(angle, vector)(ts) diff --git a/testsuite/MDAnalysisTests/transformations/test_translate.py b/testsuite/MDAnalysisTests/transformations/test_translate.py index d8bde95009b..ccb431f3c52 100644 --- a/testsuite/MDAnalysisTests/transformations/test_translate.py +++ b/testsuite/MDAnalysisTests/transformations/test_translate.py @@ -1,4 +1,4 @@ -#-*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 # # MDAnalysis --- https://www.mdanalysis.org @@ -35,9 +35,11 @@ def translate_universes(): # create the Universe objects for the tests # this universe has no masses and some tests need it as such reference = make_Universe(trajectory=True) - transformed = make_Universe(['masses'], trajectory=True) - transformed.trajectory.ts.dimensions = np.array([372., 373., 374., 90, 90, 90]) - + transformed = make_Universe(["masses"], trajectory=True) + transformed.trajectory.ts.dimensions = np.array( + [372.0, 373.0, 374.0, 90, 90, 90] + ) + return reference, transformed @@ -50,13 +52,16 @@ def test_translate_coords(translate_universes): assert_array_almost_equal(trans.positions, ref.positions, decimal=6) -@pytest.mark.parametrize('vector', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]])) +@pytest.mark.parametrize( + "vector", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + ), ) def test_translate_vector(translate_universes, vector): # what happens if the vector argument is of wrong size? @@ -64,16 +69,18 @@ def test_translate_vector(translate_universes, vector): with pytest.raises(ValueError): translate(vector)(ts) - + def test_translate_transformations_api(translate_universes): - # test if the translate transformation works when using the + # test if the translate transformation works when using the # on-the-fly transformations API ref_u, trans_u = translate_universes ref = ref_u.trajectory.ts vector = np.float32([1, 2, 3]) ref.positions += vector trans_u.trajectory.add_transformations(translate(vector)) - assert_array_almost_equal(trans_u.trajectory.ts.positions, ref.positions, decimal=6) + assert_array_almost_equal( + trans_u.trajectory.ts.positions, ref.positions, decimal=6 + ) def test_center_in_box_bad_ag(translate_universes): @@ -81,33 +88,36 @@ def test_center_in_box_bad_ag(translate_universes): ts = translate_universes[0].trajectory.ts # what happens if something other than an AtomGroup is given? bad_ag = 1 - with pytest.raises(ValueError): + with pytest.raises(ValueError): center_in_box(bad_ag)(ts) -@pytest.mark.parametrize('point', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]])) +@pytest.mark.parametrize( + "point", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + ), ) def test_center_in_box_bad_point(translate_universes, point): ts = translate_universes[0].trajectory.ts ag = translate_universes[0].residues[0].atoms # what if the box is in the wrong format? - with pytest.raises(ValueError): + with pytest.raises(ValueError): center_in_box(ag, point=point)(ts) - -def test_center_in_box_bad_pbc(translate_universes): + +def test_center_in_box_bad_pbc(translate_universes): # this universe has a box size zero ts = translate_universes[0].trajectory.ts ag = translate_universes[0].residues[0].atoms # is pbc passed to the center methods? # if yes it should raise an exception for boxes that are zero in size - with pytest.raises(ValueError): + with pytest.raises(ValueError): center_in_box(ag, wrap=True)(ts) @@ -117,11 +127,11 @@ def test_center_in_box_bad_center(translate_universes): ag = translate_universes[0].residues[0].atoms # what if a wrong center type name is passed? bad_center = " " - with pytest.raises(ValueError): + with pytest.raises(ValueError): center_in_box(ag, center=bad_center)(ts) -def test_center_in_box_no_masses(translate_universes): +def test_center_in_box_no_masses(translate_universes): # this universe has no masses ts = translate_universes[0].trajectory.ts ag = translate_universes[0].residues[0].atoms @@ -137,7 +147,7 @@ def test_center_in_box_coords_no_options(translate_universes): ref_u, trans_u = translate_universes ref = ref_u.trajectory.ts ref_center = np.float32([6, 7, 8]) - box_center = np.float32([186., 186.5, 187.]) + box_center = np.float32([186.0, 186.5, 187.0]) ref.positions += box_center - ref_center ag = trans_u.residues[0].atoms trans = center_in_box(ag)(trans_u.trajectory.ts) @@ -149,28 +159,28 @@ def test_center_in_box_coords_with_pbc(translate_universes): # using pbc into account for center of geometry calculation ref_u, trans_u = translate_universes ref = ref_u.trajectory.ts - trans_u.dimensions = [363., 364., 365., 90., 90., 90.] + trans_u.dimensions = [363.0, 364.0, 365.0, 90.0, 90.0, 90.0] ag = trans_u.residues[24].atoms - box_center = np.float32([181.5, 182., 182.5]) - ref_center = np.float32([75.6, 75.8, 76.]) + box_center = np.float32([181.5, 182.0, 182.5]) + ref_center = np.float32([75.6, 75.8, 76.0]) ref.positions += box_center - ref_center trans = center_in_box(ag, wrap=True)(trans_u.trajectory.ts) assert_array_almost_equal(trans.positions, ref.positions, decimal=6) -def test_center_in_box_coords_with_mass(translate_universes): +def test_center_in_box_coords_with_mass(translate_universes): # using masses for calculating the center of the atomgroup ref_u, trans_u = translate_universes ref = ref_u.trajectory.ts ag = trans_u.residues[24].atoms - box_center = np.float32([186., 186.5, 187.]) + box_center = np.float32([186.0, 186.5, 187.0]) ref_center = ag.center_of_mass() ref.positions += box_center - ref_center trans = center_in_box(ag, center="mass")(trans_u.trajectory.ts) assert_array_almost_equal(trans.positions, ref.positions, decimal=6) -def test_center_in_box_coords_with_box(translate_universes): +def test_center_in_box_coords_with_box(translate_universes): # using masses for calculating the center of the atomgroup ref_u, trans_u = translate_universes ref = ref_u.trajectory.ts @@ -193,18 +203,22 @@ def test_center_in_box_coords_all_options(translate_universes): box_center = np.float32(newpoint) ref_center = ag.center_of_mass(pbc=True) ref.positions += box_center - ref_center - trans = center_in_box(ag, center='mass', wrap=True, point=newpoint)(trans_u.trajectory.ts) + trans = center_in_box(ag, center="mass", wrap=True, point=newpoint)( + trans_u.trajectory.ts + ) assert_array_almost_equal(trans.positions, ref.positions, decimal=6) def test_center_transformations_api(translate_universes): - # test if the translate transformation works when using the + # test if the translate transformation works when using the # on-the-fly transformations API ref_u, trans_u = translate_universes ref = ref_u.trajectory.ts ref_center = np.float32([6, 7, 8]) - box_center = np.float32([186., 186.5, 187.]) + box_center = np.float32([186.0, 186.5, 187.0]) ref.positions += box_center - ref_center ag = trans_u.residues[0].atoms trans_u.trajectory.add_transformations(center_in_box(ag)) - assert_array_almost_equal(trans_u.trajectory.ts.positions, ref.positions, decimal=6) + assert_array_almost_equal( + trans_u.trajectory.ts.positions, ref.positions, decimal=6 + ) diff --git a/testsuite/MDAnalysisTests/transformations/test_wrap.py b/testsuite/MDAnalysisTests/transformations/test_wrap.py index a9fa34a36a4..a3439fb95cb 100644 --- a/testsuite/MDAnalysisTests/transformations/test_wrap.py +++ b/testsuite/MDAnalysisTests/transformations/test_wrap.py @@ -1,4 +1,4 @@ -#-*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 # # MDAnalysis --- https://www.mdanalysis.org @@ -39,7 +39,7 @@ def wrap_universes(): transformed = mda.Universe(fullerene) transformed.dimensions = np.asarray([10, 10, 10, 90, 90, 90], np.float32) transformed.atoms.wrap() - + return reference, transformed @@ -52,30 +52,33 @@ def compound_wrap_universes(): reference = mda.Universe(TPR, GRO) # wrap the atoms back into the unit cell # in this coordinate file only the protein - # is broken across PBC however the system + # is broken across PBC however the system # shape is not the same as the unit cell make_whole(reference.select_atoms("protein")) make_whole(transformed.select_atoms("protein")) - + return transformed, reference -@pytest.mark.parametrize('ag', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]]), - 'thisisnotanag', - 1) +@pytest.mark.parametrize( + "ag", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + "thisisnotanag", + 1, + ), ) def test_wrap_bad_ag(wrap_universes, ag): # this universe has a box size zero ts = wrap_universes[0].trajectory.ts # what happens if something other than an AtomGroup is given? bad_ag = ag - with pytest.raises(AttributeError): + with pytest.raises(AttributeError): wrap(bad_ag)(ts) @@ -85,45 +88,53 @@ def test_wrap_no_options(wrap_universes): trans, ref = wrap_universes trans.dimensions = ref.dimensions wrap(trans.atoms)(trans.trajectory.ts) - assert_array_almost_equal(trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6) + assert_array_almost_equal( + trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6 + ) -@pytest.mark.parametrize('compound', ( - "group", - "residues", - "segments", - "fragments") +@pytest.mark.parametrize( + "compound", ("group", "residues", "segments", "fragments") ) def test_wrap_with_compounds(compound_wrap_universes, compound): - trans, ref= compound_wrap_universes + trans, ref = compound_wrap_universes ref.select_atoms("not resname SOL").wrap(compound=compound) - wrap(trans.select_atoms("not resname SOL"), compound=compound)(trans.trajectory.ts) - assert_array_almost_equal(trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6) + wrap(trans.select_atoms("not resname SOL"), compound=compound)( + trans.trajectory.ts + ) + assert_array_almost_equal( + trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6 + ) def test_wrap_api(wrap_universes): trans, ref = wrap_universes trans.dimensions = ref.dimensions trans.trajectory.add_transformations(wrap(trans.atoms)) - assert_array_almost_equal(trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6) - - -@pytest.mark.parametrize('ag', ( - [0, 1], - [0, 1, 2, 3, 4], - np.array([0, 1]), - np.array([0, 1, 2, 3, 4]), - np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), - np.array([[0], [1], [2]]), - 'thisisnotanag', - 1) + assert_array_almost_equal( + trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6 + ) + + +@pytest.mark.parametrize( + "ag", + ( + [0, 1], + [0, 1, 2, 3, 4], + np.array([0, 1]), + np.array([0, 1, 2, 3, 4]), + np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + np.array([[0], [1], [2]]), + "thisisnotanag", + 1, + ), ) def test_unwrap_bad_ag(wrap_universes, ag): # this universe has a box size zero ts = wrap_universes[0].trajectory.ts # what happens if something other than an AtomGroup is given? bad_ag = ag - with pytest.raises(AttributeError): + with pytest.raises(AttributeError): unwrap(bad_ag)(ts) @@ -131,11 +142,15 @@ def test_unwrap(wrap_universes): ref, trans = wrap_universes # after rebuild the trans molecule it should match the reference unwrap(trans.atoms)(trans.trajectory.ts) - assert_array_almost_equal(trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6) + assert_array_almost_equal( + trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6 + ) def test_unwrap_api(wrap_universes): ref, trans = wrap_universes # after rebuild the trans molecule it should match the reference trans.trajectory.add_transformations(unwrap(trans.atoms)) - assert_array_almost_equal(trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6) + assert_array_almost_equal( + trans.trajectory.ts.positions, ref.trajectory.ts.positions, decimal=6 + ) diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 7f42a28fefd..b53e8782e10 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -160,6 +160,7 @@ include = ''' ( setup\.py | MDAnalysisTests/auxiliary/.*\.py +| MDAnalysisTests/transformations/.*\.py ) ''' extend-exclude = '__pycache__' From 4e903c7ca11ac67f4d8b3990b40e52e272f3d3fc Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Sat, 7 Dec 2024 04:39:25 +1100 Subject: [PATCH 47/57] TST, BUG: add default None -> UserWarning (#4747) * add default None -> UserWarning for `no_deprecated_call` decorator * Add a regression test that serves the dual purpose of ensuring that the `no_deprecated_call` decorator behaves properly when the warning category is `None`, and also enforces our intended warnings behavior for the element guessing changes in MDAnalysisgh-4744. --------- Co-authored-by: Tyler Reddy --- testsuite/MDAnalysisTests/topology/test_itp.py | 3 +++ testsuite/MDAnalysisTests/util.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index 81a73cd3316..8711ac072a3 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -26,6 +26,7 @@ import numpy as np from numpy.testing import assert_allclose, assert_equal +from MDAnalysisTests.util import no_deprecated_call from MDAnalysisTests.topology.base import ParserBase from MDAnalysisTests.datafiles import ( ITP, # GROMACS itp @@ -490,6 +491,8 @@ def test_missing_elements_no_attribute(): u = mda.Universe(ITP_atomtypes) with pytest.raises(AttributeError): _ = u.atoms.elements + with no_deprecated_call(): + mda.Universe(ITP_atomtypes) def test_elements_deprecation_warning(): diff --git a/testsuite/MDAnalysisTests/util.py b/testsuite/MDAnalysisTests/util.py index 549a9f418a2..88631bdcff7 100644 --- a/testsuite/MDAnalysisTests/util.py +++ b/testsuite/MDAnalysisTests/util.py @@ -233,6 +233,11 @@ def _warn(self, message, category=None, *args, **kwargs): if isinstance(message, Warning): self._captured_categories.append(message.__class__) else: + # as follows Python documentation at + # https://docs.python.org/3/library/warnings.html#warnings.warn + # if category is None, the default UserWarning is used + if category is None: + category = UserWarning self._captured_categories.append(category) def __exit__(self, exc_type, exc_val, exc_tb): From 25e755fd78e0a6fb71a91c9d3d989328f021f34b Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Fri, 6 Dec 2024 22:47:30 +0100 Subject: [PATCH 48/57] [fmt] lib (#4804) Co-authored-by: Egor Marin --- package/MDAnalysis/lib/NeighborSearch.py | 38 +- package/MDAnalysis/lib/__init__.py | 21 +- package/MDAnalysis/lib/_distopia.py | 25 +- package/MDAnalysis/lib/correlations.py | 15 +- package/MDAnalysis/lib/distances.py | 699 ++++--- package/MDAnalysis/lib/formats/__init__.py | 2 +- package/MDAnalysis/lib/log.py | 12 +- package/MDAnalysis/lib/mdamath.py | 43 +- package/MDAnalysis/lib/picklable_file_io.py | 111 +- package/MDAnalysis/lib/pkdtree.py | 112 +- package/MDAnalysis/lib/transformations.py | 208 ++- package/MDAnalysis/lib/util.py | 538 ++++-- package/pyproject.toml | 3 +- testsuite/MDAnalysisTests/lib/test_augment.py | 145 +- testsuite/MDAnalysisTests/lib/test_cutil.py | 72 +- .../MDAnalysisTests/lib/test_distances.py | 1604 +++++++++++------ testsuite/MDAnalysisTests/lib/test_log.py | 12 +- .../lib/test_neighborsearch.py | 12 +- testsuite/MDAnalysisTests/lib/test_nsgrid.py | 267 ++- testsuite/MDAnalysisTests/lib/test_pkdtree.py | 85 +- testsuite/MDAnalysisTests/lib/test_qcprot.py | 22 +- testsuite/MDAnalysisTests/lib/test_util.py | 1592 +++++++++------- testsuite/pyproject.toml | 1 + 23 files changed, 3576 insertions(+), 2063 deletions(-) diff --git a/package/MDAnalysis/lib/NeighborSearch.py b/package/MDAnalysis/lib/NeighborSearch.py index d09284773ec..b1f5fa7185d 100644 --- a/package/MDAnalysis/lib/NeighborSearch.py +++ b/package/MDAnalysis/lib/NeighborSearch.py @@ -44,8 +44,9 @@ class AtomNeighborSearch(object): :class:`~MDAnalysis.lib.distances.capped_distance`. """ - def __init__(self, atom_group: AtomGroup, - box: Optional[npt.ArrayLike] = None) -> None: + def __init__( + self, atom_group: AtomGroup, box: Optional[npt.ArrayLike] = None + ) -> None: """ Parameters @@ -62,10 +63,9 @@ def __init__(self, atom_group: AtomGroup, self._u = atom_group.universe self._box = box - def search(self, atoms: AtomGroup, - radius: float, - level: str = 'A' - ) -> Optional[Union[AtomGroup, ResidueGroup, SegmentGroup]]: + def search( + self, atoms: AtomGroup, radius: float, level: str = "A" + ) -> Optional[Union[AtomGroup, ResidueGroup, SegmentGroup]]: """ Return all atoms/residues/segments that are within *radius* of the atoms in *atoms*. @@ -102,17 +102,21 @@ def search(self, atoms: AtomGroup, except AttributeError: # For atom, take the position attribute position = atoms.position - pairs = capped_distance(position, self.atom_group.positions, - radius, box=self._box, return_distances=False) + pairs = capped_distance( + position, + self.atom_group.positions, + radius, + box=self._box, + return_distances=False, + ) if pairs.size > 0: unique_idx = unique_int_1d(np.asarray(pairs[:, 1], dtype=np.intp)) return self._index2level(unique_idx, level) - def _index2level(self, - indices: List[int], - level: str - ) -> Union[AtomGroup, ResidueGroup, SegmentGroup]: + def _index2level( + self, indices: List[int], level: str + ) -> Union[AtomGroup, ResidueGroup, SegmentGroup]: """Convert list of atom_indices in a AtomGroup to either the Atoms or segments/residues containing these atoms. @@ -125,11 +129,13 @@ def _index2level(self, *radius* of *atoms*. """ atomgroup = self.atom_group[indices] - if level == 'A': + if level == "A": return atomgroup - elif level == 'R': + elif level == "R": return atomgroup.residues - elif level == 'S': + elif level == "S": return atomgroup.segments else: - raise NotImplementedError('{0}: level not implemented'.format(level)) + raise NotImplementedError( + "{0}: level not implemented".format(level) + ) diff --git a/package/MDAnalysis/lib/__init__.py b/package/MDAnalysis/lib/__init__.py index a5bc6f8e877..cba6900d5bf 100644 --- a/package/MDAnalysis/lib/__init__.py +++ b/package/MDAnalysis/lib/__init__.py @@ -27,8 +27,17 @@ ================================================================ """ -__all__ = ['log', 'transformations', 'util', 'mdamath', 'distances', - 'NeighborSearch', 'formats', 'pkdtree', 'nsgrid'] +__all__ = [ + "log", + "transformations", + "util", + "mdamath", + "distances", + "NeighborSearch", + "formats", + "pkdtree", + "nsgrid", +] from . import log from . import transformations @@ -39,6 +48,8 @@ from . import formats from . import pkdtree from . import nsgrid -from .picklable_file_io import (FileIOPicklable, - BufferIOPicklable, - TextIOPicklable) +from .picklable_file_io import ( + FileIOPicklable, + BufferIOPicklable, + TextIOPicklable, +) diff --git a/package/MDAnalysis/lib/_distopia.py b/package/MDAnalysis/lib/_distopia.py index c2564bc2d23..297ce4a3b5e 100644 --- a/package/MDAnalysis/lib/_distopia.py +++ b/package/MDAnalysis/lib/_distopia.py @@ -39,13 +39,15 @@ # check for compatibility: currently needs to be >=0.2.0,<0.3.0 (issue # #4740) No distopia.__version__ available so we have to do some probing. - needed_funcs = ['calc_bonds_no_box_float', 'calc_bonds_ortho_float'] + needed_funcs = ["calc_bonds_no_box_float", "calc_bonds_ortho_float"] has_distopia_020 = all([hasattr(distopia, func) for func in needed_funcs]) if not has_distopia_020: - warnings.warn("Install 'distopia>=0.2.0,<0.3.0' to be used with this " - "release of MDAnalysis. Your installed version of " - "distopia >=0.3.0 will NOT be used.", - category=RuntimeWarning) + warnings.warn( + "Install 'distopia>=0.2.0,<0.3.0' to be used with this " + "release of MDAnalysis. Your installed version of " + "distopia >=0.3.0 will NOT be used.", + category=RuntimeWarning, + ) del distopia HAS_DISTOPIA = False @@ -59,23 +61,22 @@ 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 - ) + 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 - ) + 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 + coords1: np.ndarray, + coords2: np.ndarray, + box: np.ndarray, + results: np.ndarray, ) -> None: # redirect to serial backend warnings.warn( diff --git a/package/MDAnalysis/lib/correlations.py b/package/MDAnalysis/lib/correlations.py index 1ce0338c676..af14df99fd7 100644 --- a/package/MDAnalysis/lib/correlations.py +++ b/package/MDAnalysis/lib/correlations.py @@ -135,12 +135,18 @@ def autocorrelation(list_of_sets, tau_max, window_step=1): """ # check types - if (type(list_of_sets) != list and len(list_of_sets) != 0) or type(list_of_sets[0]) != set: - raise TypeError("list_of_sets must be a one-dimensional list of sets") # pragma: no cover + if (type(list_of_sets) != list and len(list_of_sets) != 0) or type( + list_of_sets[0] + ) != set: + raise TypeError( + "list_of_sets must be a one-dimensional list of sets" + ) # pragma: no cover # Check dimensions of parameters if len(list_of_sets) < tau_max: - raise ValueError("tau_max cannot be greater than the length of list_of_sets") # pragma: no cover + raise ValueError( + "tau_max cannot be greater than the length of list_of_sets" + ) # pragma: no cover tau_timeseries = list(range(1, tau_max + 1)) timeseries_data = [[] for _ in range(tau_max)] @@ -157,7 +163,7 @@ def autocorrelation(list_of_sets, tau_max, window_step=1): break # continuous: IDs that survive from t to t + tau and at every frame in between - Ntau = len(set.intersection(*list_of_sets[t:t + tau + 1])) + Ntau = len(set.intersection(*list_of_sets[t : t + tau + 1])) timeseries_data[tau - 1].append(Ntau / float(Nt)) timeseries = [np.mean(x) for x in timeseries_data] @@ -257,4 +263,3 @@ def correct_intermittency(list_of_sets, intermittency): seen_frames_ago[element] = 0 return list_of_sets - diff --git a/package/MDAnalysis/lib/distances.py b/package/MDAnalysis/lib/distances.py index a6c30abacd0..524b9f40635 100644 --- a/package/MDAnalysis/lib/distances.py +++ b/package/MDAnalysis/lib/distances.py @@ -123,6 +123,7 @@ from typing import Union, Optional, Callable from typing import TYPE_CHECKING + if TYPE_CHECKING: # pragma: no cover from ..core.groups import AtomGroup from .util import check_coords, check_box @@ -137,22 +138,31 @@ # the cython parallel code (prange) in parallel.distances is # independent from the OpenMP code import importlib + _distances = {} -_distances['serial'] = importlib.import_module(".c_distances", - package="MDAnalysis.lib") +_distances["serial"] = importlib.import_module( + ".c_distances", package="MDAnalysis.lib" +) try: - _distances['openmp'] = importlib.import_module(".c_distances_openmp", - package="MDAnalysis.lib") + _distances["openmp"] = importlib.import_module( + ".c_distances_openmp", package="MDAnalysis.lib" + ) except ImportError: pass if HAS_DISTOPIA: - _distances["distopia"] = importlib.import_module("._distopia", - package="MDAnalysis.lib") + _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: + +def _run( + funcname: str, + args: Optional[tuple] = None, + kwargs: Optional[dict] = None, + backend: str = "serial", +) -> Callable: """Helper function to select a backend function `funcname`.""" args = args if args is not None else tuple() kwargs = kwargs if kwargs is not None else dict() @@ -160,38 +170,44 @@ def _run(funcname: str, args: Optional[tuple] = None, try: func = getattr(_distances[backend], funcname) except KeyError: - errmsg = (f"Function {funcname} not available with backend {backend} " - f"try one of: {_distances.keys()}") + errmsg = ( + f"Function {funcname} not available with backend {backend} " + f"try one of: {_distances.keys()}" + ) raise ValueError(errmsg) from None return func(*args, **kwargs) + # serial versions are always available (and are typically used within # the core and topology modules) -from .c_distances import (_UINT64_MAX, - calc_distance_array, - calc_distance_array_ortho, - calc_distance_array_triclinic, - calc_self_distance_array, - calc_self_distance_array_ortho, - calc_self_distance_array_triclinic, - coord_transform, - calc_bond_distance, - calc_bond_distance_ortho, - calc_bond_distance_triclinic, - calc_angle, - calc_angle_ortho, - calc_angle_triclinic, - calc_dihedral, - calc_dihedral_ortho, - calc_dihedral_triclinic, - ortho_pbc, - triclinic_pbc) +from .c_distances import ( + _UINT64_MAX, + calc_distance_array, + calc_distance_array_ortho, + calc_distance_array_triclinic, + calc_self_distance_array, + calc_self_distance_array_ortho, + calc_self_distance_array_triclinic, + coord_transform, + calc_bond_distance, + calc_bond_distance_ortho, + calc_bond_distance_triclinic, + calc_angle, + calc_angle_ortho, + calc_angle_triclinic, + calc_dihedral, + calc_dihedral_ortho, + calc_dihedral_triclinic, + ortho_pbc, + triclinic_pbc, +) from .c_distances_openmp import OPENMP_ENABLED as USED_OPENMP -def _check_result_array(result: Optional[npt.NDArray], - shape: tuple) -> npt.NDArray: +def _check_result_array( + result: Optional[npt.NDArray], shape: tuple +) -> npt.NDArray: """Check if the result array is ok to use. The `result` array must meet the following requirements: @@ -221,24 +237,35 @@ def _check_result_array(result: Optional[npt.NDArray], if result is None: return np.zeros(shape, dtype=np.float64) if result.shape != shape: - raise ValueError("Result array has incorrect shape, should be {0}, got " - "{1}.".format(shape, result.shape)) + raise ValueError( + "Result array has incorrect shape, should be {0}, got " + "{1}.".format(shape, result.shape) + ) if result.dtype != np.float64: - raise TypeError("Result array must be of type numpy.float64, got {}." - "".format(result.dtype)) -# The following two lines would break a lot of tests. WHY?! -# if not coords.flags['C_CONTIGUOUS']: -# raise ValueError("{0} is not C-contiguous.".format(desc)) + raise TypeError( + "Result array must be of type numpy.float64, got {}." + "".format(result.dtype) + ) + # The following two lines would break a lot of tests. WHY?! + # if not coords.flags['C_CONTIGUOUS']: + # raise ValueError("{0} is not C-contiguous.".format(desc)) return result -@check_coords('reference', 'configuration', reduce_result_if_single=False, - check_lengths_match=False, allow_atomgroup=True) -def distance_array(reference: Union[npt.NDArray, 'AtomGroup'], - configuration: Union[npt.NDArray, 'AtomGroup'], - box: Optional[npt.NDArray] = None, - result: Optional[npt.NDArray] = None, - backend: str = "serial") -> npt.NDArray: +@check_coords( + "reference", + "configuration", + reduce_result_if_single=False, + check_lengths_match=False, + allow_atomgroup=True, +) +def distance_array( + reference: Union[npt.NDArray, "AtomGroup"], + configuration: Union[npt.NDArray, "AtomGroup"], + box: Optional[npt.NDArray] = None, + result: Optional[npt.NDArray] = None, + backend: str = "serial", +) -> npt.NDArray: """Calculate all possible distances between a reference set and another configuration. @@ -297,35 +324,45 @@ def distance_array(reference: Union[npt.NDArray, 'AtomGroup'], # check resulting array will not overflow UINT64_MAX if refnum * confnum > _UINT64_MAX: - raise ValueError(f"Size of resulting array {refnum * confnum} elements" - " larger than size of maximum integer") + raise ValueError( + f"Size of resulting array {refnum * confnum} elements" + " larger than size of maximum integer" + ) distances = _check_result_array(result, (refnum, confnum)) if len(distances) == 0: return distances if box is not None: boxtype, box = check_box(box) - if boxtype == 'ortho': - _run("calc_distance_array_ortho", - args=(reference, configuration, box, distances), - backend=backend) + if boxtype == "ortho": + _run( + "calc_distance_array_ortho", + args=(reference, configuration, box, distances), + backend=backend, + ) else: - _run("calc_distance_array_triclinic", - args=(reference, configuration, box, distances), - backend=backend) + _run( + "calc_distance_array_triclinic", + args=(reference, configuration, box, distances), + backend=backend, + ) else: - _run("calc_distance_array", - args=(reference, configuration, distances), - backend=backend) + _run( + "calc_distance_array", + args=(reference, configuration, distances), + backend=backend, + ) return distances -@check_coords('reference', reduce_result_if_single=False, allow_atomgroup=True) -def self_distance_array(reference: Union[npt.NDArray, 'AtomGroup'], - box: Optional[npt.NDArray] = None, - result: Optional[npt.NDArray] = None, - backend: str = "serial") -> npt.NDArray: +@check_coords("reference", reduce_result_if_single=False, allow_atomgroup=True) +def self_distance_array( + reference: Union[npt.NDArray, "AtomGroup"], + box: Optional[npt.NDArray] = None, + result: Optional[npt.NDArray] = None, + backend: str = "serial", +) -> npt.NDArray: """Calculate all possible distances within a configuration `reference`. If the optional argument `box` is supplied, the minimum image convention is @@ -380,39 +417,55 @@ def self_distance_array(reference: Union[npt.NDArray, 'AtomGroup'], distnum = refnum * (refnum - 1) // 2 # check resulting array will not overflow UINT64_MAX if distnum > _UINT64_MAX: - raise ValueError(f"Size of resulting array {distnum} elements larger" - " than size of maximum integer") + raise ValueError( + f"Size of resulting array {distnum} elements larger" + " than size of maximum integer" + ) distances = _check_result_array(result, (distnum,)) if len(distances) == 0: return distances if box is not None: boxtype, box = check_box(box) - if boxtype == 'ortho': - _run("calc_self_distance_array_ortho", - args=(reference, box, distances), - backend=backend) + if boxtype == "ortho": + _run( + "calc_self_distance_array_ortho", + args=(reference, box, distances), + backend=backend, + ) else: - _run("calc_self_distance_array_triclinic", - args=(reference, box, distances), - backend=backend) + _run( + "calc_self_distance_array_triclinic", + args=(reference, box, distances), + backend=backend, + ) else: - _run("calc_self_distance_array", - args=(reference, distances), - backend=backend) + _run( + "calc_self_distance_array", + args=(reference, distances), + backend=backend, + ) return distances -@check_coords('reference', 'configuration', enforce_copy=False, - reduce_result_if_single=False, check_lengths_match=False, - allow_atomgroup=True) -def capped_distance(reference: Union[npt.NDArray, 'AtomGroup'], - configuration: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - method: Optional[str] = None, - return_distances: Optional[bool] = True): +@check_coords( + "reference", + "configuration", + enforce_copy=False, + reduce_result_if_single=False, + check_lengths_match=False, + allow_atomgroup=True, +) +def capped_distance( + reference: Union[npt.NDArray, "AtomGroup"], + configuration: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + method: Optional[str] = None, + return_distances: Optional[bool] = True, +): """Calculates pairs of indices corresponding to entries in the `reference` and `configuration` arrays which are separated by a distance lying within the specified cutoff(s). Optionally, these distances can be returned as @@ -496,27 +549,43 @@ def capped_distance(reference: Union[npt.NDArray, 'AtomGroup'], if box is not None: box = np.asarray(box, dtype=np.float32) if box.shape[0] != 6: - raise ValueError("Box Argument is of incompatible type. The " - "dimension should be either None or of the form " - "[lx, ly, lz, alpha, beta, gamma]") + raise ValueError( + "Box Argument is of incompatible type. The " + "dimension should be either None or of the form " + "[lx, ly, lz, alpha, beta, gamma]" + ) # The check_coords decorator made sure that reference and configuration # are arrays of positions. Mypy does not know about that so we have to # tell it. reference_positions: npt.NDArray = reference # type: ignore configuration_positions: npt.NDArray = configuration # type: ignore - function = _determine_method(reference_positions, configuration_positions, - max_cutoff, min_cutoff=min_cutoff, - box=box, method=method) - return function(reference, configuration, - max_cutoff, min_cutoff=min_cutoff, - box=box, return_distances=return_distances) - - -def _determine_method(reference: npt.NDArray, configuration: npt.NDArray, - max_cutoff: float, min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - method: Optional[str] = None) -> Callable: + function = _determine_method( + reference_positions, + configuration_positions, + max_cutoff, + min_cutoff=min_cutoff, + box=box, + method=method, + ) + return function( + reference, + configuration, + max_cutoff, + min_cutoff=min_cutoff, + box=box, + return_distances=return_distances, + ) + + +def _determine_method( + reference: npt.NDArray, + configuration: npt.NDArray, + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + method: Optional[str] = None, +) -> Callable: """Guesses the fastest method for capped distance calculations based on the size of the coordinate sets and the relative size of the target volume. @@ -554,46 +623,57 @@ def _determine_method(reference: npt.NDArray, configuration: npt.NDArray, .. versionchanged:: 1.1.0 enabled nsgrid again """ - methods = {'bruteforce': _bruteforce_capped, - 'pkdtree': _pkdtree_capped, - 'nsgrid': _nsgrid_capped, + methods = { + "bruteforce": _bruteforce_capped, + "pkdtree": _pkdtree_capped, + "nsgrid": _nsgrid_capped, } if method is not None: return methods[method.lower()] if len(reference) < 10 or len(configuration) < 10: - return methods['bruteforce'] + return methods["bruteforce"] elif len(reference) * len(configuration) >= 1e8: # CAUTION : for large datasets, shouldnt go into 'bruteforce' # in any case. Arbitrary number, but can be characterized - return methods['nsgrid'] + return methods["nsgrid"] else: if box is None: - min_dim = np.array([reference.min(axis=0), - configuration.min(axis=0)]) - max_dim = np.array([reference.max(axis=0), - configuration.max(axis=0)]) + min_dim = np.array( + [reference.min(axis=0), configuration.min(axis=0)] + ) + max_dim = np.array( + [reference.max(axis=0), configuration.max(axis=0)] + ) size = max_dim.max(axis=0) - min_dim.min(axis=0) elif np.all(box[3:] == 90.0): size = box[:3] else: tribox = triclinic_vectors(box) size = tribox.max(axis=0) - tribox.min(axis=0) - if np.any(max_cutoff > 0.3*size): - return methods['bruteforce'] + if np.any(max_cutoff > 0.3 * size): + return methods["bruteforce"] else: - return methods['nsgrid'] - - -@check_coords('reference', 'configuration', enforce_copy=False, - reduce_result_if_single=False, check_lengths_match=False, - allow_atomgroup=True) -def _bruteforce_capped(reference: Union[npt.NDArray, 'AtomGroup'], - configuration: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - return_distances: Optional[bool] = True): + return methods["nsgrid"] + + +@check_coords( + "reference", + "configuration", + enforce_copy=False, + reduce_result_if_single=False, + check_lengths_match=False, + allow_atomgroup=True, +) +def _bruteforce_capped( + reference: Union[npt.NDArray, "AtomGroup"], + configuration: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + return_distances: Optional[bool] = True, +): """Capped distance evaluations using a brute force method. Computes and returns an array containing pairs of indices corresponding to @@ -658,8 +738,9 @@ def _bruteforce_capped(reference: Union[npt.NDArray, 'AtomGroup'], if len(reference) > 0 and len(configuration) > 0: _distances = distance_array(reference, configuration, box=box) if min_cutoff is not None: - mask = np.where((_distances <= max_cutoff) & \ - (_distances > min_cutoff)) + mask = np.where( + (_distances <= max_cutoff) & (_distances > min_cutoff) + ) else: mask = np.where((_distances <= max_cutoff)) if mask[0].size > 0: @@ -673,14 +754,22 @@ def _bruteforce_capped(reference: Union[npt.NDArray, 'AtomGroup'], return pairs -@check_coords('reference', 'configuration', enforce_copy=False, - reduce_result_if_single=False, check_lengths_match=False, - allow_atomgroup=True) -def _pkdtree_capped(reference: Union[npt.NDArray, 'AtomGroup'], - configuration: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - return_distances: Optional[bool] = True): +@check_coords( + "reference", + "configuration", + enforce_copy=False, + reduce_result_if_single=False, + check_lengths_match=False, + allow_atomgroup=True, +) +def _pkdtree_capped( + reference: Union[npt.NDArray, "AtomGroup"], + configuration: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + return_distances: Optional[bool] = True, +): """Capped distance evaluations using a KDtree method. Computes and returns an array containing pairs of indices corresponding to @@ -738,7 +827,9 @@ def _pkdtree_capped(reference: Union[npt.NDArray, 'AtomGroup'], Can now accept an :class:`~MDAnalysis.core.groups.AtomGroup` as an argument in any position and checks inputs using type hinting. """ - from .pkdtree import PeriodicKDTree # must be here to avoid circular import + from .pkdtree import ( + PeriodicKDTree, + ) # must be here to avoid circular import # Default return values (will be overwritten only if pairs are found): pairs = np.empty((0, 2), dtype=np.intp) @@ -751,10 +842,11 @@ def _pkdtree_capped(reference: Union[npt.NDArray, 'AtomGroup'], _pairs = kdtree.search_tree(reference, max_cutoff) if _pairs.size > 0: pairs = _pairs - if (return_distances or (min_cutoff is not None)): + if return_distances or (min_cutoff is not None): refA, refB = pairs[:, 0], pairs[:, 1] - distances = calc_bonds(reference[refA], configuration[refB], - box=box) + distances = calc_bonds( + reference[refA], configuration[refB], box=box + ) if min_cutoff is not None: mask = np.where(distances > min_cutoff) pairs, distances = pairs[mask], distances[mask] @@ -765,14 +857,22 @@ def _pkdtree_capped(reference: Union[npt.NDArray, 'AtomGroup'], return pairs -@check_coords('reference', 'configuration', enforce_copy=False, - reduce_result_if_single=False, check_lengths_match=False, - allow_atomgroup=True) -def _nsgrid_capped(reference: Union[npt.NDArray, 'AtomGroup'], - configuration: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - return_distances: Optional[bool] = True): +@check_coords( + "reference", + "configuration", + enforce_copy=False, + reduce_result_if_single=False, + check_lengths_match=False, + allow_atomgroup=True, +) +def _nsgrid_capped( + reference: Union[npt.NDArray, "AtomGroup"], + configuration: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + return_distances: Optional[bool] = True, +): """Capped distance evaluations using a grid-based search method. Computes and returns an array containing pairs of indices corresponding to @@ -845,17 +945,19 @@ def _nsgrid_capped(reference: Union[npt.NDArray, 'AtomGroup'], lmax = all_coords.max(axis=0) lmin = all_coords.min(axis=0) # Using maximum dimension as the box size - boxsize = (lmax-lmin).max() + boxsize = (lmax - lmin).max() # to avoid failures for very close particles but with # larger cutoff boxsize = np.maximum(boxsize, 2 * max_cutoff) - pseudobox[:3] = boxsize + 2.2*max_cutoff - pseudobox[3:] = 90. + pseudobox[:3] = boxsize + 2.2 * max_cutoff + pseudobox[3:] = 90.0 shiftref, shiftconf = reference.copy(), configuration.copy() # Extra padding near the origin - shiftref -= lmin - 0.1*max_cutoff - shiftconf -= lmin - 0.1*max_cutoff - gridsearch = FastNS(max_cutoff, shiftconf, box=pseudobox, pbc=False) + shiftref -= lmin - 0.1 * max_cutoff + shiftconf -= lmin - 0.1 * max_cutoff + gridsearch = FastNS( + max_cutoff, shiftconf, box=pseudobox, pbc=False + ) results = gridsearch.search(shiftref) else: gridsearch = FastNS(max_cutoff, configuration, box=box) @@ -874,15 +976,21 @@ def _nsgrid_capped(reference: Union[npt.NDArray, 'AtomGroup'], return pairs -@check_coords('reference', enforce_copy=False, - reduce_result_if_single=False, check_lengths_match=False, - allow_atomgroup=True) -def self_capped_distance(reference: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, - min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - method: Optional[str] = None, - return_distances: Optional[bool] = True): +@check_coords( + "reference", + enforce_copy=False, + reduce_result_if_single=False, + check_lengths_match=False, + allow_atomgroup=True, +) +def self_capped_distance( + reference: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + method: Optional[str] = None, + return_distances: Optional[bool] = True, +): """Calculates pairs of indices corresponding to entries in the `reference` array which are separated by a distance lying within the specified cutoff(s). Optionally, these distances can be returned as well. @@ -968,24 +1076,38 @@ def self_capped_distance(reference: Union[npt.NDArray, 'AtomGroup'], if box is not None: box = np.asarray(box, dtype=np.float32) if box.shape[0] != 6: - raise ValueError("Box Argument is of incompatible type. The " - "dimension should be either None or of the form " - "[lx, ly, lz, alpha, beta, gamma]") + raise ValueError( + "Box Argument is of incompatible type. The " + "dimension should be either None or of the form " + "[lx, ly, lz, alpha, beta, gamma]" + ) # The check_coords decorator made sure that reference is an # array of positions. Mypy does not know about that so we have to # tell it. reference_positions: npt.NDArray = reference # type: ignore - function = _determine_method_self(reference_positions, - max_cutoff, min_cutoff=min_cutoff, - box=box, method=method) - return function(reference, max_cutoff, min_cutoff=min_cutoff, box=box, - return_distances=return_distances) - - -def _determine_method_self(reference: npt.NDArray, max_cutoff: float, - min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - method: Optional[str] = None): + function = _determine_method_self( + reference_positions, + max_cutoff, + min_cutoff=min_cutoff, + box=box, + method=method, + ) + return function( + reference, + max_cutoff, + min_cutoff=min_cutoff, + box=box, + return_distances=return_distances, + ) + + +def _determine_method_self( + reference: npt.NDArray, + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + method: Optional[str] = None, +): """Guesses the fastest method for capped distance calculations based on the size of the `reference` coordinate set and the relative size of the target volume. @@ -1020,16 +1142,17 @@ def _determine_method_self(reference: npt.NDArray, max_cutoff: float, .. versionchanged:: 1.0.2 enabled nsgrid again """ - methods = {'bruteforce': _bruteforce_capped_self, - 'pkdtree': _pkdtree_capped_self, - 'nsgrid': _nsgrid_capped_self, + methods = { + "bruteforce": _bruteforce_capped_self, + "pkdtree": _pkdtree_capped_self, + "nsgrid": _nsgrid_capped_self, } if method is not None: return methods[method.lower()] if len(reference) < 100: - return methods['bruteforce'] + return methods["bruteforce"] if box is None: min_dim = np.array([reference.min(axis=0)]) @@ -1041,19 +1164,25 @@ def _determine_method_self(reference: npt.NDArray, max_cutoff: float, tribox = triclinic_vectors(box) size = tribox.max(axis=0) - tribox.min(axis=0) - if max_cutoff < 0.03*size.min(): - return methods['pkdtree'] + if max_cutoff < 0.03 * size.min(): + return methods["pkdtree"] else: - return methods['nsgrid'] - - -@check_coords('reference', enforce_copy=False, reduce_result_if_single=False, - allow_atomgroup=True) -def _bruteforce_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, - min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - return_distances: Optional[bool] = True): + return methods["nsgrid"] + + +@check_coords( + "reference", + enforce_copy=False, + reduce_result_if_single=False, + allow_atomgroup=True, +) +def _bruteforce_capped_self( + reference: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + return_distances: Optional[bool] = True, +): """Capped distance evaluations using a brute force method. Computes and returns an array containing pairs of indices corresponding to @@ -1130,13 +1259,19 @@ def _bruteforce_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], return pairs -@check_coords('reference', enforce_copy=False, reduce_result_if_single=False, - allow_atomgroup=True) -def _pkdtree_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, - min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - return_distances: Optional[bool] = True): +@check_coords( + "reference", + enforce_copy=False, + reduce_result_if_single=False, + allow_atomgroup=True, +) +def _pkdtree_capped_self( + reference: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + return_distances: Optional[bool] = True, +): """Capped distance evaluations using a KDtree method. Computes and returns an array containing pairs of indices corresponding to @@ -1188,7 +1323,9 @@ def _pkdtree_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], Can now accept an :class:`~MDAnalysis.core.groups.AtomGroup` as an argument in any position and checks inputs using type hinting. """ - from .pkdtree import PeriodicKDTree # must be here to avoid circular import + from .pkdtree import ( + PeriodicKDTree, + ) # must be here to avoid circular import # Default return values (will be overwritten only if pairs are found): pairs = np.empty((0, 2), dtype=np.intp) @@ -1203,9 +1340,11 @@ def _pkdtree_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], _pairs = kdtree.search_pairs(max_cutoff) if _pairs.size > 0: pairs = _pairs - if (return_distances or (min_cutoff is not None)): + if return_distances or (min_cutoff is not None): refA, refB = pairs[:, 0], pairs[:, 1] - distances = calc_bonds(reference[refA], reference[refB], box=box) + distances = calc_bonds( + reference[refA], reference[refB], box=box + ) if min_cutoff is not None: idx = distances > min_cutoff pairs, distances = pairs[idx], distances[idx] @@ -1214,13 +1353,19 @@ def _pkdtree_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], return pairs -@check_coords('reference', enforce_copy=False, reduce_result_if_single=False, - allow_atomgroup=True) -def _nsgrid_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], - max_cutoff: float, - min_cutoff: Optional[float] = None, - box: Optional[npt.NDArray] = None, - return_distances: Optional[bool] = True): +@check_coords( + "reference", + enforce_copy=False, + reduce_result_if_single=False, + allow_atomgroup=True, +) +def _nsgrid_capped_self( + reference: Union[npt.NDArray, "AtomGroup"], + max_cutoff: float, + min_cutoff: Optional[float] = None, + box: Optional[npt.NDArray] = None, + return_distances: Optional[bool] = True, +): """Capped distance evaluations using a grid-based search method. Computes and returns an array containing pairs of indices corresponding to @@ -1286,19 +1431,19 @@ def _nsgrid_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], lmax = reference.max(axis=0) lmin = reference.min(axis=0) # Using maximum dimension as the box size - boxsize = (lmax-lmin).max() + boxsize = (lmax - lmin).max() # to avoid failures of very close particles # but with larger cutoff - if boxsize < 2*max_cutoff: + if boxsize < 2 * max_cutoff: # just enough box size so that NSGrid doesnot fails - sizefactor = 2.2*max_cutoff/boxsize + sizefactor = 2.2 * max_cutoff / boxsize else: sizefactor = 1.2 - pseudobox[:3] = sizefactor*boxsize - pseudobox[3:] = 90. + pseudobox[:3] = sizefactor * boxsize + pseudobox[3:] = 90.0 shiftref = reference.copy() # Extra padding near the origin - shiftref -= lmin - 0.1*boxsize + shiftref -= lmin - 0.1 * boxsize gridsearch = FastNS(max_cutoff, shiftref, box=pseudobox, pbc=False) results = gridsearch.self_search() else: @@ -1317,7 +1462,7 @@ def _nsgrid_capped_self(reference: Union[npt.NDArray, 'AtomGroup'], return pairs -@check_coords('coords') +@check_coords("coords") def transform_RtoS(coords, box, backend="serial"): """Transform an array of coordinates from real space to S space (a.k.a. lambda space) @@ -1354,20 +1499,20 @@ def transform_RtoS(coords, box, backend="serial"): if len(coords) == 0: return coords boxtype, box = check_box(box) - if boxtype == 'ortho': + if boxtype == "ortho": box = np.diag(box) box = box.astype(np.float64) # Create inverse matrix of box # need order C here - inv = np.array(np.linalg.inv(box), order='C') + inv = np.array(np.linalg.inv(box), order="C") _run("coord_transform", args=(coords, inv), backend=backend) return coords -@check_coords('coords') +@check_coords("coords") def transform_StoR(coords, box, backend="serial"): """Transform an array of coordinates from S space into real space. @@ -1403,7 +1548,7 @@ def transform_StoR(coords, box, backend="serial"): if len(coords) == 0: return coords boxtype, box = check_box(box) - if boxtype == 'ortho': + if boxtype == "ortho": box = np.diag(box) box = box.astype(np.float64) @@ -1411,12 +1556,14 @@ def transform_StoR(coords, box, backend="serial"): return coords -@check_coords('coords1', 'coords2', allow_atomgroup=True) -def calc_bonds(coords1: Union[npt.NDArray, 'AtomGroup'], - coords2: Union[npt.NDArray, 'AtomGroup'], - box: Optional[npt.NDArray] = None, - result: Optional[npt.NDArray] = None, - backend: str = "serial") -> npt.NDArray: +@check_coords("coords1", "coords2", allow_atomgroup=True) +def calc_bonds( + coords1: Union[npt.NDArray, "AtomGroup"], + coords2: Union[npt.NDArray, "AtomGroup"], + box: Optional[npt.NDArray] = None, + result: Optional[npt.NDArray] = None, + backend: str = "serial", +) -> npt.NDArray: """Calculates the bond lengths between pairs of atom positions from the two coordinate arrays `coords1` and `coords2`, which must contain the same number of coordinates. ``coords1[i]`` and ``coords2[i]`` represent the @@ -1489,7 +1636,7 @@ def calc_bonds(coords1: Union[npt.NDArray, 'AtomGroup'], if box is not None: boxtype, box = check_box(box) if boxtype == "ortho": - if backend == 'distopia': + if backend == "distopia": bondlengths = bondlengths.astype(np.float32) _run( "calc_bond_distance_ortho", @@ -1503,25 +1650,27 @@ def calc_bonds(coords1: Union[npt.NDArray, 'AtomGroup'], backend=backend, ) else: - if backend == 'distopia': + if backend == "distopia": bondlengths = bondlengths.astype(np.float32) _run( "calc_bond_distance", args=(coords1, coords2, bondlengths), backend=backend, ) - if backend == 'distopia': + if backend == "distopia": bondlengths = bondlengths.astype(np.float64) return bondlengths -@check_coords('coords1', 'coords2', 'coords3', allow_atomgroup=True) -def calc_angles(coords1: Union[npt.NDArray, 'AtomGroup'], - coords2: Union[npt.NDArray, 'AtomGroup'], - coords3: Union[npt.NDArray, 'AtomGroup'], - box: Optional[npt.NDArray] = None, - result: Optional[npt.NDArray] = None, - backend: str = "serial") -> npt.NDArray: +@check_coords("coords1", "coords2", "coords3", allow_atomgroup=True) +def calc_angles( + coords1: Union[npt.NDArray, "AtomGroup"], + coords2: Union[npt.NDArray, "AtomGroup"], + coords3: Union[npt.NDArray, "AtomGroup"], + box: Optional[npt.NDArray] = None, + result: Optional[npt.NDArray] = None, + backend: str = "serial", +) -> npt.NDArray: """Calculates the angles formed between triplets of atom positions from the three coordinate arrays `coords1`, `coords2`, and `coords3`. All coordinate arrays must contain the same number of coordinates. @@ -1601,30 +1750,38 @@ def calc_angles(coords1: Union[npt.NDArray, 'AtomGroup'], if numatom > 0: if box is not None: boxtype, box = check_box(box) - if boxtype == 'ortho': - _run("calc_angle_ortho", - args=(coords1, coords2, coords3, box, angles), - backend=backend) + if boxtype == "ortho": + _run( + "calc_angle_ortho", + args=(coords1, coords2, coords3, box, angles), + backend=backend, + ) else: - _run("calc_angle_triclinic", - args=(coords1, coords2, coords3, box, angles), - backend=backend) + _run( + "calc_angle_triclinic", + args=(coords1, coords2, coords3, box, angles), + backend=backend, + ) else: - _run("calc_angle", - args=(coords1, coords2, coords3, angles), - backend=backend) + _run( + "calc_angle", + args=(coords1, coords2, coords3, angles), + backend=backend, + ) return angles -@check_coords('coords1', 'coords2', 'coords3', 'coords4', allow_atomgroup=True) -def calc_dihedrals(coords1: Union[npt.NDArray, 'AtomGroup'], - coords2: Union[npt.NDArray, 'AtomGroup'], - coords3: Union[npt.NDArray, 'AtomGroup'], - coords4: Union[npt.NDArray, 'AtomGroup'], - box: Optional[npt.NDArray] = None, - result: Optional[npt.NDArray] = None, - backend: str = "serial") -> npt.NDArray: +@check_coords("coords1", "coords2", "coords3", "coords4", allow_atomgroup=True) +def calc_dihedrals( + coords1: Union[npt.NDArray, "AtomGroup"], + coords2: Union[npt.NDArray, "AtomGroup"], + coords3: Union[npt.NDArray, "AtomGroup"], + coords4: Union[npt.NDArray, "AtomGroup"], + box: Optional[npt.NDArray] = None, + result: Optional[npt.NDArray] = None, + backend: str = "serial", +) -> npt.NDArray: r"""Calculates the dihedral angles formed between quadruplets of positions from the four coordinate arrays `coords1`, `coords2`, `coords3`, and `coords4`, which must contain the same number of coordinates. @@ -1694,7 +1851,7 @@ def calc_dihedrals(coords1: Union[npt.NDArray, 'AtomGroup'], Array containing the dihedral angles formed by each quadruplet of coordinates. Values are returned in radians (rad). If four single coordinates were supplied, the dihedral angle is returned as a single - number instead of an array. The range of dihedral angle is + number instead of an array. The range of dihedral angle is :math:`(-\pi, \pi)`. @@ -1719,26 +1876,34 @@ def calc_dihedrals(coords1: Union[npt.NDArray, 'AtomGroup'], if numatom > 0: if box is not None: boxtype, box = check_box(box) - if boxtype == 'ortho': - _run("calc_dihedral_ortho", - args=(coords1, coords2, coords3, coords4, box, dihedrals), - backend=backend) + if boxtype == "ortho": + _run( + "calc_dihedral_ortho", + args=(coords1, coords2, coords3, coords4, box, dihedrals), + backend=backend, + ) else: - _run("calc_dihedral_triclinic", - args=(coords1, coords2, coords3, coords4, box, dihedrals), - backend=backend) + _run( + "calc_dihedral_triclinic", + args=(coords1, coords2, coords3, coords4, box, dihedrals), + backend=backend, + ) else: - _run("calc_dihedral", - args=(coords1, coords2, coords3, coords4, dihedrals), - backend=backend) + _run( + "calc_dihedral", + args=(coords1, coords2, coords3, coords4, dihedrals), + backend=backend, + ) return dihedrals -@check_coords('coords', allow_atomgroup=True) -def apply_PBC(coords: Union[npt.NDArray, 'AtomGroup'], - box: Optional[npt.NDArray] = None, - backend: str = "serial") -> npt.NDArray: +@check_coords("coords", allow_atomgroup=True) +def apply_PBC( + coords: Union[npt.NDArray, "AtomGroup"], + box: Optional[npt.NDArray] = None, + backend: str = "serial", +) -> npt.NDArray: """Moves coordinates into the primary unit cell. Parameters @@ -1779,7 +1944,7 @@ def apply_PBC(coords: Union[npt.NDArray, 'AtomGroup'], if len(coords_array) == 0: return coords_array boxtype, box = check_box(box) - if boxtype == 'ortho': + if boxtype == "ortho": _run("ortho_pbc", args=(coords_array, box), backend=backend) else: _run("triclinic_pbc", args=(coords_array, box), backend=backend) @@ -1787,7 +1952,7 @@ def apply_PBC(coords: Union[npt.NDArray, 'AtomGroup'], return coords_array -@check_coords('vectors', enforce_copy=False, enforce_dtype=False) +@check_coords("vectors", enforce_copy=False, enforce_dtype=False) def minimize_vectors(vectors: npt.NDArray, box: npt.NDArray) -> npt.NDArray: """Apply minimum image convention to an array of vectors @@ -1822,7 +1987,7 @@ def minimize_vectors(vectors: npt.NDArray, box: npt.NDArray) -> npt.NDArray: # use box which is same precision as input vectors box = box.astype(vectors.dtype) - if boxtype == 'ortho': + if boxtype == "ortho": _minimize_vectors_ortho(vectors, box, output) else: _minimize_vectors_triclinic(vectors, box.ravel(), output) diff --git a/package/MDAnalysis/lib/formats/__init__.py b/package/MDAnalysis/lib/formats/__init__.py index cf484ea4778..2760c495d6b 100644 --- a/package/MDAnalysis/lib/formats/__init__.py +++ b/package/MDAnalysis/lib/formats/__init__.py @@ -23,4 +23,4 @@ from . import libmdaxdr from . import libdcd -__all__ = ['libmdaxdr', 'libdcd'] +__all__ = ["libmdaxdr", "libdcd"] diff --git a/package/MDAnalysis/lib/log.py b/package/MDAnalysis/lib/log.py index 15100ef4884..d63ec547828 100644 --- a/package/MDAnalysis/lib/log.py +++ b/package/MDAnalysis/lib/log.py @@ -101,7 +101,8 @@ def start_logging(logfile="MDAnalysis.log", version=version.__version__): """ create("MDAnalysis", logfile=logfile) logging.getLogger("MDAnalysis").info( - "MDAnalysis %s STARTED logging to %r", version, logfile) + "MDAnalysis %s STARTED logging to %r", version, logfile + ) def stop_logging(): @@ -136,7 +137,8 @@ def create(logger_name="MDAnalysis", logfile="MDAnalysis.log"): # handler that writes to logfile logfile_handler = logging.FileHandler(logfile) logfile_formatter = logging.Formatter( - '%(asctime)s %(name)-12s %(levelname)-8s %(message)s') + "%(asctime)s %(name)-12s %(levelname)-8s %(message)s" + ) logfile_handler.setFormatter(logfile_formatter) logger.addHandler(logfile_handler) @@ -144,7 +146,7 @@ def create(logger_name="MDAnalysis", logfile="MDAnalysis.log"): console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # set a format which is simpler for console use - formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') + formatter = logging.Formatter("%(name)-12s: %(levelname)-8s %(message)s") console_handler.setFormatter(formatter) logger.addHandler(console_handler) @@ -334,11 +336,11 @@ def __init__(self, *args, **kwargs): """""" # ^^^^ keep the empty doc string to avoid Sphinx doc errors with the # original doc string from tqdm.auto.tqdm - verbose = kwargs.pop('verbose', True) + verbose = kwargs.pop("verbose", True) # disable: Whether to disable the entire progressbar wrapper [default: False]. # If set to None, disable on non-TTY. # disable should be the opposite of verbose unless it's None disable = verbose if verbose is None else not verbose # disable should take precedence over verbose if both are set - kwargs['disable'] = kwargs.pop('disable', disable) + kwargs["disable"] = kwargs.pop("disable", disable) super(ProgressBar, self).__init__(*args, **kwargs) diff --git a/package/MDAnalysis/lib/mdamath.py b/package/MDAnalysis/lib/mdamath.py index e904116a1a5..cef449c8f20 100644 --- a/package/MDAnalysis/lib/mdamath.py +++ b/package/MDAnalysis/lib/mdamath.py @@ -61,8 +61,12 @@ from ..exceptions import NoDataError from . import util -from ._cutil import (make_whole, find_fragments, _sarrus_det_single, - _sarrus_det_multiple) +from ._cutil import ( + make_whole, + find_fragments, + _sarrus_det_single, + _sarrus_det_multiple, +) import numpy.typing as npt from typing import Union @@ -127,7 +131,7 @@ def pdot(a: npt.NDArray, b: npt.NDArray) -> npt.NDArray: ------- :class:`numpy.ndarray` of shape (N,) """ - return np.einsum('ij,ij->i', a, b) + return np.einsum("ij,ij->i", a, b) def pnorm(a: npt.NDArray) -> npt.NDArray: @@ -141,7 +145,7 @@ def pnorm(a: npt.NDArray) -> npt.NDArray: ------- :class:`numpy.ndarray` of shape (N,) """ - return pdot(a, a)**0.5 + return pdot(a, a) ** 0.5 def angle(a: npt.ArrayLike, b: npt.ArrayLike) -> float: @@ -159,7 +163,9 @@ def angle(a: npt.ArrayLike, b: npt.ArrayLike) -> float: return np.arccos(x) -def stp(vec1: npt.ArrayLike, vec2: npt.ArrayLike, vec3: npt.ArrayLike) -> float: +def stp( + vec1: npt.ArrayLike, vec2: npt.ArrayLike, vec3: npt.ArrayLike +) -> float: r"""Takes the scalar triple product of three vectors. Returns the volume *V* of the parallel epiped spanned by the three @@ -195,7 +201,7 @@ def dihedral(ab: npt.ArrayLike, bc: npt.ArrayLike, cd: npt.ArrayLike) -> float: Moved into lib.mdamath """ x = angle(normal(ab, bc), normal(bc, cd)) - return (x if stp(ab, bc, cd) <= 0.0 else -x) + return x if stp(ab, bc, cd) <= 0.0 else -x def sarrus_det(matrix: npt.NDArray) -> Union[float, npt.NDArray]: @@ -236,14 +242,18 @@ def sarrus_det(matrix: npt.NDArray) -> Union[float, npt.NDArray]: shape = m.shape ndim = m.ndim if ndim < 2 or shape[-2:] != (3, 3): - raise ValueError("Invalid matrix shape: must be (3, 3) or (..., 3, 3), " - "got {}.".format(shape)) + raise ValueError( + "Invalid matrix shape: must be (3, 3) or (..., 3, 3), " + "got {}.".format(shape) + ) if ndim == 2: return _sarrus_det_single(m) return _sarrus_det_multiple(m.reshape((-1, 3, 3))).reshape(shape[:-2]) -def triclinic_box(x: npt.ArrayLike, y: npt.ArrayLike, z: npt.ArrayLike) -> npt.NDArray: +def triclinic_box( + x: npt.ArrayLike, y: npt.ArrayLike, z: npt.ArrayLike +) -> npt.NDArray: """Convert the three triclinic box vectors to ``[lx, ly, lz, alpha, beta, gamma]``. @@ -306,8 +316,9 @@ def triclinic_box(x: npt.ArrayLike, y: npt.ArrayLike, z: npt.ArrayLike) -> npt.N return np.zeros(6, dtype=np.float32) -def triclinic_vectors(dimensions: npt.ArrayLike, - dtype: npt.DTypeLike = np.float32) -> npt.NDArray: +def triclinic_vectors( + dimensions: npt.ArrayLike, dtype: npt.DTypeLike = np.float32 +) -> npt.NDArray: """Convert ``[lx, ly, lz, alpha, beta, gamma]`` to a triclinic matrix representation. @@ -357,8 +368,9 @@ def triclinic_vectors(dimensions: npt.ArrayLike, dim = np.asarray(dimensions, dtype=np.float64) lx, ly, lz, alpha, beta, gamma = dim # Only positive edge lengths and angles in (0, 180) are allowed: - if not (np.all(dim > 0.0) and - alpha < 180.0 and beta < 180.0 and gamma < 180.0): + if not ( + np.all(dim > 0.0) and alpha < 180.0 and beta < 180.0 and gamma < 180.0 + ): # invalid box, return zero vectors: box_matrix = np.zeros((3, 3), dtype=dtype) # detect orthogonal boxes: @@ -389,8 +401,9 @@ def triclinic_vectors(dimensions: npt.ArrayLike, box_matrix[1, 1] = ly * sin_gamma box_matrix[2, 0] = lz * cos_beta box_matrix[2, 1] = lz * (cos_alpha - cos_beta * cos_gamma) / sin_gamma - box_matrix[2, 2] = np.sqrt(lz * lz - box_matrix[2, 0] ** 2 - - box_matrix[2, 1] ** 2) + box_matrix[2, 2] = np.sqrt( + lz * lz - box_matrix[2, 0] ** 2 - box_matrix[2, 1] ** 2 + ) # The discriminant of the above square root is only negative or zero for # triplets of box angles that lead to an invalid box (i.e., the sum of # any two angles is less than or equal to the third). diff --git a/package/MDAnalysis/lib/picklable_file_io.py b/package/MDAnalysis/lib/picklable_file_io.py index e27bca4b779..f8050b14e51 100644 --- a/package/MDAnalysis/lib/picklable_file_io.py +++ b/package/MDAnalysis/lib/picklable_file_io.py @@ -107,28 +107,30 @@ class FileIOPicklable(io.FileIO): .. versionadded:: 2.0.0 """ - def __init__(self, name, mode='r'): + + def __init__(self, name, mode="r"): self._mode = mode super().__init__(name, mode) - def __setstate__(self, state): name = state["name_val"] - self.__init__(name, mode='r') + self.__init__(name, mode="r") try: self.seek(state["tell_val"]) except KeyError: pass - def __reduce_ex__(self, prot): - if self._mode != 'r': - raise RuntimeError("Can only pickle files that were opened " - "in read mode, not {}".format(self._mode)) - return (self.__class__, - (self.name, self._mode), - {"name_val": self.name, - "tell_val": self.tell()}) + if self._mode != "r": + raise RuntimeError( + "Can only pickle files that were opened " + "in read mode, not {}".format(self._mode) + ) + return ( + self.__class__, + (self.name, self._mode), + {"name_val": self.name, "tell_val": self.tell()}, + ) class BufferIOPicklable(io.BufferedReader): @@ -157,11 +159,11 @@ class BufferIOPicklable(io.BufferedReader): .. versionadded:: 2.0.0 """ + def __init__(self, raw): super().__init__(raw) self.raw_class = raw.__class__ - def __setstate__(self, state): raw_class = state["raw_class"] name = state["name_val"] @@ -172,11 +174,15 @@ def __setstate__(self, state): def __reduce_ex__(self, prot): # don't ask, for Python 3.12+ see: # https://github.com/python/cpython/pull/104370 - return (self.raw_class, - (self.name,), - {"raw_class": self.raw_class, - "name_val": self.name, - "tell_val": self.tell()}) + return ( + self.raw_class, + (self.name,), + { + "raw_class": self.raw_class, + "name_val": self.name, + "tell_val": self.tell(), + }, + ) class TextIOPicklable(io.TextIOWrapper): @@ -210,6 +216,7 @@ class TextIOPicklable(io.TextIOWrapper): so `universe.trajectory[i]` is not needed to seek to the original position. """ + def __init__(self, raw): super().__init__(raw) self.raw_class = raw.__class__ @@ -236,11 +243,15 @@ def __reduce_ex__(self, prot): except AttributeError: # This is kind of ugly--BZ2File does not save its name. name = self.buffer._fp.name - return (self.__class__.__new__, - (self.__class__,), - {"raw_class": self.raw_class, - "name_val": name, - "tell_val": curr_loc}) + return ( + self.__class__.__new__, + (self.__class__,), + { + "raw_class": self.raw_class, + "name_val": name, + "tell_val": curr_loc, + }, + ) class BZ2Picklable(bz2.BZ2File): @@ -292,14 +303,17 @@ class BZ2Picklable(bz2.BZ2File): .. versionadded:: 2.0.0 """ - def __init__(self, name, mode='rb'): + + def __init__(self, name, mode="rb"): self._bz_mode = mode super().__init__(name, mode) def __getstate__(self): - if not self._bz_mode.startswith('r'): - raise RuntimeError("Can only pickle files that were opened " - "in read mode, not {}".format(self._bz_mode)) + if not self._bz_mode.startswith("r"): + raise RuntimeError( + "Can only pickle files that were opened " + "in read mode, not {}".format(self._bz_mode) + ) return {"name_val": self._fp.name, "tell_val": self.tell()} def __setstate__(self, args): @@ -361,16 +375,18 @@ class GzipPicklable(gzip.GzipFile): .. versionadded:: 2.0.0 """ - def __init__(self, name, mode='rb'): + + def __init__(self, name, mode="rb"): self._gz_mode = mode super().__init__(name, mode) def __getstate__(self): - if not self._gz_mode.startswith('r'): - raise RuntimeError("Can only pickle files that were opened " - "in read mode, not {}".format(self._gz_mode)) - return {"name_val": self.name, - "tell_val": self.tell()} + if not self._gz_mode.startswith("r"): + raise RuntimeError( + "Can only pickle files that were opened " + "in read mode, not {}".format(self._gz_mode) + ) + return {"name_val": self.name, "tell_val": self.tell()} def __setstate__(self, args): name = args["name_val"] @@ -382,7 +398,7 @@ def __setstate__(self, args): pass -def pickle_open(name, mode='rt'): +def pickle_open(name, mode="rt"): """Open file and return a stream with pickle function implemented. This function returns a FileIOPicklable object wrapped in a @@ -443,18 +459,19 @@ def pickle_open(name, mode='rt'): .. versionadded:: 2.0.0 """ - if mode not in {'r', 'rt', 'rb'}: - raise ValueError("Only read mode ('r', 'rt', 'rb') " - "files can be pickled.") + if mode not in {"r", "rt", "rb"}: + raise ValueError( + "Only read mode ('r', 'rt', 'rb') " "files can be pickled." + ) name = os.fspath(name) raw = FileIOPicklable(name) - if mode == 'rb': + if mode == "rb": return BufferIOPicklable(raw) - elif mode in {'r', 'rt'}: + elif mode in {"r", "rt"}: return TextIOPicklable(raw) -def bz2_pickle_open(name, mode='rb'): +def bz2_pickle_open(name, mode="rb"): """Open a bzip2-compressed file in binary or text mode with pickle function implemented. @@ -515,9 +532,10 @@ def bz2_pickle_open(name, mode='rb'): .. versionadded:: 2.0.0 """ - if mode not in {'r', 'rt', 'rb'}: - raise ValueError("Only read mode ('r', 'rt', 'rb') " - "files can be pickled.") + if mode not in {"r", "rt", "rb"}: + raise ValueError( + "Only read mode ('r', 'rt', 'rb') " "files can be pickled." + ) bz_mode = mode.replace("t", "") binary_file = BZ2Picklable(name, bz_mode) if "t" in mode: @@ -526,7 +544,7 @@ def bz2_pickle_open(name, mode='rb'): return binary_file -def gzip_pickle_open(name, mode='rb'): +def gzip_pickle_open(name, mode="rb"): """Open a gzip-compressed file in binary or text mode with pickle function implemented. @@ -587,9 +605,10 @@ def gzip_pickle_open(name, mode='rb'): .. versionadded:: 2.0.0 """ - if mode not in {'r', 'rt', 'rb'}: - raise ValueError("Only read mode ('r', 'rt', 'rb') " - "files can be pickled.") + if mode not in {"r", "rt", "rb"}: + raise ValueError( + "Only read mode ('r', 'rt', 'rb') " "files can be pickled." + ) gz_mode = mode.replace("t", "") binary_file = GzipPicklable(name, gz_mode) if "t" in mode: diff --git a/package/MDAnalysis/lib/pkdtree.py b/package/MDAnalysis/lib/pkdtree.py index f50d16da9f8..952b4672e32 100644 --- a/package/MDAnalysis/lib/pkdtree.py +++ b/package/MDAnalysis/lib/pkdtree.py @@ -40,9 +40,7 @@ import numpy.typing as npt from typing import Optional, ClassVar -__all__ = [ - 'PeriodicKDTree' -] +__all__ = ["PeriodicKDTree"] class PeriodicKDTree(object): @@ -64,7 +62,9 @@ class PeriodicKDTree(object): """ - def __init__(self, box: Optional[npt.ArrayLike] = None, leafsize: int = 10) -> None: + def __init__( + self, box: Optional[npt.ArrayLike] = None, leafsize: int = 10 + ) -> None: """ Parameters @@ -98,7 +98,9 @@ def pbc(self): """ return self.box is not None - def set_coords(self, coords: npt.ArrayLike, cutoff: Optional[float] = None) -> None: + def set_coords( + self, coords: npt.ArrayLike, cutoff: Optional[float] = None + ) -> None: """Constructs KDTree from the coordinates Wrapping of coordinates to the primary unit cell is enforced @@ -138,23 +140,26 @@ def set_coords(self, coords: npt.ArrayLike, cutoff: Optional[float] = None) -> N if self.pbc: self.cutoff = cutoff if cutoff is None: - raise RuntimeError('Provide a cutoff distance' - ' with tree.set_coords(...)') + raise RuntimeError( + "Provide a cutoff distance" " with tree.set_coords(...)" + ) # Bring the coordinates in the central cell self.coords = apply_PBC(coords, self.box) # generate duplicate images - self.aug, self.mapping = augment_coordinates(self.coords, - self.box, - cutoff) + self.aug, self.mapping = augment_coordinates( + self.coords, self.box, cutoff + ) # Images + coords self.all_coords = np.concatenate([self.coords, self.aug]) self.ckdt = cKDTree(self.all_coords, leafsize=self.leafsize) else: # if cutoff distance is provided for non PBC calculations if cutoff is not None: - raise RuntimeError('Donot provide cutoff distance for' - ' non PBC aware calculations') + raise RuntimeError( + "Donot provide cutoff distance for" + " non PBC aware calculations" + ) self.coords = coords self.ckdt = cKDTree(self.coords, self.leafsize) self._built = True @@ -175,37 +180,38 @@ def search(self, centers: npt.ArrayLike, radius: float) -> npt.NDArray: """ if not self._built: - raise RuntimeError('Unbuilt tree. Run tree.set_coords(...)') + raise RuntimeError("Unbuilt tree. Run tree.set_coords(...)") centers = np.asarray(centers) - if centers.shape == (self.dim, ): + if centers.shape == (self.dim,): centers = centers.reshape((1, self.dim)) # Sanity check if self.pbc: if self.cutoff is None: raise ValueError( - "Cutoff needs to be provided when working with PBC.") + "Cutoff needs to be provided when working with PBC." + ) if self.cutoff < radius: - raise RuntimeError('Set cutoff greater or equal to the radius.') + raise RuntimeError( + "Set cutoff greater or equal to the radius." + ) # Bring all query points to the central cell wrapped_centers = apply_PBC(centers, self.box) - indices = list(self.ckdt.query_ball_point(wrapped_centers, - radius)) - self._indices = np.array(list( - itertools.chain.from_iterable(indices)), - dtype=np.intp) + indices = list(self.ckdt.query_ball_point(wrapped_centers, radius)) + self._indices = np.array( + list(itertools.chain.from_iterable(indices)), dtype=np.intp + ) if self._indices.size > 0: - self._indices = undo_augment(self._indices, - self.mapping, - len(self.coords)) + self._indices = undo_augment( + self._indices, self.mapping, len(self.coords) + ) else: wrapped_centers = np.asarray(centers) - indices = list(self.ckdt.query_ball_point(wrapped_centers, - radius)) - self._indices = np.array(list( - itertools.chain.from_iterable(indices)), - dtype=np.intp) + indices = list(self.ckdt.query_ball_point(wrapped_centers, radius)) + self._indices = np.array( + list(itertools.chain.from_iterable(indices)), dtype=np.intp + ) self._indices = np.asarray(unique_int_1d(self._indices)) return self._indices @@ -233,22 +239,27 @@ def search_pairs(self, radius: float) -> npt.NDArray: Indices of all the pairs which are within the specified radius """ if not self._built: - raise RuntimeError(' Unbuilt Tree. Run tree.set_coords(...)') + raise RuntimeError(" Unbuilt Tree. Run tree.set_coords(...)") if self.pbc: if self.cutoff is None: raise ValueError( - "Cutoff needs to be provided when working with PBC.") + "Cutoff needs to be provided when working with PBC." + ) if self.cutoff < radius: - raise RuntimeError('Set cutoff greater or equal to the radius.') + raise RuntimeError( + "Set cutoff greater or equal to the radius." + ) pairs = np.array(list(self.ckdt.query_pairs(radius)), dtype=np.intp) if self.pbc: if len(pairs) > 1: - pairs[:, 0] = undo_augment(pairs[:, 0], self.mapping, - len(self.coords)) - pairs[:, 1] = undo_augment(pairs[:, 1], self.mapping, - len(self.coords)) + pairs[:, 0] = undo_augment( + pairs[:, 0], self.mapping, len(self.coords) + ) + pairs[:, 1] = undo_augment( + pairs[:, 1], self.mapping, len(self.coords) + ) if pairs.size > 0: # First sort the pairs then pick the unique pairs pairs = np.sort(pairs, axis=1) @@ -287,34 +298,41 @@ class initialization """ if not self._built: - raise RuntimeError('Unbuilt tree. Run tree.set_coords(...)') + raise RuntimeError("Unbuilt tree. Run tree.set_coords(...)") centers = np.asarray(centers) - if centers.shape == (self.dim, ): + if centers.shape == (self.dim,): centers = centers.reshape((1, self.dim)) # Sanity check if self.pbc: if self.cutoff is None: raise ValueError( - "Cutoff needs to be provided when working with PBC.") + "Cutoff needs to be provided when working with PBC." + ) if self.cutoff < radius: - raise RuntimeError('Set cutoff greater or equal to the radius.') + raise RuntimeError( + "Set cutoff greater or equal to the radius." + ) # Bring all query points to the central cell wrapped_centers = apply_PBC(centers, self.box) other_tree = cKDTree(wrapped_centers, leafsize=self.leafsize) pairs = other_tree.query_ball_tree(self.ckdt, radius) - pairs = np.array([[i, j] for i, lst in enumerate(pairs) for j in lst], - dtype=np.intp) + pairs = np.array( + [[i, j] for i, lst in enumerate(pairs) for j in lst], + dtype=np.intp, + ) if pairs.size > 0: - pairs[:, 1] = undo_augment(pairs[:, 1], - self.mapping, - len(self.coords)) + pairs[:, 1] = undo_augment( + pairs[:, 1], self.mapping, len(self.coords) + ) else: other_tree = cKDTree(centers, leafsize=self.leafsize) pairs = other_tree.query_ball_tree(self.ckdt, radius) - pairs = np.array([[i, j] for i, lst in enumerate(pairs) for j in lst], - dtype=np.intp) + pairs = np.array( + [[i, j] for i, lst in enumerate(pairs) for j in lst], + dtype=np.intp, + ) if pairs.size > 0: pairs = unique_rows(pairs) return pairs diff --git a/package/MDAnalysis/lib/transformations.py b/package/MDAnalysis/lib/transformations.py index 5c386a01047..27e5f01db9f 100644 --- a/package/MDAnalysis/lib/transformations.py +++ b/package/MDAnalysis/lib/transformations.py @@ -162,16 +162,19 @@ MDAnalysis.lib.transformations """ -import sys +import math import os +import sys import warnings -import math + import numpy as np from numpy.linalg import norm -from .mdamath import angle as vecangle from MDAnalysis.lib.util import no_copy_shim +from .mdamath import angle as vecangle + + def identity_matrix(): """Return 4x4 identity/unit matrix. @@ -316,13 +319,20 @@ def rotation_matrix(angle, direction, point=None): ( (cosa, 0.0, 0.0), (0.0, cosa, 0.0), - (0.0, 0.0, cosa)), dtype=np.float64) + (0.0, 0.0, cosa), + ), + dtype=np.float64, + ) R += np.outer(direction, direction) * (1.0 - cosa) direction *= sina R += np.array( - ((0.0, -direction[2], direction[1]), - (direction[2], 0.0, -direction[0]), - (-direction[1], direction[0], 0.0)), dtype=np.float64) + ( + (0.0, -direction[2], direction[1]), + (direction[2], 0.0, -direction[0]), + (-direction[1], direction[0], 0.0), + ), + dtype=np.float64, + ) M = np.identity(4) M[:3, :3] = R if point is not None: @@ -367,11 +377,17 @@ def rotation_from_matrix(matrix): # rotation angle depending on direction cosa = (np.trace(R33) - 1.0) / 2.0 if abs(direction[2]) > 1e-8: - sina = (R[1, 0] + (cosa - 1.0) * direction[0] * direction[1]) / direction[2] + sina = ( + R[1, 0] + (cosa - 1.0) * direction[0] * direction[1] + ) / direction[2] elif abs(direction[1]) > 1e-8: - sina = (R[0, 2] + (cosa - 1.0) * direction[0] * direction[2]) / direction[1] + sina = ( + R[0, 2] + (cosa - 1.0) * direction[0] * direction[2] + ) / direction[1] else: - sina = (R[2, 1] + (cosa - 1.0) * direction[1] * direction[2]) / direction[0] + sina = ( + R[2, 1] + (cosa - 1.0) * direction[1] * direction[2] + ) / direction[0] angle = math.atan2(sina, cosa) return angle, direction, point @@ -399,10 +415,14 @@ def scale_matrix(factor, origin=None, direction=None): if direction is None: # uniform scaling M = np.array( - ((factor, 0.0, 0.0, 0.0), - (0.0, factor, 0.0, 0.0), - (0.0, 0.0, factor, 0.0), - (0.0, 0.0, 0.0, 1.0)), dtype=np.float64) + ( + (factor, 0.0, 0.0, 0.0), + (0.0, factor, 0.0, 0.0), + (0.0, 0.0, factor, 0.0), + (0.0, 0.0, 0.0, 1.0), + ), + dtype=np.float64, + ) if origin is not None: M[:3, 3] = origin[:3] M[:3, 3] *= 1.0 - factor @@ -462,8 +482,9 @@ def scale_from_matrix(matrix): return factor, origin, direction -def projection_matrix(point, normal, direction=None, - perspective=None, pseudo=False): +def projection_matrix( + point, normal, direction=None, perspective=None, pseudo=False +): """Return matrix to project onto plane defined by point and normal. Using either perspective point, projection direction, or none of both. @@ -502,8 +523,7 @@ def projection_matrix(point, normal, direction=None, normal = unit_vector(normal[:3]) if perspective is not None: # perspective projection - perspective = np.array(perspective[:3], dtype=np.float64, - copy=False) + perspective = np.array(perspective[:3], dtype=np.float64, copy=False) M[0, 0] = M[1, 1] = M[2, 2] = np.dot(perspective - point, normal) M[:3, :3] -= np.outer(perspective, normal) if pseudo: @@ -516,7 +536,9 @@ def projection_matrix(point, normal, direction=None, M[3, 3] = np.dot(perspective, normal) elif direction is not None: # parallel projection - direction = np.array(direction[:3], dtype=np.float64, copy=no_copy_shim) + direction = np.array( + direction[:3], dtype=np.float64, copy=no_copy_shim + ) scale = np.dot(direction, normal) M[:3, :3] -= np.outer(direction, normal) / scale M[:3, 3] = direction * (np.dot(point, normal) / scale) @@ -593,10 +615,11 @@ def projection_from_matrix(matrix, pseudo=False): i = np.where(abs(np.real(l)) > 1e-8)[0] if not len(i): raise ValueError( - "no eigenvector not corresponding to eigenvalue 0") + "no eigenvector not corresponding to eigenvalue 0" + ) point = np.real(V[:, i[-1]]).squeeze() point /= point[3] - normal = - M[3, :3] + normal = -M[3, :3] perspective = M[:3, 3] / np.dot(point[:3], normal) if pseudo: perspective -= normal @@ -649,13 +672,15 @@ def clip_matrix(left, right, bottom, top, near, far, perspective=False): (-t / (right - left), 0.0, (right + left) / (right - left), 0.0), (0.0, -t / (top - bottom), (top + bottom) / (top - bottom), 0.0), (0.0, 0.0, -(far + near) / (far - near), t * far / (far - near)), - (0.0, 0.0, -1.0, 0.0)) + (0.0, 0.0, -1.0, 0.0), + ) else: M = ( (2.0 / (right - left), 0.0, 0.0, (right + left) / (left - right)), (0.0, 2.0 / (top - bottom), 0.0, (top + bottom) / (bottom - top)), (0.0, 0.0, 2.0 / (far - near), (far + near) / (near - far)), - (0.0, 0.0, 0.0, 1.0)) + (0.0, 0.0, 0.0, 1.0), + ) return np.array(M, dtype=np.float64) @@ -717,7 +742,9 @@ def shear_from_matrix(matrix): l, V = np.linalg.eig(M33) i = np.where(abs(np.real(l) - 1.0) < 1e-4)[0] if len(i) < 2: - raise ValueError("no two linear independent eigenvectors found {0!s}".format(l)) + raise ValueError( + "no two linear independent eigenvectors found {0!s}".format(l) + ) V = np.real(V[:, i]).squeeze().T lenorm = -1.0 for i0, i1 in ((0, 1), (0, 2), (1, 2)): @@ -786,7 +813,7 @@ def decompose_matrix(matrix): if not np.linalg.det(P): raise ValueError("matrix is singular") - scale = np.zeros((3, ), dtype=np.float64) + scale = np.zeros((3,), dtype=np.float64) shear = [0, 0, 0] angles = [0, 0, 0] @@ -824,15 +851,16 @@ def decompose_matrix(matrix): angles[0] = math.atan2(row[1, 2], row[2, 2]) angles[2] = math.atan2(row[0, 1], row[0, 0]) else: - #angles[0] = math.atan2(row[1, 0], row[1, 1]) + # angles[0] = math.atan2(row[1, 0], row[1, 1]) angles[0] = math.atan2(-row[2, 1], row[1, 1]) angles[2] = 0.0 return scale, shear, angles, translate, perspective -def compose_matrix(scale=None, shear=None, angles=None, translate=None, - perspective=None): +def compose_matrix( + scale=None, shear=None, angles=None, translate=None, perspective=None +): """Return transformation matrix from sequence of transformations. This is the inverse of the decompose_matrix function. @@ -870,7 +898,7 @@ def compose_matrix(scale=None, shear=None, angles=None, translate=None, T[:3, 3] = translate[:3] M = np.dot(M, T) if angles is not None: - R = euler_matrix(angles[0], angles[1], angles[2], 'sxyz') + R = euler_matrix(angles[0], angles[1], angles[2], "sxyz") M = np.dot(M, R) if shear is not None: Z = np.identity(4) @@ -915,8 +943,10 @@ def orthogonalization_matrix(lengths, angles): (a * sinb * math.sqrt(1.0 - co * co), 0.0, 0.0, 0.0), (-a * sinb * co, b * sina, 0.0, 0.0), (a * cosb, b * cosa, c, 0.0), - (0.0, 0.0, 0.0, 1.0)), - dtype=np.float64) + (0.0, 0.0, 0.0, 1.0), + ), + dtype=np.float64, + ) def superimposition_matrix(v0, v1, scaling=False, usesvd=True): @@ -997,14 +1027,15 @@ def superimposition_matrix(v0, v1, scaling=False, usesvd=True): M[:3, :3] = R else: # compute symmetric matrix N - xx, yy, zz = np.einsum('ij,ij->i', v0 , v1) - xy, yz, zx = np.einsum('ij,ij->i', v0, np.roll(v1, -1, axis=0)) - xz, yx, zy = np.einsum('ij,ij->i', v0, np.roll(v1, -2, axis=0)) + xx, yy, zz = np.einsum("ij,ij->i", v0, v1) + xy, yz, zx = np.einsum("ij,ij->i", v0, np.roll(v1, -1, axis=0)) + xz, yx, zy = np.einsum("ij,ij->i", v0, np.roll(v1, -2, axis=0)) N = ( (xx + yy + zz, 0.0, 0.0, 0.0), (yz - zy, xx - yy - zz, 0.0, 0.0), (zx - xz, xy + yx, -xx + yy - zz, 0.0), - (xy - yx, zx + xz, yz + zy, -xx - yy + zz)) + (xy - yx, zx + xz, yz + zy, -xx - yy + zz), + ) # quaternion: eigenvector corresponding to most positive eigenvalue l, V = np.linalg.eigh(N) q = V[:, np.argmax(l)] @@ -1014,9 +1045,9 @@ def superimposition_matrix(v0, v1, scaling=False, usesvd=True): # scale: ratio of rms deviations from centroid if scaling: - M[:3, :3] *= math.sqrt(np.einsum('ij,ij->',v1,v1) / - np.einsum('ij,ij->',v0,v0)) - + M[:3, :3] *= math.sqrt( + np.einsum("ij,ij->", v1, v1) / np.einsum("ij,ij->", v0, v0) + ) # translation M[:3, 3] = t1 @@ -1026,7 +1057,7 @@ def superimposition_matrix(v0, v1, scaling=False, usesvd=True): return M -def euler_matrix(ai, aj, ak, axes='sxyz'): +def euler_matrix(ai, aj, ak, axes="sxyz"): """Return homogeneous rotation matrix from Euler angles and axis sequence. ai, aj, ak : Euler's roll, pitch and yaw angles @@ -1093,7 +1124,7 @@ def euler_matrix(ai, aj, ak, axes='sxyz'): return M -def euler_from_matrix(matrix, axes='sxyz'): +def euler_from_matrix(matrix, axes="sxyz"): """Return Euler angles from rotation matrix for specified axis sequence. axes : One of 24 axis sequences as string or encoded tuple @@ -1155,7 +1186,7 @@ def euler_from_matrix(matrix, axes='sxyz'): return ax, ay, az -def euler_from_quaternion(quaternion, axes='sxyz'): +def euler_from_quaternion(quaternion, axes="sxyz"): """Return Euler angles from quaternion for specified axis sequence. >>> from MDAnalysis.lib.transformations import euler_from_quaternion @@ -1168,7 +1199,7 @@ def euler_from_quaternion(quaternion, axes='sxyz'): return euler_from_matrix(quaternion_matrix(quaternion), axes) -def quaternion_from_euler(ai, aj, ak, axes='sxyz'): +def quaternion_from_euler(ai, aj, ak, axes="sxyz"): """Return quaternion from Euler angles and axis sequence. ai, aj, ak : Euler's roll, pitch and yaw angles @@ -1209,7 +1240,7 @@ def quaternion_from_euler(ai, aj, ak, axes='sxyz'): sc = si * ck ss = si * sk - quaternion = np.empty((4, ), dtype=np.float64) + quaternion = np.empty((4,), dtype=np.float64) if repetition: quaternion[0] = cj * (cc - ss) quaternion[i] = cj * (cs + sc) @@ -1236,7 +1267,7 @@ def quaternion_about_axis(angle, axis): True """ - quaternion = np.zeros((4, ), dtype=np.float64) + quaternion = np.zeros((4,), dtype=np.float64) quaternion[1] = axis[0] quaternion[2] = axis[1] quaternion[3] = axis[2] @@ -1272,11 +1303,28 @@ def quaternion_matrix(quaternion): q = np.outer(q, q) return np.array( ( - (1.0 - q[2, 2] - q[3, 3], q[1, 2] - q[3, 0], q[1, 3] + q[2, 0], 0.0), - (q[1, 2] + q[3, 0], 1.0 - q[1, 1] - q[3, 3], q[2, 3] - q[1, 0], 0.0), - (q[1, 3] - q[2, 0], q[2, 3] + q[1, 0], 1.0 - q[1, 1] - q[2, 2], 0.0), - (0.0, 0.0, 0.0, 1.0) - ), dtype=np.float64) + ( + 1.0 - q[2, 2] - q[3, 3], + q[1, 2] - q[3, 0], + q[1, 3] + q[2, 0], + 0.0, + ), + ( + q[1, 2] + q[3, 0], + 1.0 - q[1, 1] - q[3, 3], + q[2, 3] - q[1, 0], + 0.0, + ), + ( + q[1, 3] - q[2, 0], + q[2, 3] + q[1, 0], + 1.0 - q[1, 1] - q[2, 2], + 0.0, + ), + (0.0, 0.0, 0.0, 1.0), + ), + dtype=np.float64, + ) def quaternion_from_matrix(matrix, isprecise=False): @@ -1317,7 +1365,7 @@ def quaternion_from_matrix(matrix, isprecise=False): """ M = np.array(matrix, dtype=np.float64, copy=no_copy_shim)[:4, :4] if isprecise: - q = np.empty((4, ), dtype=np.float64) + q = np.empty((4,), dtype=np.float64) t = np.trace(M) if t > M[3, 3]: q[0] = t @@ -1347,11 +1395,14 @@ def quaternion_from_matrix(matrix, isprecise=False): m21 = M[2, 1] m22 = M[2, 2] # symmetric matrix K - K = np.array(( - (m00 - m11 - m22, 0.0, 0.0, 0.0), - (m01 + m10, m11 - m00 - m22, 0.0, 0.0), - (m02 + m20, m12 + m21, m22 - m00 - m11, 0.0), - (m21 - m12, m02 - m20, m10 - m01, m00 + m11 + m22))) + K = np.array( + ( + (m00 - m11 - m22, 0.0, 0.0, 0.0), + (m01 + m10, m11 - m00 - m22, 0.0, 0.0), + (m02 + m20, m12 + m21, m22 - m00 - m11, 0.0), + (m21 - m12, m02 - m20, m10 - m01, m00 + m11 + m22), + ) + ) K /= 3.0 # quaternion is eigenvector of K that corresponds to largest eigenvalue l, V = np.linalg.eigh(K) @@ -1379,7 +1430,10 @@ def quaternion_multiply(quaternion1, quaternion0): -x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0, x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0, -x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0, - x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0), dtype=np.float64) + x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0, + ), + dtype=np.float64, + ) def quaternion_conjugate(quaternion): @@ -1395,9 +1449,9 @@ def quaternion_conjugate(quaternion): """ return np.array( - ( - quaternion[0], -quaternion[1], - -quaternion[2], -quaternion[3]), dtype=np.float64) + (quaternion[0], -quaternion[1], -quaternion[2], -quaternion[3]), + dtype=np.float64, + ) def quaternion_inverse(quaternion): @@ -1514,7 +1568,10 @@ def random_quaternion(rand=None): np.cos(t2) * r2, np.sin(t1) * r1, np.cos(t1) * r1, - np.sin(t2) * r2), dtype=np.float64) + np.sin(t2) * r2, + ), + dtype=np.float64, + ) def random_rotation_matrix(rand=None): @@ -1579,7 +1636,7 @@ def __init__(self, initial=None): initial = np.array(initial, dtype=np.float64) if initial.shape == (4, 4): self._qdown = quaternion_from_matrix(initial) - elif initial.shape == (4, ): + elif initial.shape == (4,): initial /= vector_norm(initial) self._qdown = initial else: @@ -1658,8 +1715,9 @@ def arcball_map_to_sphere(point, center, radius): ( (point[0] - center[0]) / radius, (center[1] - point[1]) / radius, - 0.0 - ), dtype=np.float64 + 0.0, + ), + dtype=np.float64, ) n = v[0] * v[0] + v[1] * v[1] if n > 1.0: @@ -1705,15 +1763,18 @@ def arcball_nearest_axis(point, axes): _NEXT_AXIS = [1, 2, 0, 1] # map axes strings to/from tuples of inner axis, parity, repetition, frame +# fmt: off _AXES2TUPLE = { - 'sxyz': (0, 0, 0, 0), 'sxyx': (0, 0, 1, 0), 'sxzy': (0, 1, 0, 0), - 'sxzx': (0, 1, 1, 0), 'syzx': (1, 0, 0, 0), 'syzy': (1, 0, 1, 0), - 'syxz': (1, 1, 0, 0), 'syxy': (1, 1, 1, 0), 'szxy': (2, 0, 0, 0), - 'szxz': (2, 0, 1, 0), 'szyx': (2, 1, 0, 0), 'szyz': (2, 1, 1, 0), - 'rzyx': (0, 0, 0, 1), 'rxyx': (0, 0, 1, 1), 'ryzx': (0, 1, 0, 1), - 'rxzx': (0, 1, 1, 1), 'rxzy': (1, 0, 0, 1), 'ryzy': (1, 0, 1, 1), - 'rzxy': (1, 1, 0, 1), 'ryxy': (1, 1, 1, 1), 'ryxz': (2, 0, 0, 1), - 'rzxz': (2, 0, 1, 1), 'rxyz': (2, 1, 0, 1), 'rzyz': (2, 1, 1, 1)} + "sxyz": (0, 0, 0, 0), "sxyx": (0, 0, 1, 0), "sxzy": (0, 1, 0, 0), + "sxzx": (0, 1, 1, 0), "syzx": (1, 0, 0, 0), "syzy": (1, 0, 1, 0), + "syxz": (1, 1, 0, 0), "syxy": (1, 1, 1, 0), "szxy": (2, 0, 0, 0), + "szxz": (2, 0, 1, 0), "szyx": (2, 1, 0, 0), "szyz": (2, 1, 1, 0), + "rzyx": (0, 0, 0, 1), "rxyx": (0, 0, 1, 1), "ryzx": (0, 1, 0, 1), + "rxzx": (0, 1, 1, 1), "rxzy": (1, 0, 0, 1), "ryzy": (1, 0, 1, 1), + "rzxy": (1, 1, 0, 1), "ryxy": (1, 1, 1, 1), "ryxz": (2, 0, 0, 1), + "rzxz": (2, 0, 1, 1), "rxyz": (2, 1, 0, 1), "rzyz": (2, 1, 1, 1), +} +# fmt: on _TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items()) @@ -1878,7 +1939,7 @@ def is_same_transform(matrix0, matrix1): return np.allclose(matrix0, matrix1) -def _import_module(module_name, warn=True, prefix='_py_', ignore='_'): +def _import_module(module_name, warn=True, prefix="_py_", ignore="_"): """Try import all public attributes from module into global namespace. Existing attributes with name clashes are renamed with prefix. @@ -1910,6 +1971,7 @@ def _import_module(module_name, warn=True, prefix='_py_', ignore='_'): # orbeckst --- some simple geometry + def rotaxis(a, b): """Return the rotation axis to rotate vector a into b. @@ -1935,7 +1997,7 @@ def rotaxis(a, b): return c / np.linalg.norm(c) -_import_module('_transformations') +_import_module("_transformations") # Documentation in HTML format can be generated with Epydoc __docformat__ = "restructuredtext en" diff --git a/package/MDAnalysis/lib/util.py b/package/MDAnalysis/lib/util.py index 7f576af0ade..d227763d9e6 100644 --- a/package/MDAnalysis/lib/util.py +++ b/package/MDAnalysis/lib/util.py @@ -198,38 +198,38 @@ __docformat__ = "restructuredtext en" -import os -import os.path -import errno -from contextlib import contextmanager import bz2 +import errno +import functools import gzip -import re +import importlib +import inspect import io -import warnings -import functools -from functools import wraps +import itertools +import os +import os.path +import re import textwrap +import warnings import weakref -import importlib -import itertools +from contextlib import contextmanager +from functools import wraps import mmtf import numpy as np - from numpy.testing import assert_equal -import inspect - -from .picklable_file_io import pickle_open, bz2_pickle_open, gzip_pickle_open -from ..exceptions import StreamWarning, DuplicateWarning +from ..exceptions import DuplicateWarning, StreamWarning +from .picklable_file_io import bz2_pickle_open, gzip_pickle_open, pickle_open try: from ._cutil import unique_int_1d except ImportError: - raise ImportError("MDAnalysis not installed properly. " - "This can happen if your C extensions " - "have not been built.") + raise ImportError( + "MDAnalysis not installed properly. " + "This can happen if your C extensions " + "have not been built." + ) def int_array_is_sorted(array): @@ -280,7 +280,7 @@ def filename(name, ext=None, keep=False): @contextmanager -def openany(datasource, mode='rt', reset=True): +def openany(datasource, mode="rt", reset=True): """Context manager for :func:`anyopen`. Open the `datasource` and close it when the context of the :keyword:`with` @@ -330,7 +330,7 @@ def openany(datasource, mode='rt', reset=True): stream.close() -def anyopen(datasource, mode='rt', reset=True): +def anyopen(datasource, mode="rt", reset=True): """Open datasource (gzipped, bzipped, uncompressed) and return a stream. `datasource` can be a filename or a stream (see :func:`isstream`). By @@ -374,14 +374,14 @@ def anyopen(datasource, mode='rt', reset=True): :class:`MDAnalysis.lib.picklable_file_io`. """ - read_handlers = {'bz2': bz2_pickle_open, - 'gz': gzip_pickle_open, - '': pickle_open} - write_handlers = {'bz2': bz2.open, - 'gz': gzip.open, - '': open} - - if mode.startswith('r'): + read_handlers = { + "bz2": bz2_pickle_open, + "gz": gzip_pickle_open, + "": pickle_open, + } + write_handlers = {"bz2": bz2.open, "gz": gzip.open, "": open} + + if mode.startswith("r"): if isstream(datasource): stream = datasource try: @@ -395,20 +395,30 @@ def anyopen(datasource, mode='rt', reset=True): try: stream.seek(0) except (AttributeError, IOError): - warnings.warn("Stream {0}: not guaranteed to be at the beginning." - "".format(filename), - category=StreamWarning) + warnings.warn( + "Stream {0}: not guaranteed to be at the beginning." + "".format(filename), + category=StreamWarning, + ) else: stream = None filename = datasource - for ext in ('bz2', 'gz', ''): # file == '' should be last + for ext in ("bz2", "gz", ""): # file == '' should be last openfunc = read_handlers[ext] stream = _get_stream(datasource, openfunc, mode=mode) if stream is not None: break if stream is None: - raise IOError(errno.EIO, "Cannot open file or stream in mode={mode!r}.".format(**vars()), repr(filename)) - elif mode.startswith('w') or mode.startswith('a'): # append 'a' not tested... + raise IOError( + errno.EIO, + "Cannot open file or stream in mode={mode!r}.".format( + **vars() + ), + repr(filename), + ) + elif mode.startswith("w") or mode.startswith( + "a" + ): # append 'a' not tested... if isstream(datasource): stream = datasource try: @@ -419,16 +429,26 @@ def anyopen(datasource, mode='rt', reset=True): stream = None filename = datasource name, ext = os.path.splitext(filename) - if ext.startswith('.'): + if ext.startswith("."): ext = ext[1:] - if not ext in ('bz2', 'gz'): - ext = '' # anything else but bz2 or gz is just a normal file + if not ext in ("bz2", "gz"): + ext = "" # anything else but bz2 or gz is just a normal file openfunc = write_handlers[ext] stream = openfunc(datasource, mode=mode) if stream is None: - raise IOError(errno.EIO, "Cannot open file or stream in mode={mode!r}.".format(**vars()), repr(filename)) + raise IOError( + errno.EIO, + "Cannot open file or stream in mode={mode!r}.".format( + **vars() + ), + repr(filename), + ) else: - raise NotImplementedError("Sorry, mode={mode!r} is not implemented for {datasource!r}".format(**vars())) + raise NotImplementedError( + "Sorry, mode={mode!r} is not implemented for {datasource!r}".format( + **vars() + ) + ) try: stream.name = filename except (AttributeError, TypeError): @@ -436,7 +456,7 @@ def anyopen(datasource, mode='rt', reset=True): return stream -def _get_stream(filename, openfunction=open, mode='r'): +def _get_stream(filename, openfunction=open, mode="r"): """Return open stream if *filename* can be opened with *openfunction* or else ``None``.""" try: stream = openfunction(filename, mode=mode) @@ -444,10 +464,10 @@ def _get_stream(filename, openfunction=open, mode='r'): # An exception might be raised due to two reasons, first the openfunction is unable to open the file, in this # case we have to ignore the error and return None. Second is when openfunction can't open the file because # either the file isn't there or the permissions don't allow access. - if errno.errorcode[err.errno] in ['ENOENT', 'EACCES']: + if errno.errorcode[err.errno] in ["ENOENT", "EACCES"]: raise sys.exc_info()[1] from err return None - if mode.startswith('r'): + if mode.startswith("r"): # additional check for reading (eg can we uncompress) --- is this needed? try: stream.readline() @@ -490,7 +510,7 @@ def greedy_splitext(p): """ path, root = os.path.split(p) - extension = '' + extension = "" while True: root, ext = os.path.splitext(root) extension = ext + extension @@ -535,7 +555,8 @@ def isstream(obj): signature_methods = ("close",) alternative_methods = ( ("read", "readline", "readlines"), - ("write", "writeline", "writelines")) + ("write", "writeline", "writelines"), + ) # Must have ALL the signature methods for m in signature_methods: @@ -544,7 +565,8 @@ def isstream(obj): # Must have at least one complete set of alternative_methods alternative_results = [ np.all([hasmethod(obj, m) for m in alternatives]) - for alternatives in alternative_methods] + for alternatives in alternative_methods + ] return np.any(alternative_results) @@ -569,9 +591,11 @@ def which(program): Please use shutil.which instead. """ # Can't use decorator because it's declared after this method - wmsg = ("This method is deprecated as of MDAnalysis version 2.7.0 " - "and will be removed in version 3.0.0. Please use shutil.which " - "instead.") + wmsg = ( + "This method is deprecated as of MDAnalysis version 2.7.0 " + "and will be removed in version 3.0.0. Please use shutil.which " + "instead." + ) warnings.warn(wmsg, DeprecationWarning) def is_exe(fpath): @@ -681,8 +705,9 @@ def __init__(self, stream, filename, reset=True, close=False): # on __del__ and super on python 3. Let's warn the user and ensure the # class works normally. if isinstance(stream, NamedStream): - warnings.warn("Constructed NamedStream from a NamedStream", - RuntimeWarning) + warnings.warn( + "Constructed NamedStream from a NamedStream", RuntimeWarning + ) stream = stream.stream self.stream = stream self.name = filename @@ -699,9 +724,11 @@ def reset(self): try: self.stream.seek(0) # typical file objects except (AttributeError, IOError): - warnings.warn("NamedStream {0}: not guaranteed to be at the beginning." - "".format(self.name), - category=StreamWarning) + warnings.warn( + "NamedStream {0}: not guaranteed to be at the beginning." + "".format(self.name), + category=StreamWarning, + ) # access the stream def __getattr__(self, x): @@ -724,9 +751,9 @@ def __exit__(self, *args): # NOTE: By default (close=False) we only reset the stream and NOT close it; this makes # it easier to use it as a drop-in replacement for a filename that might # be opened repeatedly (at least in MDAnalysis) - #try: + # try: # return self.stream.__exit__(*args) - #except AttributeError: + # except AttributeError: # super(NamedStream, self).__exit__(*args) self.close() @@ -932,7 +959,9 @@ def realpath(*args): """ if None in args: return None - return os.path.realpath(os.path.expanduser(os.path.expandvars(os.path.join(*args)))) + return os.path.realpath( + os.path.expanduser(os.path.expandvars(os.path.join(*args))) + ) def get_ext(filename): @@ -1008,9 +1037,11 @@ def format_from_filename_extension(filename): try: root, ext = get_ext(filename) except Exception: - errmsg = (f"Cannot determine file format for file '{filename}'.\n" - f" You can set the format explicitly with " - f"'Universe(..., format=FORMAT)'.") + errmsg = ( + f"Cannot determine file format for file '{filename}'.\n" + f" You can set the format explicitly with " + f"'Universe(..., format=FORMAT)'." + ) raise TypeError(errmsg) from None format = check_compressed_format(root, ext) return format @@ -1049,16 +1080,21 @@ def guess_format(filename): format = format_from_filename_extension(filename.name) except AttributeError: # format is None so we need to complain: - errmsg = (f"guess_format requires an explicit format specifier " - f"for stream {filename}") + errmsg = ( + f"guess_format requires an explicit format specifier " + f"for stream {filename}" + ) raise ValueError(errmsg) from None else: # iterator, list, filename: simple extension checking... something more # complicated is left for the ambitious. # Note: at the moment the upper-case extension *is* the format specifier # and list of filenames is handled by ChainReader - format = (format_from_filename_extension(filename) - if not iterable(filename) else 'CHAIN') + format = ( + format_from_filename_extension(filename) + if not iterable(filename) + else "CHAIN" + ) return format.upper() @@ -1069,7 +1105,7 @@ def iterable(obj): if isinstance(obj, (str, NamedStream)): return False # avoid iterating over characters of a string - if hasattr(obj, 'next'): + if hasattr(obj, "next"): return True # any iterator will do try: len(obj) # anything else that might work @@ -1098,8 +1134,10 @@ def asiterable(obj): #: ``(?P\d?)(?P[IFELAX])(?P(?P\d+)(\.(?P\d+))?)?`` #: #: .. _FORTRAN edit descriptor: http://www.cs.mtu.edu/~shene/COURSES/cs201/NOTES/chap05/format.html -FORTRAN_format_regex = (r"(?P\d+?)(?P[IFEAX])" - r"(?P(?P\d+)(\.(?P\d+))?)?") +FORTRAN_format_regex = ( + r"(?P\d+?)(?P[IFEAX])" + r"(?P(?P\d+)(\.(?P\d+))?)?" +) _FORTRAN_format_pattern = re.compile(FORTRAN_format_regex) @@ -1114,7 +1152,8 @@ class FixedcolumnEntry(object): Reads from line[start:stop] and converts according to typespecifier. """ - convertors = {'I': int, 'F': float, 'E': float, 'A': strip} + + convertors = {"I": int, "F": float, "E": float, "A": strip} def __init__(self, start, stop, typespecifier): """ @@ -1138,10 +1177,12 @@ def __init__(self, start, stop, typespecifier): def read(self, line): """Read the entry from `line` and convert to appropriate type.""" try: - return self.convertor(line[self.start:self.stop]) + return self.convertor(line[self.start : self.stop]) except ValueError: - errmsg = (f"{self}: Failed to read&convert " - f"{line[self.start:self.stop]}") + errmsg = ( + f"{self}: Failed to read&convert " + f"{line[self.start:self.stop]}" + ) raise ValueError(errmsg) from None def __len__(self): @@ -1149,7 +1190,9 @@ def __len__(self): return self.stop - self.start def __repr__(self): - return "FixedcolumnEntry({0:d},{1:d},{2!r})".format(self.start, self.stop, self.typespecifier) + return "FixedcolumnEntry({0:d},{1:d},{2!r})".format( + self.start, self.stop, self.typespecifier + ) class FORTRANReader(object): @@ -1189,18 +1232,22 @@ def __init__(self, fmt): serial,TotRes,resName,name,x,y,z,chainID,resSeq,tempFactor = atomformat.read(line) """ - self.fmt = fmt.split(',') - descriptors = [self.parse_FORTRAN_format(descriptor) for descriptor in self.fmt] + self.fmt = fmt.split(",") + descriptors = [ + self.parse_FORTRAN_format(descriptor) for descriptor in self.fmt + ] start = 0 self.entries = [] for d in descriptors: - if d['format'] != 'X': - for x in range(d['repeat']): - stop = start + d['length'] - self.entries.append(FixedcolumnEntry(start, stop, d['format'])) + if d["format"] != "X": + for x in range(d["repeat"]): + stop = start + d["length"] + self.entries.append( + FixedcolumnEntry(start, stop, d["format"]) + ) start = stop else: - start += d['totallength'] + start += d["totallength"] def read(self, line): """Parse `line` according to the format string and return list of values. @@ -1268,24 +1315,28 @@ def parse_FORTRAN_format(self, edit_descriptor): m = _FORTRAN_format_pattern.match(edit_descriptor.upper()) if m is None: try: - m = _FORTRAN_format_pattern.match("1" + edit_descriptor.upper()) + m = _FORTRAN_format_pattern.match( + "1" + edit_descriptor.upper() + ) if m is None: raise ValueError # really no idea what the descriptor is supposed to mean except: - raise ValueError("unrecognized FORTRAN format {0!r}".format(edit_descriptor)) + raise ValueError( + "unrecognized FORTRAN format {0!r}".format(edit_descriptor) + ) d = m.groupdict() - if d['repeat'] == '': - d['repeat'] = 1 - if d['format'] == 'X': - d['length'] = 1 - for k in ('repeat', 'length', 'decimals'): + if d["repeat"] == "": + d["repeat"] = 1 + if d["format"] == "X": + d["length"] = 1 + for k in ("repeat", "length", "decimals"): try: d[k] = int(d[k]) except ValueError: # catches '' d[k] = 0 except TypeError: # keep None pass - d['totallength'] = d['repeat'] * d['length'] + d["totallength"] = d["repeat"] * d["length"] return d def __len__(self): @@ -1331,14 +1382,14 @@ def fixedwidth_bins(delta, xmin, xmax): """ if not np.all(xmin < xmax): - raise ValueError('Boundaries are not sane: should be xmin < xmax.') + raise ValueError("Boundaries are not sane: should be xmin < xmax.") _delta = np.asarray(delta, dtype=np.float64) _xmin = np.asarray(xmin, dtype=np.float64) _xmax = np.asarray(xmax, dtype=np.float64) _length = _xmax - _xmin N = np.ceil(_length / _delta).astype(np.int_) # number of bins dx = 0.5 * (N * _delta - _length) # add half of the excess to each end - return {'Nbins': N, 'delta': _delta, 'min': _xmin - dx, 'max': _xmax + dx} + return {"Nbins": N, "delta": _delta, "min": _xmin - dx, "max": _xmax + dx} def get_weights(atoms, weights): @@ -1382,16 +1433,20 @@ def get_weights(atoms, weights): if iterable(weights): if len(np.asarray(weights, dtype=object).shape) != 1: - raise ValueError("weights must be a 1D array, not with shape " - "{0}".format(np.asarray(weights, - dtype=object).shape)) + raise ValueError( + "weights must be a 1D array, not with shape " + "{0}".format(np.asarray(weights, dtype=object).shape) + ) elif len(weights) != len(atoms): - raise ValueError("weights (length {0}) must be of same length as " - "the atoms ({1})".format( - len(weights), len(atoms))) + raise ValueError( + "weights (length {0}) must be of same length as " + "the atoms ({1})".format(len(weights), len(atoms)) + ) elif weights is not None: - raise ValueError("weights must be {'mass', None} or an iterable of the " - "same size as the atomgroup.") + raise ValueError( + "weights must be {'mass', None} or an iterable of the " + "same size as the atomgroup." + ) return weights @@ -1402,25 +1457,45 @@ def get_weights(atoms, weights): #: translation table for 3-letter codes --> 1-letter codes #: .. SeeAlso:: :data:`alternative_inverse_aa_codes` canonical_inverse_aa_codes = { - 'ALA': 'A', 'CYS': 'C', 'ASP': 'D', 'GLU': 'E', - 'PHE': 'F', 'GLY': 'G', 'HIS': 'H', 'ILE': 'I', - 'LYS': 'K', 'LEU': 'L', 'MET': 'M', 'ASN': 'N', - 'PRO': 'P', 'GLN': 'Q', 'ARG': 'R', 'SER': 'S', - 'THR': 'T', 'VAL': 'V', 'TRP': 'W', 'TYR': 'Y'} + "ALA": "A", + "CYS": "C", + "ASP": "D", + "GLU": "E", + "PHE": "F", + "GLY": "G", + "HIS": "H", + "ILE": "I", + "LYS": "K", + "LEU": "L", + "MET": "M", + "ASN": "N", + "PRO": "P", + "GLN": "Q", + "ARG": "R", + "SER": "S", + "THR": "T", + "VAL": "V", + "TRP": "W", + "TYR": "Y", +} #: translation table for 1-letter codes --> *canonical* 3-letter codes. #: The table is used for :func:`convert_aa_code`. -amino_acid_codes = {one: three for three, - one in canonical_inverse_aa_codes.items()} +amino_acid_codes = { + one: three for three, one in canonical_inverse_aa_codes.items() +} #: non-default charge state amino acids or special charge state descriptions #: (Not fully synchronized with :class:`MDAnalysis.core.selection.ProteinSelection`.) +# fmt: off alternative_inverse_aa_codes = { - 'HISA': 'H', 'HISB': 'H', 'HSE': 'H', 'HSD': 'H', 'HID': 'H', 'HIE': 'H', 'HIS1': 'H', - 'HIS2': 'H', - 'ASPH': 'D', 'ASH': 'D', - 'GLUH': 'E', 'GLH': 'E', - 'LYSH': 'K', 'LYN': 'K', - 'ARGN': 'R', - 'CYSH': 'C', 'CYS1': 'C', 'CYS2': 'C'} + "HISA": "H", "HISB": "H", "HSE": "H", "HSD": "H", "HID": "H", "HIE": "H", + "HIS1": "H", "HIS2": "H", + "ASPH": "D", "ASH": "D", + "GLUH": "E", "GLH": "E", + "LYSH": "K", "LYN": "K", + "ARGN": "R", + "CYSH": "C", "CYS1": "C", "CYS2": "C", +} +# fmt: on #: lookup table from 3/4 letter resnames to 1-letter codes. Note that non-standard residue names #: for tautomers or different protonation states such as HSE are converted to canonical 1-letter codes ("H"). #: The table is used for :func:`convert_aa_code`. @@ -1460,14 +1535,17 @@ def convert_aa_code(x): try: return d[x.upper()] except KeyError: - errmsg = (f"No conversion for {x} found (1 letter -> 3 letter or 3/4 " - f"letter -> 1 letter)") + errmsg = ( + f"No conversion for {x} found (1 letter -> 3 letter or 3/4 " + f"letter -> 1 letter)" + ) raise ValueError(errmsg) from None #: Regular expression to match and parse a residue-atom selection; will match #: "LYS300:HZ1" or "K300:HZ1" or "K300" or "4GB300:H6O" or "4GB300" or "YaA300". -RESIDUE = re.compile(r""" +RESIDUE = re.compile( + r""" (?P([ACDEFGHIKLMNPQRSTVWY]) # 1-letter amino acid | # or ([0-9A-Z][a-zA-Z][A-Z][A-Z]?) # 3-letter or 4-letter residue name @@ -1479,7 +1557,9 @@ def convert_aa_code(x): \s* (?P\w+) # atom name )? # possibly one - """, re.VERBOSE | re.IGNORECASE) + """, + re.VERBOSE | re.IGNORECASE, +) # from GromacsWrapper cbook.IndexBuilder @@ -1514,14 +1594,18 @@ def parse_residue(residue): # XXX: use _translate_residue() .... m = RESIDUE.match(residue) if not m: - raise ValueError("Selection {residue!r} is not valid (only 1/3/4 letter resnames, resid required).".format(**vars())) - resid = int(m.group('resid')) - residue = m.group('aa') + raise ValueError( + "Selection {residue!r} is not valid (only 1/3/4 letter resnames, resid required).".format( + **vars() + ) + ) + resid = int(m.group("resid")) + residue = m.group("aa") if len(residue) == 1: resname = convert_aa_code(residue) # only works for AA else: resname = residue # use 3-letter for any resname - atomname = m.group('atom') + atomname = m.group("atom") return (resname, resid, atomname) @@ -1592,7 +1676,7 @@ def cached_lookup(func): def wrapper(self, *args, **kwargs): try: if universe_validation: # Universe-level cache validation - u_cache = self.universe._cache.setdefault('_valid', dict()) + u_cache = self.universe._cache.setdefault("_valid", dict()) # A WeakSet is used so that keys from out-of-scope/deleted # objects don't clutter it. valid_caches = u_cache.setdefault(key, weakref.WeakSet()) @@ -1661,20 +1745,21 @@ def unique_rows(arr, return_index=False): # This seems to fail if arr.flags['OWNDATA'] is False # this can occur when second dimension was created through broadcasting # eg: idx = np.array([1, 2])[None, :] - if not arr.flags['OWNDATA']: + if not arr.flags["OWNDATA"]: arr = arr.copy() m = arr.shape[1] if return_index: - u, r_idx = np.unique(arr.view(dtype=np.dtype([(str(i), arr.dtype) - for i in range(m)])), - return_index=True) + u, r_idx = np.unique( + arr.view(dtype=np.dtype([(str(i), arr.dtype) for i in range(m)])), + return_index=True, + ) return u.view(arr.dtype).reshape(-1, m), r_idx else: - u = np.unique(arr.view( - dtype=np.dtype([(str(i), arr.dtype) for i in range(m)]) - )) + u = np.unique( + arr.view(dtype=np.dtype([(str(i), arr.dtype) for i in range(m)])) + ) return u.view(arr.dtype).reshape(-1, m) @@ -1733,20 +1818,25 @@ def blocks_of(a, n, m): # based on: # http://stackoverflow.com/a/10862636 # but generalised to handle non square blocks. - if not a.flags['C_CONTIGUOUS']: + if not a.flags["C_CONTIGUOUS"]: raise ValueError("Input array is not C contiguous.") nblocks = a.shape[0] // n nblocks2 = a.shape[1] // m if not nblocks == nblocks2: - raise ValueError("Must divide into same number of blocks in both" - " directions. Got {} by {}" - "".format(nblocks, nblocks2)) + raise ValueError( + "Must divide into same number of blocks in both" + " directions. Got {} by {}" + "".format(nblocks, nblocks2) + ) new_shape = (nblocks, n, m) - new_strides = (n * a.strides[0] + m * a.strides[1], - a.strides[0], a.strides[1]) + new_strides = ( + n * a.strides[0] + m * a.strides[1], + a.strides[0], + a.strides[1], + ) return np.lib.stride_tricks.as_strided(a, new_shape, new_strides) @@ -1769,11 +1859,11 @@ def group_same_or_consecutive_integers(arr): >>> group_same_or_consecutive_integers(arr) [array([2, 3, 4]), array([ 7, 8, 9, 10, 11]), array([15, 16])] """ - return np.split(arr, np.where(np.ediff1d(arr)-1 > 0)[0] + 1) + return np.split(arr, np.where(np.ediff1d(arr) - 1 > 0)[0] + 1) class Namespace(dict): - """Class to allow storing attributes in new namespace. """ + """Class to allow storing attributes in new namespace.""" def __getattr__(self, key): # a.this causes a __getattr__ call for key = 'this' @@ -1850,7 +1940,7 @@ def flatten_dict(d, parent_key=tuple()): items = [] for k, v in d.items(): if type(k) != tuple: - new_key = parent_key + (k, ) + new_key = parent_key + (k,) else: new_key = parent_key + k if isinstance(v, dict): @@ -1886,10 +1976,12 @@ def static_variables(**kwargs): .. versionadded:: 0.19.0 """ + def static_decorator(func): for kwarg in kwargs: setattr(func, kwarg, kwargs[kwarg]) return func + return static_decorator @@ -1906,6 +1998,7 @@ def static_decorator(func): # method. Of course, as it is generally the case with Python warnings, this is # *not threadsafe*. + @static_variables(warned=False) def warn_if_not_unique(groupmethod): """Decorator triggering a :class:`~MDAnalysis.exceptions.DuplicateWarning` @@ -1925,6 +2018,7 @@ def warn_if_not_unique(groupmethod): .. versionadded:: 0.19.0 """ + @wraps(groupmethod) def wrapper(group, *args, **kwargs): # Proceed as usual if the calling group is unique or a DuplicateWarning @@ -1933,7 +2027,8 @@ def wrapper(group, *args, **kwargs): return groupmethod(group, *args, **kwargs) # Otherwise, throw a DuplicateWarning and execute the method. method_name = ".".join( - (group.__class__.__name__, groupmethod.__name__)) + (group.__class__.__name__, groupmethod.__name__) + ) # Try to get the group's variable name(s): caller_locals = inspect.currentframe().f_back.f_locals.items() group_names = [] @@ -1950,8 +2045,10 @@ def wrapper(group, *args, **kwargs): else: group_name = " a.k.a. ".join(sorted(group_names)) group_repr = repr(group) - msg = ("{}(): {} {} contains duplicates. Results might be biased!" - "".format(method_name, group_name, group_repr)) + msg = ( + "{}(): {} {} contains duplicates. Results might be biased!" + "".format(method_name, group_name, group_repr) + ) warnings.warn(message=msg, category=DuplicateWarning, stacklevel=2) warn_if_not_unique.warned = True try: @@ -1959,6 +2056,7 @@ def wrapper(group, *args, **kwargs): finally: warn_if_not_unique.warned = False return result + return wrapper @@ -2080,17 +2178,20 @@ def check_coords(*coord_names, **options): Can now accept an :class:`AtomGroup` as input, and added option allow_atomgroup with default False to retain old behaviour """ - enforce_copy = options.get('enforce_copy', True) - enforce_dtype = options.get('enforce_dtype', True) - allow_single = options.get('allow_single', True) - convert_single = options.get('convert_single', True) - reduce_result_if_single = options.get('reduce_result_if_single', True) - check_lengths_match = options.get('check_lengths_match', - len(coord_names) > 1) - allow_atomgroup = options.get('allow_atomgroup', False) + enforce_copy = options.get("enforce_copy", True) + enforce_dtype = options.get("enforce_dtype", True) + allow_single = options.get("allow_single", True) + convert_single = options.get("convert_single", True) + reduce_result_if_single = options.get("reduce_result_if_single", True) + check_lengths_match = options.get( + "check_lengths_match", len(coord_names) > 1 + ) + allow_atomgroup = options.get("allow_atomgroup", False) if not coord_names: - raise ValueError("Decorator check_coords() cannot be used without " - "positional arguments.") + raise ValueError( + "Decorator check_coords() cannot be used without " + "positional arguments." + ) def check_coords_decorator(func): fname = func.__name__ @@ -2105,18 +2206,22 @@ def check_coords_decorator(func): # arguments: for name in coord_names: if name not in posargnames: - raise ValueError("In decorator check_coords(): Name '{}' " - "doesn't correspond to any positional " - "argument of the decorated function {}()." - "".format(name, func.__name__)) + raise ValueError( + "In decorator check_coords(): Name '{}' " + "doesn't correspond to any positional " + "argument of the decorated function {}()." + "".format(name, func.__name__) + ) def _check_coords(coords, argname): is_single = False if isinstance(coords, np.ndarray): if allow_single: if (coords.ndim not in (1, 2)) or (coords.shape[-1] != 3): - errmsg = (f"{fname}(): {argname}.shape must be (3,) or " - f"(n, 3), got {coords.shape}") + errmsg = ( + f"{fname}(): {argname}.shape must be (3,) or " + f"(n, 3), got {coords.shape}" + ) raise ValueError(errmsg) if coords.ndim == 1: is_single = True @@ -2124,17 +2229,22 @@ def _check_coords(coords, argname): coords = coords[None, :] else: if (coords.ndim != 2) or (coords.shape[1] != 3): - errmsg = (f"{fname}(): {argname}.shape must be (n, 3) " - f"got {coords.shape}") + errmsg = ( + f"{fname}(): {argname}.shape must be (n, 3) " + f"got {coords.shape}" + ) raise ValueError(errmsg) if enforce_dtype: try: coords = coords.astype( - np.float32, order='C', copy=enforce_copy) + np.float32, order="C", copy=enforce_copy + ) except ValueError: - errmsg = (f"{fname}(): {argname}.dtype must be" - f"convertible to float32, got" - f" {coords.dtype}.") + errmsg = ( + f"{fname}(): {argname}.dtype must be" + f"convertible to float32, got" + f" {coords.dtype}." + ) raise TypeError(errmsg) from None # coordinates should now be the right shape ncoord = coords.shape[0] @@ -2143,15 +2253,19 @@ def _check_coords(coords, argname): coords = coords.positions # homogenise to a numpy array ncoord = coords.shape[0] if not allow_atomgroup: - err = TypeError("AtomGroup or other class with a" - "`.positions` method supplied as an" - "argument, but allow_atomgroup is" - " False") + err = TypeError( + "AtomGroup or other class with a" + "`.positions` method supplied as an" + "argument, but allow_atomgroup is" + " False" + ) raise err except AttributeError: - raise TypeError(f"{fname}(): Parameter '{argname}' must be" - f" a numpy.ndarray or an AtomGroup," - f" got {type(coords)}.") + raise TypeError( + f"{fname}(): Parameter '{argname}' must be" + f" a numpy.ndarray or an AtomGroup," + f" got {type(coords)}." + ) return coords, is_single, ncoord @@ -2164,11 +2278,11 @@ def wrapper(*args, **kwargs): if len(args) > nargs: # too many arguments, invoke call: return func(*args, **kwargs) - for name in posargnames[:len(args)]: + for name in posargnames[: len(args)]: if name in kwargs: # duplicate argument, invoke call: return func(*args, **kwargs) - for name in posargnames[len(args):]: + for name in posargnames[len(args) :]: if name not in kwargs: # missing argument, invoke call: return func(*args, **kwargs) @@ -2184,33 +2298,38 @@ def wrapper(*args, **kwargs): for name in coord_names: idx = posargnames.index(name) if idx < len(args): - args[idx], is_single, ncoord = _check_coords(args[idx], - name) + args[idx], is_single, ncoord = _check_coords( + args[idx], name + ) all_single &= is_single ncoords.append(ncoord) else: - kwargs[name], is_single, ncoord = _check_coords(kwargs[name], - name) + kwargs[name], is_single, ncoord = _check_coords( + kwargs[name], name + ) all_single &= is_single ncoords.append(ncoord) if check_lengths_match and ncoords: if ncoords.count(ncoords[0]) != len(ncoords): - raise ValueError("{}(): {} must contain the same number of " - "coordinates, got {}." - "".format(fname, ", ".join(coord_names), - ncoords)) + raise ValueError( + "{}(): {} must contain the same number of " + "coordinates, got {}." + "".format(fname, ", ".join(coord_names), ncoords) + ) # If all input coordinate arrays were 1-d, so should be the output: if all_single and reduce_result_if_single: return func(*args, **kwargs)[0] return func(*args, **kwargs) + return wrapper + return check_coords_decorator def check_atomgroup_not_empty(groupmethod): """Decorator triggering a ``ValueError`` if the underlying group is empty. - Avoids downstream errors in computing properties of empty atomgroups. + Avoids downstream errors in computing properties of empty atomgroups. Raises ------ @@ -2221,6 +2340,7 @@ def check_atomgroup_not_empty(groupmethod): .. versionadded:: 2.4.0 """ + @wraps(groupmethod) def wrapper(group, *args, **kwargs): # Throw error if the group is empty. @@ -2230,6 +2350,7 @@ def wrapper(group, *args, **kwargs): else: result = groupmethod(group, *args, **kwargs) return result + return wrapper @@ -2241,6 +2362,7 @@ def wrapper(group, *args, **kwargs): # From numpy/lib/utils.py 1.14.5 (used under the BSD 3-clause licence, # https://www.numpy.org/license.html#license) and modified + def _set_function_name(func, name): func.__name__ = name return func @@ -2260,13 +2382,21 @@ class _Deprecate(object): .. versionadded:: 0.19.0 """ - def __init__(self, old_name=None, new_name=None, - release=None, remove=None, message=None): + def __init__( + self, + old_name=None, + new_name=None, + release=None, + remove=None, + message=None, + ): self.old_name = old_name self.new_name = new_name if release is None: - raise ValueError("deprecate: provide release in which " - "feature was deprecated.") + raise ValueError( + "deprecate: provide release in which " + "feature was deprecated." + ) self.release = str(release) self.remove = str(remove) if remove is not None else remove self.message = message @@ -2291,14 +2421,16 @@ def __call__(self, func, *args, **kwargs): depdoc = "`{0}` is deprecated!".format(old_name) else: depdoc = "`{0}` is deprecated, use `{1}` instead!".format( - old_name, new_name) + old_name, new_name + ) warn_message = depdoc remove_text = "" if remove is not None: remove_text = "`{0}` will be removed in release {1}.".format( - old_name, remove) + old_name, remove + ) warn_message += "\n" + remove_text if message is not None: warn_message += "\n" + message @@ -2322,13 +2454,15 @@ def newfunc(*args, **kwds): except TypeError: doc = "" - deprecation_text = dedent_docstring("""\n\n + deprecation_text = dedent_docstring( + """\n\n .. deprecated:: {0} {1} {2} - """.format(release, - message if message else depdoc, - remove_text)) + """.format( + release, message if message else depdoc, remove_text + ) + ) doc = "{0}\n\n{1}\n{2}\n".format(depdoc, doc, deprecation_text) @@ -2435,6 +2569,8 @@ def func(): return _Deprecate(*args, **kwargs)(fn) else: return _Deprecate(*args, **kwargs) + + # # ------------------------------------------------------------------ @@ -2515,13 +2651,16 @@ def check_box(box): if box is None: raise ValueError("Box is None") from .mdamath import triclinic_vectors # avoid circular import - box = np.asarray(box, dtype=np.float32, order='C') + + box = np.asarray(box, dtype=np.float32, order="C") if box.shape != (6,): - raise ValueError("Invalid box information. Must be of the form " - "[lx, ly, lz, alpha, beta, gamma].") - if np.all(box[3:] == 90.): - return 'ortho', box[:3] - return 'tri_vecs', triclinic_vectors(box) + raise ValueError( + "Invalid box information. Must be of the form " + "[lx, ly, lz, alpha, beta, gamma]." + ) + if np.all(box[3:] == 90.0): + return "ortho", box[:3] + return "tri_vecs", triclinic_vectors(box) def store_init_arguments(func): @@ -2560,6 +2699,7 @@ def wrapper(self, *args, **kwargs): else: self._kwargs[key] = arg return func(self, *args, **kwargs) + return wrapper @@ -2592,12 +2732,12 @@ def atoi(s: str) -> int: 34 >>> atoi('foo') 0 - + .. versionadded:: 2.8.0 """ try: - return int(''.join(itertools.takewhile(str.isdigit, s.strip()))) + return int("".join(itertools.takewhile(str.isdigit, s.strip()))) except ValueError: return 0 @@ -2609,8 +2749,8 @@ def is_installed(modulename: str): ---------- modulename : str name of the module to be tested - - + + .. versionadded:: 2.8.0 """ - return importlib.util.find_spec(modulename) is not None + return importlib.util.find_spec(modulename) is not None diff --git a/package/pyproject.toml b/package/pyproject.toml index 72a372ccef2..05bce424867 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -132,7 +132,8 @@ tables\.py | due\.py | setup\.py | MDAnalysis/auxiliary/.*\.py -| visualization/.*\.py +| MDAnalysis/visualization/.*\.py +| MDAnalysis/lib/.*\.py^ | MDAnalysis/transformations/.*\.py ) ''' diff --git a/testsuite/MDAnalysisTests/lib/test_augment.py b/testsuite/MDAnalysisTests/lib/test_augment.py index bb9d5f54d49..455e8902510 100644 --- a/testsuite/MDAnalysisTests/lib/test_augment.py +++ b/testsuite/MDAnalysisTests/lib/test_augment.py @@ -14,71 +14,105 @@ # 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 +# 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 -import os -import pytest -import numpy as np -from numpy.testing import assert_almost_equal, assert_equal +import os +import numpy as np +import pytest from MDAnalysis.lib._augment import augment_coordinates, undo_augment from MDAnalysis.lib.distances import apply_PBC, transform_StoR +from numpy.testing import assert_almost_equal, assert_equal # Find images for several query points, # here in fractional coordinates # Every element of qres tuple is (query, images) qres = ( - ([0.1, 0.5, 0.5], [[1.1, 0.5, 0.5]]), # box face - ([0.5, 0.5, 0.5], []), # box center - ([0.5, -0.1, 0.5], [[0.5, -0.1, 0.5]]), # box face - ([0.1, 0.1, 0.5], [[1.1, 0.1, 0.5], - [0.1, 1.1, 0.5], - [1.1, 1.1, 0.5]]), # box edge - ([0.5, -0.1, 1.1], [[0.5, -0.1, 0.1], - [0.5, 0.9, 1.1], - [0.5, -0.1, 1.1]]), # box edge - ([0.1, 0.1, 0.1], [[1.1, 0.1, 0.1], - [0.1, 1.1, 0.1], - [0.1, 0.1, 1.1], - [0.1, 1.1, 1.1], - [1.1, 1.1, 0.1], - [1.1, 0.1, 1.1], - [1.1, 1.1, 1.1]]), # box vertex - ([0.1, -0.1, 1.1], [[1.1, 0.9, 0.1], - [0.1, -0.1, 0.1], - [0.1, 0.9, 1.1], - [0.1, -0.1, 1.1], - [1.1, -0.1, 0.1], - [1.1, 0.9, 1.1], - [1.1, -0.1, 1.1]]), # box vertex - ([2.1, -3.1, 0.1], [[1.1, 0.9, 0.1], - [0.1, -0.1, 0.1], - [0.1, 0.9, 1.1], - [0.1, -0.1, 1.1], - [1.1, -0.1, 0.1], - [1.1, 0.9, 1.1], - [1.1, -0.1, 1.1]]), # box vertex - ([[0.1, 0.5, 0.5], - [0.5, -0.1, 0.5]], [[1.1, 0.5, 0.5], - [0.5, -0.1, 0.5]]) # multiple queries - ) + ([0.1, 0.5, 0.5], [[1.1, 0.5, 0.5]]), # box face + ([0.5, 0.5, 0.5], []), # box center + ([0.5, -0.1, 0.5], [[0.5, -0.1, 0.5]]), # box face + ( + [0.1, 0.1, 0.5], + [ + [1.1, 0.1, 0.5], + [0.1, 1.1, 0.5], + [1.1, 1.1, 0.5], + ], + ), # box edge + ( + [0.5, -0.1, 1.1], + [ + [0.5, -0.1, 0.1], + [0.5, 0.9, 1.1], + [0.5, -0.1, 1.1], + ], + ), # box edge + ( + [0.1, 0.1, 0.1], + [ + [1.1, 0.1, 0.1], + [0.1, 1.1, 0.1], + [0.1, 0.1, 1.1], + [0.1, 1.1, 1.1], + [1.1, 1.1, 0.1], + [1.1, 0.1, 1.1], + [1.1, 1.1, 1.1], + ], + ), # box vertex + ( + [0.1, -0.1, 1.1], + [ + [1.1, 0.9, 0.1], + [0.1, -0.1, 0.1], + [0.1, 0.9, 1.1], + [0.1, -0.1, 1.1], + [1.1, -0.1, 0.1], + [1.1, 0.9, 1.1], + [1.1, -0.1, 1.1], + ], + ), # box vertex + ( + [2.1, -3.1, 0.1], + [ + [1.1, 0.9, 0.1], + [0.1, -0.1, 0.1], + [0.1, 0.9, 1.1], + [0.1, -0.1, 1.1], + [1.1, -0.1, 0.1], + [1.1, 0.9, 1.1], + [1.1, -0.1, 1.1], + ], + ), # box vertex + ( + [ + [0.1, 0.5, 0.5], + [0.5, -0.1, 0.5], + ], + [ + [1.1, 0.5, 0.5], + [0.5, -0.1, 0.5], + ], + ), # multiple queries +) -@pytest.mark.xfail(os.name == "nt", - reason="see gh-3248") -@pytest.mark.parametrize('b', ( - np.array([10, 10, 10, 90, 90, 90], dtype=np.float32), - np.array([10, 10, 10, 45, 60, 90], dtype=np.float32) - )) -@pytest.mark.parametrize('q, res', qres) +@pytest.mark.xfail(os.name == "nt", reason="see gh-3248") +@pytest.mark.parametrize( + "b", + ( + np.array([10, 10, 10, 90, 90, 90], dtype=np.float32), + np.array([10, 10, 10, 45, 60, 90], dtype=np.float32), + ), +) +@pytest.mark.parametrize("q, res", qres) def test_augment(b, q, res): radius = 1.5 q = transform_StoR(np.array(q, dtype=np.float32), b) - if q.shape == (3, ): + if q.shape == (3,): q = q.reshape((1, 3)) q = apply_PBC(q, b) aug, mapping = augment_coordinates(q, b, radius) @@ -94,18 +128,21 @@ def test_augment(b, q, res): assert_almost_equal(aug, cs, decimal=5) -@pytest.mark.parametrize('b', ( - np.array([10, 10, 10, 90, 90, 90], dtype=np.float32), - np.array([10, 10, 10, 45, 60, 90], dtype=np.float32) - )) -@pytest.mark.parametrize('qres', qres) +@pytest.mark.parametrize( + "b", + ( + np.array([10, 10, 10, 90, 90, 90], dtype=np.float32), + np.array([10, 10, 10, 45, 60, 90], dtype=np.float32), + ), +) +@pytest.mark.parametrize("qres", qres) def test_undoaugment(b, qres): radius = 1.5 q = transform_StoR(np.array(qres[0], dtype=np.float32), b) - if q.shape == (3, ): + if q.shape == (3,): q = q.reshape((1, 3)) q = apply_PBC(q, b) aug, mapping = augment_coordinates(q, b, radius) for idx, val in enumerate(aug): - imageid = np.asarray([len(q) + idx], dtype=np.intp) + imageid = np.asarray([len(q) + idx], dtype=np.intp) assert_equal(mapping[idx], undo_augment(imageid, mapping, len(q))[0]) diff --git a/testsuite/MDAnalysisTests/lib/test_cutil.py b/testsuite/MDAnalysisTests/lib/test_cutil.py index 9f710984df0..47c4d7f905c 100644 --- a/testsuite/MDAnalysisTests/lib/test_cutil.py +++ b/testsuite/MDAnalysisTests/lib/test_cutil.py @@ -25,18 +25,23 @@ from numpy.testing import assert_equal from MDAnalysis.lib._cutil import ( - unique_int_1d, find_fragments, _in2d, + unique_int_1d, + find_fragments, + _in2d, ) -@pytest.mark.parametrize('values', ( - [], # empty array - [1, 1, 1, 1, ], # all identical - [2, 3, 5, 7, ], # all different, monotonic - [5, 2, 7, 3, ], # all different, non-monotonic - [1, 2, 2, 4, 4, 6, ], # duplicates, monotonic - [1, 2, 2, 6, 4, 4, ], # duplicates, non-monotonic -)) +@pytest.mark.parametrize( + "values", + ( + [], # empty array + [1, 1, 1, 1], # all identical + [2, 3, 5, 7], # all different, monotonic + [5, 2, 7, 3], # all different, non-monotonic + [1, 2, 2, 4, 4, 6], # duplicates, monotonic + [1, 2, 2, 6, 4, 4], # duplicates, non-monotonic + ), +) def test_unique_int_1d(values): array = np.array(values, dtype=np.intp) ref = np.unique(array) @@ -46,16 +51,21 @@ def test_unique_int_1d(values): assert res.dtype == ref.dtype -@pytest.mark.parametrize('edges,ref', [ - ([[0, 1], [1, 2], [2, 3], [3, 4]], - [[0, 1, 2, 3, 4]]), # linear chain - ([[0, 1], [1, 2], [2, 3], [3, 4], [4, 10]], - [[0, 1, 2, 3, 4]]), # unused edge (4, 10) - ([[0, 1], [1, 2], [2, 3]], - [[0, 1, 2, 3], [4]]), # lone atom - ([[0, 1], [1, 2], [2, 0], [3, 4], [4, 3]], - [[0, 1, 2], [3, 4]]), # circular -]) +@pytest.mark.parametrize( + "edges,ref", + [ + ([[0, 1], [1, 2], [2, 3], [3, 4]], [[0, 1, 2, 3, 4]]), # linear chain + ( + [[0, 1], [1, 2], [2, 3], [3, 4], [4, 10]], + [[0, 1, 2, 3, 4]], + ), # unused edge (4, 10) + ([[0, 1], [1, 2], [2, 3]], [[0, 1, 2, 3], [4]]), # lone atom + ( + [[0, 1], [1, 2], [2, 0], [3, 4], [4, 3]], + [[0, 1, 2], [3, 4]], + ), # circular + ], +) def test_find_fragments(edges, ref): atoms = np.arange(5) @@ -75,13 +85,21 @@ def test_in2d(): assert_equal(result, np.array([False, True, False])) -@pytest.mark.parametrize('arr1,arr2', [ - (np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.intp), - np.array([[1, 2], [3, 4]], dtype=np.intp)), - (np.array([[1, 2], [3, 4]], dtype=np.intp), - np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.intp)), -]) +@pytest.mark.parametrize( + "arr1,arr2", + [ + ( + np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.intp), + np.array([[1, 2], [3, 4]], dtype=np.intp), + ), + ( + np.array([[1, 2], [3, 4]], dtype=np.intp), + np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.intp), + ), + ], +) def test_in2d_VE(arr1, arr2): - with pytest.raises(ValueError, - match=r'Both arrays must be \(n, 2\) arrays'): + with pytest.raises( + ValueError, match=r"Both arrays must be \(n, 2\) arrays" + ): _in2d(arr1, arr2) diff --git a/testsuite/MDAnalysisTests/lib/test_distances.py b/testsuite/MDAnalysisTests/lib/test_distances.py index 0586ba071fe..8844ef9b848 100644 --- a/testsuite/MDAnalysisTests/lib/test_distances.py +++ b/testsuite/MDAnalysisTests/lib/test_distances.py @@ -20,19 +20,18 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -import sys -from unittest.mock import Mock, patch -import pytest -import numpy as np -from numpy.testing import assert_equal, assert_almost_equal, assert_allclose import itertools +import sys from itertools import combinations_with_replacement as comb +from unittest.mock import Mock, patch import MDAnalysis -from MDAnalysis.lib import distances +import numpy as np +import pytest +from MDAnalysis.lib import distances, mdamath from MDAnalysis.lib.distances import HAS_DISTOPIA -from MDAnalysis.lib import mdamath -from MDAnalysis.tests.datafiles import PSF, DCD, TRIC +from MDAnalysis.tests.datafiles import DCD, PSF, TRIC +from numpy.testing import assert_allclose, assert_almost_equal, assert_equal class TestCheckResultArray(object): @@ -52,25 +51,30 @@ def test_check_result_array_wrong_shape(self): wrong_shape = (1,) + self.ref.shape with pytest.raises(ValueError) as err: res = distances._check_result_array(self.ref, wrong_shape) - assert err.msg == ("Result array has incorrect shape, should be " - "{0}, got {1}.".format(self.ref.shape, - wrong_shape)) + assert err.msg == ( + "Result array has incorrect shape, should be " + "{0}, got {1}.".format(self.ref.shape, wrong_shape) + ) def test_check_result_array_wrong_dtype(self): wrong_dtype = np.int64 ref_wrong_dtype = self.ref.astype(wrong_dtype) with pytest.raises(TypeError) as err: - res = distances._check_result_array(ref_wrong_dtype, self.ref.shape) - assert err.msg == ("Result array must be of type numpy.float64, " - "got {}.".format(wrong_dtype)) + res = distances._check_result_array( + ref_wrong_dtype, self.ref.shape + ) + assert err.msg == ( + "Result array must be of type numpy.float64, " + "got {}.".format(wrong_dtype) + ) -@pytest.mark.parametrize('coord_dtype', (np.float32, np.float64)) +@pytest.mark.parametrize("coord_dtype", (np.float32, np.float64)) def test_transform_StoR_pass(coord_dtype): box = np.array([10, 7, 3, 45, 60, 90], dtype=np.float32) s = np.array([[0.5, -0.1, 0.5]], dtype=coord_dtype) - original_r = np.array([[ 5.75, 0.36066014, 0.75]], dtype=np.float32) + original_r = np.array([[5.75, 0.36066014, 0.75]], dtype=np.float32) test_r = distances.transform_StoR(s, box) @@ -81,10 +85,11 @@ class TestCappedDistances(object): npoints_1 = (1, 100) - boxes_1 = (np.array([10, 20, 30, 90, 90, 90], dtype=np.float32), # ortho - np.array([10, 20, 30, 30, 45, 60], dtype=np.float32), # tri_box - None, # Non Periodic - ) + boxes_1 = ( + np.array([10, 20, 30, 90, 90, 90], dtype=np.float32), # ortho + np.array([10, 20, 30, 30, 45, 60], dtype=np.float32), # tri_box + None, # Non Periodic + ) @pytest.fixture() def query_1(self): @@ -110,7 +115,7 @@ def query_2_atomgroup(self, query_2): u.atoms.positions = q2 return u.atoms - method_1 = ('bruteforce', 'pkdtree', 'nsgrid') + method_1 = ("bruteforce", "pkdtree", "nsgrid") min_cutoff_1 = (None, 0.1) @@ -118,90 +123,112 @@ def test_capped_distance_noresults(self): point1 = np.array([0.1, 0.1, 0.1], dtype=np.float32) point2 = np.array([0.95, 0.1, 0.1], dtype=np.float32) - pairs, dists = distances.capped_distance(point1, - point2, max_cutoff=0.2) + pairs, dists = distances.capped_distance( + point1, point2, max_cutoff=0.2 + ) assert_equal(len(pairs), 0) - @pytest.mark.parametrize('query', ['query_1', 'query_2', - 'query_1_atomgroup', 'query_2_atomgroup']) - @pytest.mark.parametrize('npoints', npoints_1) - @pytest.mark.parametrize('box', boxes_1) - @pytest.mark.parametrize('method', method_1) - @pytest.mark.parametrize('min_cutoff', min_cutoff_1) - def test_capped_distance_checkbrute(self, npoints, box, method, - min_cutoff, query, request): + @pytest.mark.parametrize( + "query", + ["query_1", "query_2", "query_1_atomgroup", "query_2_atomgroup"], + ) + @pytest.mark.parametrize("npoints", npoints_1) + @pytest.mark.parametrize("box", boxes_1) + @pytest.mark.parametrize("method", method_1) + @pytest.mark.parametrize("min_cutoff", min_cutoff_1) + def test_capped_distance_checkbrute( + self, npoints, box, method, min_cutoff, query, request + ): q = request.getfixturevalue(query) np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(npoints, 3))*(self.boxes_1[0][:3])).astype(np.float32) + points = ( + np.random.uniform(low=0, high=1.0, size=(npoints, 3)) + * (self.boxes_1[0][:3]) + ).astype(np.float32) max_cutoff = 2.5 # capped distance should be able to handle array of vectors # as well as single vectors. - pairs, dist = distances.capped_distance(q, points, max_cutoff, - min_cutoff=min_cutoff, box=box, - method=method) + pairs, dist = distances.capped_distance( + q, + points, + max_cutoff, + min_cutoff=min_cutoff, + box=box, + method=method, + ) - if pairs.shape != (0, ): + if pairs.shape != (0,): found_pairs = pairs[:, 1] else: found_pairs = list() if isinstance(q, np.ndarray): - if(q.shape[0] == 3): + if q.shape[0] == 3: q = q.reshape((1, 3)) dists = distances.distance_array(q, points, box=box) if min_cutoff is None: - min_cutoff = 0. + min_cutoff = 0.0 indices = np.where((dists <= max_cutoff) & (dists > min_cutoff)) assert_equal(np.sort(found_pairs, axis=0), np.sort(indices[1], axis=0)) # for coverage - @pytest.mark.parametrize('query', ['query_1', 'query_2', - 'query_1_atomgroup', 'query_2_atomgroup']) - @pytest.mark.parametrize('npoints', npoints_1) - @pytest.mark.parametrize('box', boxes_1) - @pytest.mark.parametrize('method', method_1) - @pytest.mark.parametrize('min_cutoff', min_cutoff_1) - def test_capped_distance_return(self, npoints, box, query, request, - method, min_cutoff): + @pytest.mark.parametrize( + "query", + ["query_1", "query_2", "query_1_atomgroup", "query_2_atomgroup"], + ) + @pytest.mark.parametrize("npoints", npoints_1) + @pytest.mark.parametrize("box", boxes_1) + @pytest.mark.parametrize("method", method_1) + @pytest.mark.parametrize("min_cutoff", min_cutoff_1) + def test_capped_distance_return( + self, npoints, box, query, request, method, min_cutoff + ): q = request.getfixturevalue(query) np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(npoints, 3))*(self.boxes_1[0][:3])).astype(np.float32) + points = ( + np.random.uniform(low=0, high=1.0, size=(npoints, 3)) + * (self.boxes_1[0][:3]) + ).astype(np.float32) max_cutoff = 0.3 # capped distance should be able to handle array of vectors # as well as single vectors. - pairs = distances.capped_distance(q, points, max_cutoff, - min_cutoff=min_cutoff, box=box, - method=method, - return_distances=False) + pairs = distances.capped_distance( + q, + points, + max_cutoff, + min_cutoff=min_cutoff, + box=box, + method=method, + return_distances=False, + ) - if pairs.shape != (0, ): + if pairs.shape != (0,): found_pairs = pairs[:, 1] else: found_pairs = list() if isinstance(q, np.ndarray): - if(q.shape[0] == 3): + if q.shape[0] == 3: q = q.reshape((1, 3)) dists = distances.distance_array(q, points, box=box) if min_cutoff is None: - min_cutoff = 0. + min_cutoff = 0.0 indices = np.where((dists <= max_cutoff) & (dists > min_cutoff)) - assert_equal(np.sort(found_pairs, axis=0), - np.sort(indices[1], axis=0)) + assert_equal(np.sort(found_pairs, axis=0), np.sort(indices[1], axis=0)) def points_or_ag_self_capped(self, npoints, atomgroup=False): np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(npoints, 3))*(self.boxes_1[0][:3])).astype(np.float32) + points = ( + np.random.uniform(low=0, high=1.0, size=(npoints, 3)) + * (self.boxes_1[0][:3]) + ).astype(np.float32) if atomgroup: u = MDAnalysis.Universe.empty(points.shape[0], trajectory=True) u.atoms.positions = points @@ -209,20 +236,25 @@ def points_or_ag_self_capped(self, npoints, atomgroup=False): else: return points - @pytest.mark.parametrize('npoints', npoints_1) - @pytest.mark.parametrize('box', boxes_1) - @pytest.mark.parametrize('method', method_1) - @pytest.mark.parametrize('min_cutoff', min_cutoff_1) - @pytest.mark.parametrize('ret_dist', (False, True)) - @pytest.mark.parametrize('atomgroup', (False, True)) - def test_self_capped_distance(self, npoints, box, method, min_cutoff, - ret_dist, atomgroup): + @pytest.mark.parametrize("npoints", npoints_1) + @pytest.mark.parametrize("box", boxes_1) + @pytest.mark.parametrize("method", method_1) + @pytest.mark.parametrize("min_cutoff", min_cutoff_1) + @pytest.mark.parametrize("ret_dist", (False, True)) + @pytest.mark.parametrize("atomgroup", (False, True)) + def test_self_capped_distance( + self, npoints, box, method, min_cutoff, ret_dist, atomgroup + ): points = self.points_or_ag_self_capped(npoints, atomgroup=atomgroup) max_cutoff = 0.2 - result = distances.self_capped_distance(points, max_cutoff, - min_cutoff=min_cutoff, box=box, - method=method, - return_distances=ret_dist) + result = distances.self_capped_distance( + points, + max_cutoff, + min_cutoff=min_cutoff, + box=box, + method=method, + return_distances=ret_dist, + ) if ret_dist: pairs, cdists = result else: @@ -251,50 +283,70 @@ def test_self_capped_distance(self, npoints, box, method, min_cutoff, if min_cutoff is not None: assert d_ref > min_cutoff - @pytest.mark.parametrize('box', (None, - np.array([1, 1, 1, 90, 90, 90], - dtype=np.float32), - np.array([1, 1, 1, 60, 75, 80], - dtype=np.float32))) - @pytest.mark.parametrize('npoints,cutoff,meth', - [(1, 0.02, '_bruteforce_capped_self'), - (1, 0.2, '_bruteforce_capped_self'), - (600, 0.02, '_pkdtree_capped_self'), - (600, 0.2, '_nsgrid_capped_self')]) + @pytest.mark.parametrize( + "box", + ( + None, + np.array([1, 1, 1, 90, 90, 90], dtype=np.float32), + np.array([1, 1, 1, 60, 75, 80], dtype=np.float32), + ), + ) + @pytest.mark.parametrize( + "npoints,cutoff,meth", + [ + (1, 0.02, "_bruteforce_capped_self"), + (1, 0.2, "_bruteforce_capped_self"), + (600, 0.02, "_pkdtree_capped_self"), + (600, 0.2, "_nsgrid_capped_self"), + ], + ) def test_method_selfselection(self, box, npoints, cutoff, meth): np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(npoints, 3))).astype(np.float32) + points = ( + np.random.uniform(low=0, high=1.0, size=(npoints, 3)) + ).astype(np.float32) method = distances._determine_method_self(points, cutoff, box=box) assert_equal(method.__name__, meth) - @pytest.mark.parametrize('box', (None, - np.array([1, 1, 1, 90, 90, 90], - dtype=np.float32), - np.array([1, 1, 1, 60, 75, 80], - dtype=np.float32))) - @pytest.mark.parametrize('npoints,cutoff,meth', - [(1, 0.02, '_bruteforce_capped'), - (1, 0.2, '_bruteforce_capped'), - (200, 0.02, '_nsgrid_capped'), - (200, 0.35, '_bruteforce_capped'), - (10000, 0.35, '_nsgrid_capped')]) + @pytest.mark.parametrize( + "box", + ( + None, + np.array([1, 1, 1, 90, 90, 90], dtype=np.float32), + np.array([1, 1, 1, 60, 75, 80], dtype=np.float32), + ), + ) + @pytest.mark.parametrize( + "npoints,cutoff,meth", + [ + (1, 0.02, "_bruteforce_capped"), + (1, 0.2, "_bruteforce_capped"), + (200, 0.02, "_nsgrid_capped"), + (200, 0.35, "_bruteforce_capped"), + (10000, 0.35, "_nsgrid_capped"), + ], + ) def test_method_selection(self, box, npoints, cutoff, meth): np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(npoints, 3)).astype(np.float32)) + points = np.random.uniform(low=0, high=1.0, size=(npoints, 3)).astype( + np.float32 + ) method = distances._determine_method(points, points, cutoff, box=box) assert_equal(method.__name__, meth) @pytest.fixture() def ref_system(): - box = np.array([1., 1., 2., 90., 90., 90], dtype=np.float32) + box = np.array([1.0, 1.0, 2.0, 90.0, 90.0, 90], dtype=np.float32) + # fmt: off points = np.array( [ [0, 0, 0], [1, 1, 2], [1, 0, 2], # identical under PBC [0.5, 0.5, 1.5], - ], dtype=np.float32) + ], + dtype=np.float32, + ) + # fmt: on ref = points[0:1] conf = points[1:] @@ -307,11 +359,15 @@ def ref_system_universe(ref_system): u = MDAnalysis.Universe.empty(points.shape[0], trajectory=True) u.atoms.positions = points u.trajectory.ts.dimensions = box - return (box, u.atoms, u.select_atoms("index 0"), - u.select_atoms("index 1 to 3")) + return ( + box, + u.atoms, + u.select_atoms("index 0"), + u.select_atoms("index 1 to 3"), + ) -@pytest.mark.parametrize('backend', ['serial', 'openmp']) +@pytest.mark.parametrize("backend", ["serial", "openmp"]) class TestDistanceArray(object): @staticmethod def _dist(x, ref): @@ -320,65 +376,87 @@ def _dist(x, ref): return np.sqrt(np.dot(r, r)) # test both AtomGroup and numpy array - @pytest.mark.parametrize('pos', ['ref_system', 'ref_system_universe']) + @pytest.mark.parametrize("pos", ["ref_system", "ref_system_universe"]) def test_noPBC(self, backend, ref_system, pos, request): _, points, reference, _ = ref_system # reference values _, all, ref, _ = request.getfixturevalue(pos) d = distances.distance_array(ref, all, backend=backend) - assert_almost_equal(d, np.array([[ - self._dist(points[0], reference[0]), - self._dist(points[1], reference[0]), - self._dist(points[2], reference[0]), - self._dist(points[3], reference[0])] - ])) + assert_almost_equal( + d, + np.array( + [ + [ + self._dist(points[0], reference[0]), + self._dist(points[1], reference[0]), + self._dist(points[2], reference[0]), + self._dist(points[3], reference[0]), + ] + ] + ), + ) # cycle through combinations of numpy array and AtomGroup - @pytest.mark.parametrize('pos0', ['ref_system', 'ref_system_universe']) - @pytest.mark.parametrize('pos1', ['ref_system', 'ref_system_universe']) - def test_noPBC_mixed_combinations(self, backend, ref_system, pos0, pos1, - request): + @pytest.mark.parametrize("pos0", ["ref_system", "ref_system_universe"]) + @pytest.mark.parametrize("pos1", ["ref_system", "ref_system_universe"]) + def test_noPBC_mixed_combinations( + self, backend, ref_system, pos0, pos1, request + ): _, points, reference, _ = ref_system # reference values _, _, ref_val, _ = request.getfixturevalue(pos0) _, points_val, _, _ = request.getfixturevalue(pos1) - d = distances.distance_array(ref_val, points_val, - backend=backend) - assert_almost_equal(d, np.array([[ - self._dist(points[0], reference[0]), - self._dist(points[1], reference[0]), - self._dist(points[2], reference[0]), - self._dist(points[3], reference[0])] - ])) + d = distances.distance_array(ref_val, points_val, backend=backend) + assert_almost_equal( + d, + np.array( + [ + [ + self._dist(points[0], reference[0]), + self._dist(points[1], reference[0]), + self._dist(points[2], reference[0]), + self._dist(points[3], reference[0]), + ] + ] + ), + ) # test both AtomGroup and numpy array - @pytest.mark.parametrize('pos', ['ref_system', 'ref_system_universe']) + @pytest.mark.parametrize("pos", ["ref_system", "ref_system_universe"]) def test_PBC(self, backend, ref_system, pos, request): box, points, _, _ = ref_system _, all, ref, _ = request.getfixturevalue(pos) d = distances.distance_array(ref, all, box=box, backend=backend) - assert_almost_equal(d, np.array([[0., 0., 0., self._dist(points[3], - ref=[1, 1, 2])]])) + assert_almost_equal( + d, + np.array([[0.0, 0.0, 0.0, self._dist(points[3], ref=[1, 1, 2])]]), + ) # cycle through combinations of numpy array and AtomGroup - @pytest.mark.parametrize('pos0', ['ref_system', 'ref_system_universe']) - @pytest.mark.parametrize('pos1', ['ref_system', 'ref_system_universe']) - def test_PBC_mixed_combinations(self, backend, ref_system, pos0, pos1, - request): + @pytest.mark.parametrize("pos0", ["ref_system", "ref_system_universe"]) + @pytest.mark.parametrize("pos1", ["ref_system", "ref_system_universe"]) + def test_PBC_mixed_combinations( + self, backend, ref_system, pos0, pos1, request + ): box, points, _, _ = ref_system _, _, ref_val, _ = request.getfixturevalue(pos0) _, points_val, _, _ = request.getfixturevalue(pos1) - d = distances.distance_array(ref_val, points_val, - box=box, - backend=backend) + d = distances.distance_array( + ref_val, points_val, box=box, backend=backend + ) assert_almost_equal( - d, np.array([[0., 0., 0., self._dist(points[3], ref=[1, 1, 2])]])) + d, + np.array([[0.0, 0.0, 0.0, self._dist(points[3], ref=[1, 1, 2])]]), + ) def test_PBC2(self, backend): a = np.array([7.90146923, -13.72858524, 3.75326586], dtype=np.float32) b = np.array([-1.36250901, 13.45423985, -0.36317623], dtype=np.float32) - box = np.array([5.5457325, 5.5457325, 5.5457325, 90., 90., 90.], dtype=np.float32) + box = np.array( + [5.5457325, 5.5457325, 5.5457325, 90.0, 90.0, 90.0], + dtype=np.float32, + ) def mindist(a, b, box): x = a - b @@ -387,24 +465,32 @@ def mindist(a, b, box): ref = mindist(a, b, box[:3]) val = distances.distance_array(a, b, box=box, backend=backend)[0, 0] - assert_almost_equal(val, ref, decimal=6, - err_msg="Issue 151 not correct (PBC in distance array)") + assert_almost_equal( + val, + ref, + decimal=6, + err_msg="Issue 151 not correct (PBC in distance array)", + ) + def test_distance_array_overflow_exception(): class FakeArray(np.ndarray): shape = (4294967296, 3) # upper limit is sqrt(UINT64_MAX) ndim = 2 + dummy_array = FakeArray([1, 2, 3]) - box = np.array([100, 100, 100, 90., 90., 90.], dtype=np.float32) + box = np.array([100, 100, 100, 90.0, 90.0, 90.0], dtype=np.float32) with pytest.raises(ValueError, match="Size of resulting array"): distances.distance_array.__wrapped__(dummy_array, dummy_array, box=box) + def test_self_distance_array_overflow_exception(): class FakeArray(np.ndarray): shape = (6074001001, 3) # solution of x**2 -x = 2*UINT64_MAX ndim = 2 + dummy_array = FakeArray([1, 2, 3]) - box = np.array([100, 100, 100, 90., 90., 90.], dtype=np.float32) + box = np.array([100, 100, 100, 90.0, 90.0, 90.0], dtype=np.float32) with pytest.raises(ValueError, match="Size of resulting array"): distances.self_distance_array.__wrapped__(dummy_array, box=box) @@ -428,7 +514,8 @@ def Triclinic_Universe(): universe = MDAnalysis.Universe(TRIC) return universe -@pytest.mark.parametrize('backend', ['serial', 'openmp']) + +@pytest.mark.parametrize("backend", ["serial", "openmp"]) class TestDistanceArrayDCD_TRIC(object): # reasonable precision so that tests succeed on 32 and 64 bit machines # (the reference values were obtained on 64 bit) @@ -446,12 +533,21 @@ def test_simple(self, DCD_Universe, backend): trajectory[10] x1 = U.atoms.positions d = distances.distance_array(x0, x1, backend=backend) - assert_equal(d.shape, (3341, 3341), "wrong shape (should be" - "(Natoms,Natoms))") - assert_almost_equal(d.min(), 0.11981228170520701, self.prec, - err_msg="wrong minimum distance value") - assert_almost_equal(d.max(), 53.572192429459619, self.prec, - err_msg="wrong maximum distance value") + assert_equal( + d.shape, (3341, 3341), "wrong shape (should be" "(Natoms,Natoms))" + ) + assert_almost_equal( + d.min(), + 0.11981228170520701, + self.prec, + err_msg="wrong minimum distance value", + ) + assert_almost_equal( + d.max(), + 53.572192429459619, + self.prec, + err_msg="wrong maximum distance value", + ) def test_outarray(self, DCD_Universe, backend): U = DCD_Universe @@ -463,12 +559,23 @@ def test_outarray(self, DCD_Universe, backend): natoms = len(U.atoms) d = np.zeros((natoms, natoms), np.float64) distances.distance_array(x0, x1, result=d, backend=backend) - assert_equal(d.shape, (natoms, natoms), "wrong shape, should be" - " (Natoms,Natoms) entries") - assert_almost_equal(d.min(), 0.11981228170520701, self.prec, - err_msg="wrong minimum distance value") - assert_almost_equal(d.max(), 53.572192429459619, self.prec, - err_msg="wrong maximum distance value") + assert_equal( + d.shape, + (natoms, natoms), + "wrong shape, should be" " (Natoms,Natoms) entries", + ) + assert_almost_equal( + d.min(), + 0.11981228170520701, + self.prec, + err_msg="wrong minimum distance value", + ) + assert_almost_equal( + d.max(), + 53.572192429459619, + self.prec, + err_msg="wrong maximum distance value", + ) def test_periodic(self, DCD_Universe, backend): # boring with the current dcd as that has no PBC @@ -478,14 +585,26 @@ def test_periodic(self, DCD_Universe, backend): x0 = U.atoms.positions trajectory[10] x1 = U.atoms.positions - d = distances.distance_array(x0, x1, box=U.coord.dimensions, - backend=backend) - assert_equal(d.shape, (3341, 3341), "should be square matrix with" - " Natoms entries") - assert_almost_equal(d.min(), 0.11981228170520701, self.prec, - err_msg="wrong minimum distance value with PBC") - assert_almost_equal(d.max(), 53.572192429459619, self.prec, - err_msg="wrong maximum distance value with PBC") + d = distances.distance_array( + x0, x1, box=U.coord.dimensions, backend=backend + ) + assert_equal( + d.shape, + (3341, 3341), + "should be square matrix with" " Natoms entries", + ) + assert_almost_equal( + d.min(), + 0.11981228170520701, + self.prec, + err_msg="wrong minimum distance value with PBC", + ) + assert_almost_equal( + d.max(), + 53.572192429459619, + self.prec, + err_msg="wrong maximum distance value with PBC", + ) def test_atomgroup_simple(self, DCD_Universe, DCD_Universe2, backend): # need two copies as moving ts updates underlying array on atomgroup @@ -499,53 +618,77 @@ def test_atomgroup_simple(self, DCD_Universe, DCD_Universe2, backend): trajectory2[10] x1 = U2.select_atoms("all") d = distances.distance_array(x0, x1, backend=backend) - assert_equal(d.shape, (3341, 3341), "wrong shape (should be" - " (Natoms,Natoms))") - assert_almost_equal(d.min(), 0.11981228170520701, self.prec, - err_msg="wrong minimum distance value") - assert_almost_equal(d.max(), 53.572192429459619, self.prec, - err_msg="wrong maximum distance value") + assert_equal( + d.shape, (3341, 3341), "wrong shape (should be" " (Natoms,Natoms))" + ) + assert_almost_equal( + d.min(), + 0.11981228170520701, + self.prec, + err_msg="wrong minimum distance value", + ) + assert_almost_equal( + d.max(), + 53.572192429459619, + self.prec, + err_msg="wrong maximum distance value", + ) # check no box and ortho box types and some slices - @pytest.mark.parametrize('box', [None, [50, 50, 50, 90, 90, 90]]) - @pytest.mark.parametrize("sel, np_slice", [("all", np.s_[:, :]), - ("index 0 to 8 ", np.s_[0:9, :]), - ("index 9", np.s_[8, :])]) - def test_atomgroup_matches_numpy(self, DCD_Universe, backend, sel, - np_slice, box): + @pytest.mark.parametrize("box", [None, [50, 50, 50, 90, 90, 90]]) + @pytest.mark.parametrize( + "sel, np_slice", + [ + ("all", np.s_[:, :]), + ("index 0 to 8 ", np.s_[0:9, :]), + ("index 9", np.s_[8, :]), + ], + ) + def test_atomgroup_matches_numpy( + self, DCD_Universe, backend, sel, np_slice, box + ): U = DCD_Universe x0_ag = U.select_atoms(sel) x0_arr = U.atoms.positions[np_slice] x1_ag = U.select_atoms(sel) x1_arr = U.atoms.positions[np_slice] - d_ag = distances.distance_array(x0_ag, x1_ag, box=box, - backend=backend) - d_arr = distances.distance_array(x0_arr, x1_arr, box=box, - backend=backend) - assert_allclose(d_ag, d_arr, - err_msg="AtomGroup and NumPy distances do not match") + d_ag = distances.distance_array(x0_ag, x1_ag, box=box, backend=backend) + d_arr = distances.distance_array( + x0_arr, x1_arr, box=box, backend=backend + ) + assert_allclose( + d_ag, d_arr, err_msg="AtomGroup and NumPy distances do not match" + ) # check triclinic box and some slices - @pytest.mark.parametrize("sel, np_slice", [("all", np.s_[:, :]), - ("index 0 to 8 ", np.s_[0:9, :]), - ("index 9", np.s_[8, :])]) - def test_atomgroup_matches_numpy_tric(self, Triclinic_Universe, backend, - sel, np_slice): + @pytest.mark.parametrize( + "sel, np_slice", + [ + ("all", np.s_[:, :]), + ("index 0 to 8 ", np.s_[0:9, :]), + ("index 9", np.s_[8, :]), + ], + ) + def test_atomgroup_matches_numpy_tric( + self, Triclinic_Universe, backend, sel, np_slice + ): U = Triclinic_Universe x0_ag = U.select_atoms(sel) x0_arr = U.atoms.positions[np_slice] x1_ag = U.select_atoms(sel) x1_arr = U.atoms.positions[np_slice] - d_ag = distances.distance_array(x0_ag, x1_ag, box=U.coord.dimensions, - backend=backend) - d_arr = distances.distance_array(x0_arr, x1_arr, - box=U.coord.dimensions, - backend=backend) - assert_allclose(d_ag, d_arr, - err_msg="AtomGroup and NumPy distances do not match") + d_ag = distances.distance_array( + x0_ag, x1_ag, box=U.coord.dimensions, backend=backend + ) + d_arr = distances.distance_array( + x0_arr, x1_arr, box=U.coord.dimensions, backend=backend + ) + assert_allclose( + d_ag, d_arr, err_msg="AtomGroup and NumPy distances do not match" + ) -@pytest.mark.parametrize('backend', ['serial', 'openmp']) +@pytest.mark.parametrize("backend", ["serial", "openmp"]) class TestSelfDistanceArrayDCD_TRIC(object): prec = 5 @@ -556,11 +699,21 @@ def test_simple(self, DCD_Universe, backend): x0 = U.atoms.positions d = distances.self_distance_array(x0, backend=backend) N = 3341 * (3341 - 1) / 2 - assert_equal(d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))") - assert_almost_equal(d.min(), 0.92905562402529318, self.prec, - err_msg="wrong minimum distance value") - assert_almost_equal(d.max(), 52.4702570624190590, self.prec, - err_msg="wrong maximum distance value") + assert_equal( + d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))" + ) + assert_almost_equal( + d.min(), + 0.92905562402529318, + self.prec, + err_msg="wrong minimum distance value", + ) + assert_almost_equal( + d.max(), + 52.4702570624190590, + self.prec, + err_msg="wrong maximum distance value", + ) def test_outarray(self, DCD_Universe, backend): U = DCD_Universe @@ -571,11 +724,21 @@ def test_outarray(self, DCD_Universe, backend): N = natoms * (natoms - 1) // 2 d = np.zeros((N,), np.float64) distances.self_distance_array(x0, result=d, backend=backend) - assert_equal(d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))") - assert_almost_equal(d.min(), 0.92905562402529318, self.prec, - err_msg="wrong minimum distance value") - assert_almost_equal(d.max(), 52.4702570624190590, self.prec, - err_msg="wrong maximum distance value") + assert_equal( + d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))" + ) + assert_almost_equal( + d.min(), + 0.92905562402529318, + self.prec, + err_msg="wrong minimum distance value", + ) + assert_almost_equal( + d.max(), + 52.4702570624190590, + self.prec, + err_msg="wrong maximum distance value", + ) def test_periodic(self, DCD_Universe, backend): # boring with the current dcd as that has no PBC @@ -585,13 +748,24 @@ def test_periodic(self, DCD_Universe, backend): x0 = U.atoms.positions natoms = len(U.atoms) N = natoms * (natoms - 1) / 2 - d = distances.self_distance_array(x0, box=U.coord.dimensions, - backend=backend) - assert_equal(d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))") - assert_almost_equal(d.min(), 0.92905562402529318, self.prec, - err_msg="wrong minimum distance value with PBC") - assert_almost_equal(d.max(), 52.4702570624190590, self.prec, - err_msg="wrong maximum distance value with PBC") + d = distances.self_distance_array( + x0, box=U.coord.dimensions, backend=backend + ) + assert_equal( + d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))" + ) + assert_almost_equal( + d.min(), + 0.92905562402529318, + self.prec, + err_msg="wrong minimum distance value with PBC", + ) + assert_almost_equal( + d.max(), + 52.4702570624190590, + self.prec, + err_msg="wrong maximum distance value with PBC", + ) def test_atomgroup_simple(self, DCD_Universe, backend): U = DCD_Universe @@ -600,49 +774,68 @@ def test_atomgroup_simple(self, DCD_Universe, backend): x0 = U.select_atoms("all") d = distances.self_distance_array(x0, backend=backend) N = 3341 * (3341 - 1) / 2 - assert_equal(d.shape, (N,), "wrong shape (should be" - " (Natoms*(Natoms-1)/2,))") - assert_almost_equal(d.min(), 0.92905562402529318, self.prec, - err_msg="wrong minimum distance value") - assert_almost_equal(d.max(), 52.4702570624190590, self.prec, - err_msg="wrong maximum distance value") + assert_equal( + d.shape, (N,), "wrong shape (should be" " (Natoms*(Natoms-1)/2,))" + ) + assert_almost_equal( + d.min(), + 0.92905562402529318, + self.prec, + err_msg="wrong minimum distance value", + ) + assert_almost_equal( + d.max(), + 52.4702570624190590, + self.prec, + err_msg="wrong maximum distance value", + ) # check no box and ortho box types and some slices - @pytest.mark.parametrize('box', [None, [50, 50, 50, 90, 90, 90]]) - @pytest.mark.parametrize("sel, np_slice", [("all", np.s_[:, :]), - ("index 0 to 8 ", np.s_[0:9, :]), - ("index 9", np.s_[8, :])]) - def test_atomgroup_matches_numpy(self, DCD_Universe, backend, - sel, np_slice, box): + @pytest.mark.parametrize("box", [None, [50, 50, 50, 90, 90, 90]]) + @pytest.mark.parametrize( + "sel, np_slice", + [ + ("all", np.s_[:, :]), + ("index 0 to 8 ", np.s_[0:9, :]), + ("index 9", np.s_[8, :]), + ], + ) + def test_atomgroup_matches_numpy( + self, DCD_Universe, backend, sel, np_slice, box + ): U = DCD_Universe x0_ag = U.select_atoms(sel) x0_arr = U.atoms.positions[np_slice] - d_ag = distances.self_distance_array(x0_ag, box=box, - backend=backend) - d_arr = distances.self_distance_array(x0_arr, box=box, - backend=backend) - assert_allclose(d_ag, d_arr, - err_msg="AtomGroup and NumPy distances do not match") + d_ag = distances.self_distance_array(x0_ag, box=box, backend=backend) + d_arr = distances.self_distance_array(x0_arr, box=box, backend=backend) + assert_allclose( + d_ag, d_arr, err_msg="AtomGroup and NumPy distances do not match" + ) # check triclinic box and some slices - @pytest.mark.parametrize("sel, np_slice", [ - ("index 0 to 8 ", np.s_[0:9, :]), - ("index 9", np.s_[8, :])]) - def test_atomgroup_matches_numpy_tric(self, Triclinic_Universe, backend, - sel, np_slice): + @pytest.mark.parametrize( + "sel, np_slice", + [("index 0 to 8 ", np.s_[0:9, :]), ("index 9", np.s_[8, :])], + ) + def test_atomgroup_matches_numpy_tric( + self, Triclinic_Universe, backend, sel, np_slice + ): U = Triclinic_Universe x0_ag = U.select_atoms(sel) x0_arr = U.atoms.positions[np_slice] - d_ag = distances.self_distance_array(x0_ag, box=U.coord.dimensions, - backend=backend) - d_arr = distances.self_distance_array(x0_arr, box=U.coord.dimensions, - backend=backend) - assert_allclose(d_ag, d_arr, - err_msg="AtomGroup and NumPy distances do not match") + d_ag = distances.self_distance_array( + x0_ag, box=U.coord.dimensions, backend=backend + ) + d_arr = distances.self_distance_array( + x0_arr, box=U.coord.dimensions, backend=backend + ) + assert_allclose( + d_ag, d_arr, err_msg="AtomGroup and NumPy distances do not match" + ) -@pytest.mark.parametrize('backend', ['serial', 'openmp']) +@pytest.mark.parametrize("backend", ["serial", "openmp"]) class TestTriclinicDistances(object): """Unit tests for the Triclinic PBC functions. Tests: @@ -686,7 +879,7 @@ def S_mol_single(TRIC): S_mol2 = TRIC.atoms[390].position return S_mol1, S_mol2 - @pytest.mark.parametrize('S_mol', [S_mol, S_mol_single], indirect=True) + @pytest.mark.parametrize("S_mol", [S_mol, S_mol_single], indirect=True) def test_transforms(self, S_mol, tri_vec_box, box, backend): # To check the cython coordinate transform, the same operation is done in numpy # Is a matrix multiplication of Coords x tri_vec_box = NewCoords, so can use np.dot @@ -697,26 +890,48 @@ def test_transforms(self, S_mol, tri_vec_box, box, backend): R_mol2 = distances.transform_StoR(S_mol2, box, backend=backend) R_np2 = np.dot(S_mol2, tri_vec_box) - assert_almost_equal(R_mol1, R_np1, self.prec, err_msg="StoR transform failed for S_mol1") - assert_almost_equal(R_mol2, R_np2, self.prec, err_msg="StoR transform failed for S_mol2") + assert_almost_equal( + R_mol1, + R_np1, + self.prec, + err_msg="StoR transform failed for S_mol1", + ) + assert_almost_equal( + R_mol2, + R_np2, + self.prec, + err_msg="StoR transform failed for S_mol2", + ) # Round trip test S_test1 = distances.transform_RtoS(R_mol1, box, backend=backend) S_test2 = distances.transform_RtoS(R_mol2, box, backend=backend) - assert_almost_equal(S_test1, S_mol1, self.prec, err_msg="Round trip 1 failed in transform") - assert_almost_equal(S_test2, S_mol2, self.prec, err_msg="Round trip 2 failed in transform") + assert_almost_equal( + S_test1, + S_mol1, + self.prec, + err_msg="Round trip 1 failed in transform", + ) + assert_almost_equal( + S_test2, + S_mol2, + self.prec, + err_msg="Round trip 2 failed in transform", + ) def test_selfdist(self, S_mol, box, tri_vec_box, backend): S_mol1, S_mol2 = S_mol R_coords = distances.transform_StoR(S_mol1, box, backend=backend) # Transform functions are tested elsewhere so taken as working here - dists = distances.self_distance_array(R_coords, box=box, backend=backend) + dists = distances.self_distance_array( + R_coords, box=box, backend=backend + ) # Manually calculate self_distance_array manual = np.zeros(len(dists), dtype=np.float64) distpos = 0 for i, Ri in enumerate(R_coords): - for Rj in R_coords[i + 1:]: + for Rj in R_coords[i + 1 :]: Rij = Rj - Ri Rij -= round(Rij[2] / tri_vec_box[2][2]) * tri_vec_box[2] Rij -= round(Rij[1] / tri_vec_box[1][1]) * tri_vec_box[1] @@ -725,18 +940,24 @@ def test_selfdist(self, S_mol, box, tri_vec_box, backend): manual[distpos] = Rij # and done, phew distpos += 1 - assert_almost_equal(dists, manual, self.prec, - err_msg="self_distance_array failed with input 1") + assert_almost_equal( + dists, + manual, + self.prec, + err_msg="self_distance_array failed with input 1", + ) # Do it again for input 2 (has wider separation in points) R_coords = distances.transform_StoR(S_mol2, box, backend=backend) # Transform functions are tested elsewhere so taken as working here - dists = distances.self_distance_array(R_coords, box=box, backend=backend) + dists = distances.self_distance_array( + R_coords, box=box, backend=backend + ) # Manually calculate self_distance_array manual = np.zeros(len(dists), dtype=np.float64) distpos = 0 for i, Ri in enumerate(R_coords): - for Rj in R_coords[i + 1:]: + for Rj in R_coords[i + 1 :]: Rij = Rj - Ri Rij -= round(Rij[2] / tri_vec_box[2][2]) * tri_vec_box[2] Rij -= round(Rij[1] / tri_vec_box[1][1]) * tri_vec_box[1] @@ -745,8 +966,12 @@ def test_selfdist(self, S_mol, box, tri_vec_box, backend): manual[distpos] = Rij # and done, phew distpos += 1 - assert_almost_equal(dists, manual, self.prec, - err_msg="self_distance_array failed with input 2") + assert_almost_equal( + dists, + manual, + self.prec, + err_msg="self_distance_array failed with input 2", + ) def test_distarray(self, S_mol, tri_vec_box, box, backend): S_mol1, S_mol2 = S_mol @@ -755,7 +980,9 @@ def test_distarray(self, S_mol, tri_vec_box, box, backend): R_mol2 = distances.transform_StoR(S_mol2, box, backend=backend) # Try with box - dists = distances.distance_array(R_mol1, R_mol2, box=box, backend=backend) + dists = distances.distance_array( + R_mol1, R_mol2, box=box, backend=backend + ) # Manually calculate distance_array manual = np.zeros((len(R_mol1), len(R_mol2))) for i, Ri in enumerate(R_mol1): @@ -767,36 +994,46 @@ def test_distarray(self, S_mol, tri_vec_box, box, backend): Rij = np.linalg.norm(Rij) # find norm of Rij vector manual[i][j] = Rij - assert_almost_equal(dists, manual, self.prec, - err_msg="distance_array failed with box") + assert_almost_equal( + dists, manual, self.prec, err_msg="distance_array failed with box" + ) def test_pbc_dist(self, S_mol, box, backend): S_mol1, S_mol2 = S_mol results = np.array([[37.629944]]) - dists = distances.distance_array(S_mol1, S_mol2, box=box, backend=backend) + dists = distances.distance_array( + S_mol1, S_mol2, box=box, backend=backend + ) - assert_almost_equal(dists, results, self.prec, - err_msg="distance_array failed to retrieve PBC distance") + assert_almost_equal( + dists, + results, + self.prec, + err_msg="distance_array failed to retrieve PBC distance", + ) def test_pbc_wrong_wassenaar_distance(self, backend): box = [2, 2, 2, 60, 60, 60] tri_vec_box = mdamath.triclinic_vectors(box) a, b, c = tri_vec_box point_a = a + b - point_b = .5 * point_a - dist = distances.distance_array(point_a, point_b, box=box, backend=backend) + point_b = 0.5 * point_a + dist = distances.distance_array( + point_a, point_b, box=box, backend=backend + ) assert_almost_equal(dist[0, 0], 1) # check that our distance is different from the wassenaar distance as # expected. assert np.linalg.norm(point_a - point_b) != dist[0, 0] -@pytest.mark.parametrize("box", +@pytest.mark.parametrize( + "box", [ None, - np.array([10., 15., 20., 90., 90., 90.]), # otrho - np.array([10., 15., 20., 70.53571, 109.48542, 70.518196]), # TRIC - ] + np.array([10.0, 15.0, 20.0, 90.0, 90.0, 90.0]), # otrho + np.array([10.0, 15.0, 20.0, 70.53571, 109.48542, 70.518196]), # TRIC + ], ) def test_issue_3725(box): """ @@ -806,10 +1043,10 @@ def test_issue_3725(box): random_coords = np.random.uniform(-50, 50, (1000, 3)) self_da_serial = distances.self_distance_array( - random_coords, box=box, backend='serial' + random_coords, box=box, backend="serial" ) self_da_openmp = distances.self_distance_array( - random_coords, box=box, backend='openmp' + random_coords, box=box, backend="openmp" ) np.testing.assert_allclose(self_da_serial, self_da_openmp) @@ -823,10 +1060,12 @@ def conv_dtype_if_ndarr(a, dtype): def convert_position_dtype_if_ndarray(a, b, c, d, dtype): - return (conv_dtype_if_ndarr(a, dtype), - conv_dtype_if_ndarr(b, dtype), - conv_dtype_if_ndarr(c, dtype), - conv_dtype_if_ndarr(d, dtype)) + return ( + conv_dtype_if_ndarr(a, dtype), + conv_dtype_if_ndarr(b, dtype), + conv_dtype_if_ndarr(c, dtype), + conv_dtype_if_ndarr(d, dtype), + ) def distopia_conditional_backend(): @@ -848,29 +1087,33 @@ def test_HAS_DISTOPIA_incompatible_distopia(): # 0.3.0 functions (from # https://github.com/MDAnalysis/distopia/blob/main/distopia/__init__.py # __all__): - mock_distopia_030 = Mock(spec=[ - 'calc_bonds_ortho', - 'calc_bonds_no_box', - 'calc_bonds_triclinic', - 'calc_angles_no_box', - 'calc_angles_ortho', - 'calc_angles_triclinic', - 'calc_dihedrals_no_box', - 'calc_dihedrals_ortho', - 'calc_dihedrals_triclinic', - 'calc_distance_array_no_box', - 'calc_distance_array_ortho', - 'calc_distance_array_triclinic', - 'calc_self_distance_array_no_box', - 'calc_self_distance_array_ortho', - 'calc_self_distance_array_triclinic', - ]) + mock_distopia_030 = Mock( + spec=[ + "calc_bonds_ortho", + "calc_bonds_no_box", + "calc_bonds_triclinic", + "calc_angles_no_box", + "calc_angles_ortho", + "calc_angles_triclinic", + "calc_dihedrals_no_box", + "calc_dihedrals_ortho", + "calc_dihedrals_triclinic", + "calc_distance_array_no_box", + "calc_distance_array_ortho", + "calc_distance_array_triclinic", + "calc_self_distance_array_no_box", + "calc_self_distance_array_ortho", + "calc_self_distance_array_triclinic", + ] + ) with patch.dict("sys.modules", {"distopia": mock_distopia_030}): - with pytest.warns(RuntimeWarning, - match="Install 'distopia>=0.2.0,<0.3.0' to"): + with pytest.warns( + RuntimeWarning, match="Install 'distopia>=0.2.0,<0.3.0' to" + ): import MDAnalysis.lib._distopia assert not MDAnalysis.lib._distopia.HAS_DISTOPIA + 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 @@ -880,23 +1123,61 @@ class TestCythonFunctions(object): @staticmethod @pytest.fixture() def box(): - return np.array([10., 10., 10., 90., 90., 90.], dtype=np.float32) + return np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0], dtype=np.float32) @staticmethod @pytest.fixture() def triclinic_box(): - box_vecs = np.array([[10., 0., 0.], [1., 10., 0., ], [1., 0., 10.]], - dtype=np.float32) + box_vecs = np.array( + [ + [10.0, 0.0, 0.0], + [1.0, 10.0, 0.0], + [1.0, 0.0, 10.0], + ], + dtype=np.float32, + ) return mdamath.triclinic_box(box_vecs[0], box_vecs[1], box_vecs[2]) @staticmethod @pytest.fixture() def positions(): # dummy atom data - a = np.array([[0., 0., 0.], [0., 0., 0.], [0., 11., 0.], [1., 1., 1.]], dtype=np.float32) - b = np.array([[0., 0., 0.], [1., 1., 1.], [0., 0., 0.], [29., -21., 99.]], dtype=np.float32) - c = np.array([[0., 0., 0.], [2., 2., 2.], [11., 0., 0.], [1., 9., 9.]], dtype=np.float32) - d = np.array([[0., 0., 0.], [3., 3., 3.], [11., -11., 0.], [65., -65., 65.]], dtype=np.float32) + a = np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 11.0, 0.0], + [1.0, 1.0, 1.0], + ], + dtype=np.float32, + ) + b = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + [0.0, 0.0, 0.0], + [29.0, -21.0, 99.0], + ], + dtype=np.float32, + ) + c = np.array( + [ + [0.0, 0.0, 0.0], + [2.0, 2.0, 2.0], + [11.0, 0.0, 0.0], + [1.0, 9.0, 9.0], + ], + dtype=np.float32, + ) + d = np.array( + [ + [0.0, 0.0, 0.0], + [3.0, 3.0, 3.0], + [11.0, -11.0, 0.0], + [65.0, -65.0, 65.0], + ], + dtype=np.float32, + ) return a, b, c, d @staticmethod @@ -904,8 +1185,10 @@ def positions(): def positions_atomgroups(positions): a, b, c, d = positions arrs = [a, b, c, d] - universes = [MDAnalysis.Universe.empty(arr.shape[0], - trajectory=True) for arr in arrs] + universes = [ + MDAnalysis.Universe.empty(arr.shape[0], trajectory=True) + for arr in arrs + ] for u, a in zip(universes, arrs): u.atoms.positions = a return tuple([u.atoms for u in universes]) @@ -914,14 +1197,15 @@ def positions_atomgroups(positions): @pytest.fixture() def wronglength(): # has a different length to other inputs and should raise ValueError - return np.array([[0., 0., 0.], [3., 3., 3.]], - dtype=np.float32) + return np.array([[0.0, 0.0, 0.0], [3.0, 3.0, 3.0]], dtype=np.float32) # coordinate shifts for single coord tests - shifts = [((0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)), # no shifting - ((1, 0, 0), (0, 1, 1), (0, 0, 1), (1, 1, 0)), # single box lengths - ((-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 + shifts = [ + ((0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)), # no shifting + ((1, 0, 0), (0, 1, 1), (0, 0, 1), (1, 1, 0)), # single box lengths + ((-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"]) @@ -930,27 +1214,51 @@ 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) dists = distances.calc_bonds(a, b, backend=backend) - assert_equal(len(dists), 4, err_msg="calc_bonds results have wrong length") + assert_equal( + len(dists), 4, err_msg="calc_bonds results have wrong length" + ) dists_pbc = distances.calc_bonds(a, b, box=box, backend=backend) - #tests 0 length - assert_almost_equal(dists[0], 0.0, self.prec, err_msg="Zero length calc_bonds fail") - assert_almost_equal(dists[1], 1.7320508075688772, self.prec, - err_msg="Standard length calc_bonds fail") # arbitrary length check + # tests 0 length + assert_almost_equal( + dists[0], 0.0, self.prec, err_msg="Zero length calc_bonds fail" + ) + assert_almost_equal( + dists[1], + 1.7320508075688772, + self.prec, + err_msg="Standard length calc_bonds fail", + ) # arbitrary length check # PBC checks, 2 without, 2 with - assert_almost_equal(dists[2], 11.0, self.prec, - err_msg="PBC check #1 w/o box") # pbc check 1, subtract single box length - assert_almost_equal(dists_pbc[2], 1.0, self.prec, - err_msg="PBC check #1 with box") - assert_almost_equal(dists[3], 104.26888318, self.prec, # pbc check 2, subtract multiple box - err_msg="PBC check #2 w/o box") # lengths in all directions - assert_almost_equal(dists_pbc[3], 3.46410072, self.prec, - err_msg="PBC check #w with box") + assert_almost_equal( + dists[2], 11.0, self.prec, err_msg="PBC check #1 w/o box" + ) # pbc check 1, subtract single box length + assert_almost_equal( + dists_pbc[2], 1.0, self.prec, err_msg="PBC check #1 with box" + ) + assert_almost_equal( + dists[3], + 104.26888318, + self.prec, # pbc check 2, subtract multiple box + err_msg="PBC check #2 w/o box", + ) # lengths in all directions + 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) - badbox2 = np.array([[10., 10.], [10., 10., ]], dtype=np.float32) + badbox1 = np.array([10.0, 10.0, 10.0], dtype=np.float64) + badbox2 = np.array( + [ + [10.0, 10.0], + [10.0, 10.0], + ], + dtype=np.float32, + ) with pytest.raises(ValueError): distances.calc_bonds(a, b, box=badbox1, backend=backend) @@ -968,18 +1276,25 @@ def test_bonds_badresult(self, positions, backend): @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): + 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") + 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("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) + box = np.array([10, 20, 30, 90.0, 90.0, 90.0], dtype=np.float32) coords = np.array([[1, 1, 1], [3, 1, 1]], dtype=np.float32) @@ -989,7 +1304,9 @@ def test_bonds_single_coords(self, shift, periodic, backend): coords[1] += shift2 * box[:3] box = box if periodic else None - result = distances.calc_bonds(coords[0], coords[1], box, backend=backend) + result = distances.calc_bonds( + coords[0], coords[1], box, backend=backend + ) reference = 2.0 if periodic else np.linalg.norm(coords[0] - coords[1]) @@ -1003,15 +1320,29 @@ def test_angles(self, backend, dtype, pos, request): a, b, c, d = convert_position_dtype_if_ndarray(a, b, c, d, dtype) angles = distances.calc_angles(a, b, c, backend=backend) # Check calculated values - assert_equal(len(angles), 4, err_msg="calc_angles results have wrong length") + assert_equal( + len(angles), 4, err_msg="calc_angles results have wrong length" + ) # assert_almost_equal(angles[0], 0.0, self.prec, # err_msg="Zero length angle calculation failed") # What should this be? - assert_almost_equal(angles[1], np.pi, self.prec, - err_msg="180 degree angle calculation failed") - assert_almost_equal(np.rad2deg(angles[2]), 90., self.prec, - err_msg="Ninety degree angle in calc_angles failed") - assert_almost_equal(angles[3], 0.098174833, self.prec, - err_msg="Small angle failed in calc_angles") + assert_almost_equal( + angles[1], + np.pi, + self.prec, + err_msg="180 degree angle calculation failed", + ) + assert_almost_equal( + np.rad2deg(angles[2]), + 90.0, + self.prec, + err_msg="Ninety degree angle in calc_angles failed", + ) + 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): @@ -1044,7 +1375,7 @@ def test_angles_single_coords(self, case, shift, periodic, backend): def manual_angle(x, y, z): return mdamath.angle(y - x, y - z) - box = np.array([10, 20, 30, 90., 90., 90.], dtype=np.float32) + box = np.array([10, 20, 30, 90.0, 90.0, 90.0], dtype=np.float32) (a, b, c), ref = case shift1, shift2, shift3, _ = shift @@ -1066,12 +1397,25 @@ def test_dihedrals(self, backend, dtype, pos, request): a, b, c, d = convert_position_dtype_if_ndarray(a, b, c, d, dtype) dihedrals = distances.calc_dihedrals(a, b, c, d, backend=backend) # Check calculated values - assert_equal(len(dihedrals), 4, err_msg="calc_dihedrals results have wrong length") + assert_equal( + len(dihedrals), + 4, + err_msg="calc_dihedrals results have wrong length", + ) assert np.isnan(dihedrals[0]), "Zero length dihedral failed" assert np.isnan(dihedrals[1]), "Straight line dihedral failed" - assert_almost_equal(dihedrals[2], np.pi, self.prec, err_msg="180 degree dihedral failed") - assert_almost_equal(dihedrals[3], -0.50714064, self.prec, - err_msg="arbitrary dihedral angle failed") + assert_almost_equal( + dihedrals[2], + np.pi, + self.prec, + err_msg="180 degree dihedral failed", + ) + 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): @@ -1094,44 +1438,52 @@ def test_dihedrals_bad_result(self, positions, backend): badresult = np.zeros(len(a) - 1) # Bad result array with pytest.raises(ValueError): - distances.calc_dihedrals(a, b, c, d, result=badresult, backend=backend) + 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 + [[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 + [[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 + [[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 + [[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 + [[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 + [[1, 2, 1], [1, 1, 1], [2, 1, 1], [2, 0, 2]], + dtype=np.float32, ), 0.75 * np.pi, ), # 135 @@ -1144,7 +1496,7 @@ 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) - box = np.array([10., 10., 10., 90., 90., 90.], dtype=np.float32) + box = np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0], dtype=np.float32) (a, b, c, d), ref = case @@ -1181,7 +1533,9 @@ def test_numpy_compliance_angles(self, positions, backend): 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)]) + 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:], @@ -1198,12 +1552,18 @@ def test_numpy_compliance_dihedrals(self, positions, 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(dihedrals, dihedrals_numpy, self.prec, - err_msg="Cython dihedrals didn't match numpy calculations") + dihedrals_numpy = np.array( + [mdamath.dihedral(x, y, z) for x, y, z in zip(ab, bc, cd)] + ) + assert_almost_equal( + dihedrals, + dihedrals_numpy, + self.prec, + err_msg="Cython dihedrals didn't match numpy calculations", + ) -@pytest.mark.parametrize('backend', ['serial', 'openmp']) +@pytest.mark.parametrize("backend", ["serial", "openmp"]) class Test_apply_PBC(object): prec = 6 @@ -1237,23 +1597,30 @@ def Triclinic_universe_ag_box(self, Triclinic_Universe): box = U.dimensions return atoms, box - @pytest.mark.parametrize('pos', ['DCD_universe_pos', 'DCD_universe_ag']) + @pytest.mark.parametrize("pos", ["DCD_universe_pos", "DCD_universe_ag"]) def test_ortho_PBC(self, backend, pos, request, DCD_universe_pos): positions = request.getfixturevalue(pos) - box = np.array([2.5, 2.5, 3.5, 90., 90., 90.], dtype=np.float32) + box = np.array([2.5, 2.5, 3.5, 90.0, 90.0, 90.0], dtype=np.float32) with pytest.raises(ValueError): cyth1 = distances.apply_PBC(positions, box[:3], backend=backend) cyth2 = distances.apply_PBC(positions, box, backend=backend) - reference = (DCD_universe_pos - - np.floor(DCD_universe_pos / box[:3]) * box[:3]) + reference = ( + DCD_universe_pos - np.floor(DCD_universe_pos / box[:3]) * box[:3] + ) - assert_almost_equal(cyth2, reference, self.prec, - err_msg="Ortho apply_PBC #2 failed comparison with np") + assert_almost_equal( + cyth2, + reference, + self.prec, + err_msg="Ortho apply_PBC #2 failed comparison with np", + ) - @pytest.mark.parametrize('pos', ['Triclinic_universe_pos_box', - 'Triclinic_universe_ag_box']) + @pytest.mark.parametrize( + "pos", ["Triclinic_universe_pos_box", "Triclinic_universe_ag_box"] + ) def test_tric_PBC(self, backend, pos, request): positions, box = request.getfixturevalue(pos) + def numpy_PBC(coords, box): # need this to allow both AtomGroup and array if isinstance(coords, MDAnalysis.core.AtomGroup): @@ -1271,8 +1638,12 @@ def numpy_PBC(coords, box): reference = numpy_PBC(positions, box) - assert_almost_equal(cyth1, reference, decimal=4, - err_msg="Triclinic apply_PBC failed comparison with np") + assert_almost_equal( + cyth1, + reference, + decimal=4, + err_msg="Triclinic apply_PBC failed comparison with np", + ) box = np.array([10, 7, 3, 45, 60, 90], dtype=np.float32) r = np.array([5.75, 0.36066014, 0.75], dtype=np.float32) @@ -1283,14 +1654,19 @@ def numpy_PBC(coords, box): def test_coords_strictly_in_central_image_ortho(self, backend): box = np.array([10.1, 10.1, 10.1, 90.0, 90.0, 90.0], dtype=np.float32) # coordinates just below lower or exactly at the upper box boundaries: - coords = np.array([[-1.0e-7, -1.0e-7, -1.0e-7], - [-1.0e-7, -1.0e-7, box[2]], - [-1.0e-7, box[1], -1.0e-7], - [ box[0], -1.0e-7, -1.0e-7], - [ box[0], box[1], -1.0e-7], - [ box[0], -1.0e-7, box[2]], - [-1.0e-7, box[1], box[2]], - [ box[0], box[1], box[2]]], dtype=np.float32) + coords = np.array( + [ + [-1.0e-7, -1.0e-7, -1.0e-7], + [-1.0e-7, -1.0e-7, box[2]], + [-1.0e-7, box[1], -1.0e-7], + [box[0], -1.0e-7, -1.0e-7], + [box[0], box[1], -1.0e-7], + [box[0], -1.0e-7, box[2]], + [-1.0e-7, box[1], box[2]], + [box[0], box[1], box[2]], + ], + dtype=np.float32, + ) # Check that all test coordinates actually lie below the lower or # exactly at the upper box boundary: assert np.all((coords < 0.0) | (coords == box[:3])) @@ -1301,22 +1677,33 @@ def test_coords_strictly_in_central_image_ortho(self, backend): def test_coords_in_central_image_tric(self, backend): # Triclinic box corresponding to this box matrix: - tbx = np.array([[10.1 , 0. , 0. ], - [ 1.0100002, 10.1 , 0. ], - [ 1.0100006, 1.0100021, 10.1 ]], - dtype=np.float32) + tbx = np.array( + [ + [10.1, 0.0, 0.0], + [1.0100002, 10.1, 0.0], + [1.0100006, 1.0100021, 10.1], + ], + dtype=np.float32, + ) box = mdamath.triclinic_box(*tbx) # coordinates just below lower or exactly at the upper box boundaries: - coords = np.array([[ -1.0e-7, -1.0e-7, -1.0e-7], - [tbx[0, 0], -1.0e-7, -1.0e-7], - [ 1.01 , tbx[1, 1], -1.0e-7], - [ 1.01 , 1.01 , tbx[2, 2]], - [tbx[0, 0] + tbx[1, 0], tbx[1, 1], -1.0e-7], - [tbx[0, 0] + tbx[2, 0], 1.01, tbx[2, 2]], - [2.02, tbx[1, 1] + tbx[2, 1], tbx[2, 2]], - [tbx[0, 0] + tbx[1, 0] + tbx[2, 0], - tbx[1, 1] + tbx[2, 1], tbx[2, 2]]], - dtype=np.float32) + coords = np.array( + [ + [-1.0e-7, -1.0e-7, -1.0e-7], + [tbx[0, 0], -1.0e-7, -1.0e-7], + [1.01, tbx[1, 1], -1.0e-7], + [1.01, 1.01, tbx[2, 2]], + [tbx[0, 0] + tbx[1, 0], tbx[1, 1], -1.0e-7], + [tbx[0, 0] + tbx[2, 0], 1.01, tbx[2, 2]], + [2.02, tbx[1, 1] + tbx[2, 1], tbx[2, 2]], + [ + tbx[0, 0] + tbx[1, 0] + tbx[2, 0], + tbx[1, 1] + tbx[2, 1], + tbx[2, 2], + ], + ], + dtype=np.float32, + ) relcoords = distances.transform_RtoS(coords, box) # Check that all test coordinates actually lie below the lower or # exactly at the upper box boundary: @@ -1328,11 +1715,12 @@ def test_coords_in_central_image_tric(self, backend): assert np.all(relres < 1.0) -@pytest.mark.parametrize('backend', ['serial', 'openmp']) +@pytest.mark.parametrize("backend", ["serial", "openmp"]) class TestPeriodicAngles(object): """Test case for properly considering minimum image convention when calculating angles and dihedrals (Issue 172) """ + @staticmethod @pytest.fixture() def positions(): @@ -1361,7 +1749,12 @@ def test_angles(self, positions, backend): test4 = distances.calc_angles(a2, b2, c2, box=box, backend=backend) for val in [test1, test2, test3, test4]: - assert_almost_equal(ref, val, self.prec, err_msg="Min image in angle calculation failed") + assert_almost_equal( + ref, + val, + self.prec, + err_msg="Min image in angle calculation failed", + ) def test_dihedrals(self, positions, backend): a, b, c, d, box = positions @@ -1377,10 +1770,18 @@ def test_dihedrals(self, positions, backend): test2 = distances.calc_dihedrals(a, b2, c, d, box=box, backend=backend) test3 = distances.calc_dihedrals(a, b, c2, d, box=box, backend=backend) test4 = distances.calc_dihedrals(a, b, c, d2, box=box, backend=backend) - test5 = distances.calc_dihedrals(a2, b2, c2, d2, box=box, backend=backend) + test5 = distances.calc_dihedrals( + a2, b2, c2, d2, box=box, backend=backend + ) for val in [test1, test2, test3, test4, test5]: - assert_almost_equal(ref, val, self.prec, err_msg="Min image in dihedral calculation failed") + assert_almost_equal( + ref, + val, + self.prec, + err_msg="Min image in dihedral calculation failed", + ) + class TestInputUnchanged(object): """Tests ensuring that the following functions in MDAnalysis.lib.distances @@ -1397,87 +1798,100 @@ class TestInputUnchanged(object): * apply_PBC """ - boxes = ([1.0, 1.0, 1.0, 90.0, 90.0, 90.0], # orthorhombic - [1.0, 1.0, 1.0, 80.0, 80.0, 80.0], # triclinic - None) # no PBC + boxes = ( + [1.0, 1.0, 1.0, 90.0, 90.0, 90.0], # orthorhombic + [1.0, 1.0, 1.0, 80.0, 80.0, 80.0], # triclinic + None, + ) # no PBC @staticmethod @pytest.fixture() def coords(): # input coordinates, some outside the [1, 1, 1] box: - return [np.array([[0.1, 0.1, 0.1], [-0.9, -0.9, -0.9]], dtype=np.float32), - np.array([[0.1, 0.1, 1.9], [-0.9, -0.9, 0.9]], dtype=np.float32), - np.array([[0.1, 1.9, 1.9], [-0.9, 0.9, 0.9]], dtype=np.float32), - np.array([[0.1, 1.9, 0.1], [-0.9, 0.9, -0.9]], dtype=np.float32)] + return [ + np.array([[0.1, 0.1, 0.1], [-0.9, -0.9, -0.9]], dtype=np.float32), + np.array([[0.1, 0.1, 1.9], [-0.9, -0.9, 0.9]], dtype=np.float32), + np.array([[0.1, 1.9, 1.9], [-0.9, 0.9, 0.9]], dtype=np.float32), + np.array([[0.1, 1.9, 0.1], [-0.9, 0.9, -0.9]], dtype=np.float32), + ] @staticmethod @pytest.fixture() def coords_atomgroups(coords): - universes = [MDAnalysis.Universe.empty(arr.shape[0], trajectory=True) - for arr in coords] + universes = [ + MDAnalysis.Universe.empty(arr.shape[0], trajectory=True) + for arr in coords + ] for u, a in zip(universes, coords): u.atoms.positions = a return [u.atoms for u in universes] - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_input_unchanged_distance_array(self, coords, box, backend): crds = coords[:2] refs = [crd.copy() for crd in crds] - res = distances.distance_array(crds[0], crds[1], box=box, - backend=backend) + res = distances.distance_array( + 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_distance_array_atomgroup(self, coords_atomgroups, - box, backend): + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_input_unchanged_distance_array_atomgroup( + self, coords_atomgroups, box, backend + ): crds = coords_atomgroups[:2] refs = [crd.positions.copy() for crd in crds] - res = distances.distance_array(crds[0], crds[1], box=box, - backend=backend) + res = distances.distance_array( + crds[0], crds[1], box=box, backend=backend + ) assert_equal([crd.positions for crd in crds], refs) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_input_unchanged_self_distance_array(self, coords, box, backend): crd = coords[0] ref = crd.copy() res = distances.self_distance_array(crd, box=box, backend=backend) assert_equal(crd, ref) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) - def test_input_unchanged_self_distance_array_atomgroup(self, - coords_atomgroups, - box, backend): + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_input_unchanged_self_distance_array_atomgroup( + self, coords_atomgroups, box, backend + ): crd = coords_atomgroups[0] ref = crd.positions.copy() res = distances.self_distance_array(crd, box=box, backend=backend) assert_equal(crd.positions, ref) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('met', ["bruteforce", "pkdtree", "nsgrid", None]) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("met", ["bruteforce", "pkdtree", "nsgrid", None]) def test_input_unchanged_capped_distance(self, coords, box, met): crds = coords[:2] refs = [crd.copy() for crd in crds] - res = distances.capped_distance(crds[0], crds[1], max_cutoff=0.3, - box=box, method=met) + res = distances.capped_distance( + crds[0], crds[1], max_cutoff=0.3, box=box, method=met + ) assert_equal(crds, refs) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('met', ["bruteforce", "pkdtree", "nsgrid", None]) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("met", ["bruteforce", "pkdtree", "nsgrid", None]) def test_input_unchanged_self_capped_distance(self, coords, box, met): crd = coords[0] ref = crd.copy() r_cut = 0.25 - res = distances.self_capped_distance(crd, max_cutoff=r_cut, box=box, - method=met) + res = distances.self_capped_distance( + crd, max_cutoff=r_cut, box=box, method=met + ) assert_equal(crd, ref) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) - def test_input_unchanged_transform_RtoS_and_StoR(self, coords, box, backend): + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_input_unchanged_transform_RtoS_and_StoR( + self, coords, box, backend + ): crd = coords[0] ref = crd.copy() res = distances.transform_RtoS(crd, box, backend=backend) @@ -1505,61 +1919,69 @@ def test_input_unchanged_calc_bonds_atomgroup( res = distances.calc_bonds(crds[0], crds[1], box=box, backend=backend) assert_equal([crd.positions for crd in crds], refs) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_input_unchanged_calc_angles(self, coords, box, backend): crds = coords[:3] refs = [crd.copy() for crd in crds] - res = distances.calc_angles(crds[0], crds[1], crds[2], box=box, - backend=backend) + res = distances.calc_angles( + crds[0], crds[1], crds[2], box=box, backend=backend + ) assert_equal(crds, refs) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) - def test_input_unchanged_calc_angles_atomgroup(self, coords_atomgroups, - box, backend): + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_input_unchanged_calc_angles_atomgroup( + self, coords_atomgroups, box, backend + ): crds = coords_atomgroups[:3] refs = [crd.positions.copy() for crd in crds] - res = distances.calc_angles(crds[0], crds[1], crds[2], box=box, - backend=backend) + res = distances.calc_angles( + crds[0], crds[1], crds[2], box=box, backend=backend + ) assert_equal([crd.positions for crd in crds], refs) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_input_unchanged_calc_dihedrals(self, coords, box, backend): crds = coords refs = [crd.copy() for crd in crds] - res = distances.calc_dihedrals(crds[0], crds[1], crds[2], crds[3], - box=box, backend=backend) + res = distances.calc_dihedrals( + crds[0], crds[1], crds[2], crds[3], box=box, backend=backend + ) assert_equal(crds, refs) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) - def test_input_unchanged_calc_dihedrals_atomgroup(self, coords_atomgroups, - box, backend): + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_input_unchanged_calc_dihedrals_atomgroup( + self, coords_atomgroups, box, backend + ): crds = coords_atomgroups refs = [crd.positions.copy() for crd in crds] - res = distances.calc_dihedrals(crds[0], crds[1], crds[2], crds[3], - box=box, backend=backend) + res = distances.calc_dihedrals( + crds[0], crds[1], crds[2], crds[3], box=box, backend=backend + ) assert_equal([crd.positions for crd in crds], refs) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_input_unchanged_apply_PBC(self, coords, box, backend): crd = coords[0] ref = crd.copy() res = distances.apply_PBC(crd, box, backend=backend) assert_equal(crd, ref) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) - def test_input_unchanged_apply_PBC_atomgroup(self, coords_atomgroups, box, - backend): + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) + def test_input_unchanged_apply_PBC_atomgroup( + self, coords_atomgroups, box, backend + ): crd = coords_atomgroups[0] ref = crd.positions.copy() res = distances.apply_PBC(crd, box, backend=backend) assert_equal(crd.positions, ref) + class TestEmptyInputCoordinates(object): """Tests ensuring that the following functions in MDAnalysis.lib.distances do not choke on empty input coordinate arrays: @@ -1578,9 +2000,11 @@ class TestEmptyInputCoordinates(object): max_cut = 0.25 # max_cutoff parameter for *capped_distance() min_cut = 0.0 # optional min_cutoff parameter for *capped_distance() - boxes = ([1.0, 1.0, 1.0, 90.0, 90.0, 90.0], # orthorhombic - [1.0, 1.0, 1.0, 80.0, 80.0, 80.0], # triclinic - None) # no PBC + boxes = ( + [1.0, 1.0, 1.0, 90.0, 90.0, 90.0], # orthorhombic + [1.0, 1.0, 1.0, 80.0, 80.0, 80.0], # triclinic + None, + ) # no PBC @staticmethod @pytest.fixture() @@ -1588,60 +2012,73 @@ def empty_coord(): # empty coordinate array: return np.empty((0, 3), dtype=np.float32) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_empty_input_distance_array(self, empty_coord, box, backend): - res = distances.distance_array(empty_coord, empty_coord, box=box, - backend=backend) + res = distances.distance_array( + empty_coord, empty_coord, box=box, backend=backend + ) assert_equal(res, np.empty((0, 0), dtype=np.float64)) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_empty_input_self_distance_array(self, empty_coord, box, backend): - res = distances.self_distance_array(empty_coord, box=box, - backend=backend) + res = distances.self_distance_array( + empty_coord, box=box, backend=backend + ) assert_equal(res, np.empty((0,), dtype=np.float64)) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('min_cut', [min_cut, None]) - @pytest.mark.parametrize('ret_dist', [False, True]) - @pytest.mark.parametrize('met', ["bruteforce", "pkdtree", "nsgrid", None]) - def test_empty_input_capped_distance(self, empty_coord, min_cut, box, met, - ret_dist): - res = distances.capped_distance(empty_coord, empty_coord, - max_cutoff=self.max_cut, - min_cutoff=min_cut, box=box, method=met, - return_distances=ret_dist) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("min_cut", [min_cut, None]) + @pytest.mark.parametrize("ret_dist", [False, True]) + @pytest.mark.parametrize("met", ["bruteforce", "pkdtree", "nsgrid", None]) + def test_empty_input_capped_distance( + self, empty_coord, min_cut, box, met, ret_dist + ): + res = distances.capped_distance( + empty_coord, + empty_coord, + max_cutoff=self.max_cut, + min_cutoff=min_cut, + box=box, + method=met, + return_distances=ret_dist, + ) if ret_dist: assert_equal(res[0], np.empty((0, 2), dtype=np.int64)) assert_equal(res[1], np.empty((0,), dtype=np.float64)) else: assert_equal(res, np.empty((0, 2), dtype=np.int64)) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('min_cut', [min_cut, None]) - @pytest.mark.parametrize('ret_dist', [False, True]) - @pytest.mark.parametrize('met', ["bruteforce", "pkdtree", "nsgrid", None]) - def test_empty_input_self_capped_distance(self, empty_coord, min_cut, box, - met, ret_dist): - res = distances.self_capped_distance(empty_coord, - max_cutoff=self.max_cut, - min_cutoff=min_cut, box=box, - method=met, return_distances=ret_dist) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("min_cut", [min_cut, None]) + @pytest.mark.parametrize("ret_dist", [False, True]) + @pytest.mark.parametrize("met", ["bruteforce", "pkdtree", "nsgrid", None]) + def test_empty_input_self_capped_distance( + self, empty_coord, min_cut, box, met, ret_dist + ): + res = distances.self_capped_distance( + empty_coord, + max_cutoff=self.max_cut, + min_cutoff=min_cut, + box=box, + method=met, + return_distances=ret_dist, + ) if ret_dist: assert_equal(res[0], np.empty((0, 2), dtype=np.int64)) assert_equal(res[1], np.empty((0,), dtype=np.float64)) else: assert_equal(res, np.empty((0, 2), dtype=np.int64)) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_empty_input_transform_RtoS(self, empty_coord, box, backend): res = distances.transform_RtoS(empty_coord, box, backend=backend) assert_equal(res, empty_coord) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) 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) @@ -1649,26 +2086,34 @@ def test_empty_input_transform_StoR(self, empty_coord, box, backend): @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) + res = distances.calc_bonds( + empty_coord, empty_coord, box=box, backend=backend + ) assert_equal(res, np.empty((0,), dtype=np.float64)) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_empty_input_calc_angles(self, empty_coord, box, backend): - res = distances.calc_angles(empty_coord, empty_coord, empty_coord, - box=box, backend=backend) + res = distances.calc_angles( + empty_coord, empty_coord, empty_coord, box=box, backend=backend + ) assert_equal(res, np.empty((0,), dtype=np.float64)) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_empty_input_calc_dihedrals(self, empty_coord, box, backend): - res = distances.calc_dihedrals(empty_coord, empty_coord, empty_coord, - empty_coord, box=box, backend=backend) + res = distances.calc_dihedrals( + empty_coord, + empty_coord, + empty_coord, + empty_coord, + box=box, + backend=backend, + ) assert_equal(res, np.empty((0,), dtype=np.float64)) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_empty_input_apply_PBC(self, empty_coord, box, backend): res = distances.apply_PBC(empty_coord, box, backend=backend) assert_equal(res, empty_coord) @@ -1704,46 +2149,60 @@ class TestOutputTypes(object): * apply_PBC: - numpy.ndarray (shape=input.shape, dtype=numpy.float32) """ + max_cut = 0.25 # max_cutoff parameter for *capped_distance() min_cut = 0.0 # optional min_cutoff parameter for *capped_distance() - boxes = ([1.0, 1.0, 1.0, 90.0, 90.0, 90.0], # orthorhombic - [1.0, 1.0, 1.0, 80.0, 80.0, 80.0], # triclinic - None) # no PBC + boxes = ( + [1.0, 1.0, 1.0, 90.0, 90.0, 90.0], # orthorhombic + [1.0, 1.0, 1.0, 80.0, 80.0, 80.0], # triclinic + None, + ) # no PBC - coords = [np.empty((0, 3), dtype=np.float32), # empty coord array - np.array([[0.1, 0.1, 0.1]], dtype=np.float32), # coord array - np.array([0.1, 0.1, 0.1], dtype=np.float32), # single coord - np.array([[-1.1, -1.1, -1.1]], dtype=np.float32)] # outside box + coords = [ + np.empty((0, 3), dtype=np.float32), # empty coord array + np.array([[0.1, 0.1, 0.1]], dtype=np.float32), # coord array + np.array([0.1, 0.1, 0.1], dtype=np.float32), # single coord + np.array([[-1.1, -1.1, -1.1]], dtype=np.float32), + ] # outside box - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('incoords', list(comb(coords, 2))) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("incoords", list(comb(coords, 2))) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_output_type_distance_array(self, incoords, box, backend): res = distances.distance_array(*incoords, box=box, backend=backend) assert type(res) == np.ndarray - assert res.shape == (incoords[0].shape[0] % 2, incoords[1].shape[0] % 2) + assert res.shape == ( + incoords[0].shape[0] % 2, + incoords[1].shape[0] % 2, + ) assert res.dtype.type == np.float64 - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('incoords', coords) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("incoords", coords) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_output_type_self_distance_array(self, incoords, box, backend): res = distances.self_distance_array(incoords, box=box, backend=backend) assert type(res) == np.ndarray assert res.shape == (0,) assert res.dtype.type == np.float64 - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('min_cut', [min_cut, None]) - @pytest.mark.parametrize('ret_dist', [False, True]) - @pytest.mark.parametrize('incoords', list(comb(coords, 2))) - @pytest.mark.parametrize('met', ["bruteforce", "pkdtree", "nsgrid", None]) - def test_output_type_capped_distance(self, incoords, min_cut, box, met, - ret_dist): - res = distances.capped_distance(*incoords, max_cutoff=self.max_cut, - min_cutoff=min_cut, box=box, method=met, - return_distances=ret_dist) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("min_cut", [min_cut, None]) + @pytest.mark.parametrize("ret_dist", [False, True]) + @pytest.mark.parametrize("incoords", list(comb(coords, 2))) + @pytest.mark.parametrize("met", ["bruteforce", "pkdtree", "nsgrid", None]) + def test_output_type_capped_distance( + self, incoords, min_cut, box, met, ret_dist + ): + res = distances.capped_distance( + *incoords, + max_cutoff=self.max_cut, + min_cutoff=min_cut, + box=box, + method=met, + return_distances=ret_dist, + ) if ret_dist: pairs, dist = res else: @@ -1757,18 +2216,22 @@ def test_output_type_capped_distance(self, incoords, min_cut, box, met, assert dist.dtype.type == np.float64 assert dist.shape == (pairs.shape[0],) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('min_cut', [min_cut, None]) - @pytest.mark.parametrize('ret_dist', [False, True]) - @pytest.mark.parametrize('incoords', coords) - @pytest.mark.parametrize('met', ["bruteforce", "pkdtree", "nsgrid", None]) - def test_output_type_self_capped_distance(self, incoords, min_cut, box, - met, ret_dist): - res = distances.self_capped_distance(incoords, - max_cutoff=self.max_cut, - min_cutoff=min_cut, - box=box, method=met, - return_distances=ret_dist) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize("min_cut", [min_cut, None]) + @pytest.mark.parametrize("ret_dist", [False, True]) + @pytest.mark.parametrize("incoords", coords) + @pytest.mark.parametrize("met", ["bruteforce", "pkdtree", "nsgrid", None]) + def test_output_type_self_capped_distance( + self, incoords, min_cut, box, met, ret_dist + ): + res = distances.self_capped_distance( + incoords, + max_cutoff=self.max_cut, + min_cutoff=min_cut, + box=box, + method=met, + return_distances=ret_dist, + ) if ret_dist: pairs, dist = res else: @@ -1782,18 +2245,18 @@ def test_output_type_self_capped_distance(self, incoords, min_cut, box, assert dist.dtype.type == np.float64 assert dist.shape == (pairs.shape[0],) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('incoords', coords) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("incoords", coords) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_output_dtype_transform_RtoS(self, incoords, box, backend): res = distances.transform_RtoS(incoords, box, backend=backend) assert type(res) == np.ndarray assert res.dtype.type == np.float32 assert res.shape == incoords.shape - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('incoords', coords) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("incoords", coords) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_output_dtype_transform_RtoS(self, incoords, box, backend): res = distances.transform_RtoS(incoords, box, backend=backend) assert type(res) == np.ndarray @@ -1801,7 +2264,9 @@ def test_output_dtype_transform_RtoS(self, incoords, box, backend): 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( + "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) @@ -1814,10 +2279,11 @@ def test_output_type_calc_bonds(self, incoords, box, backend): coord = [crd for crd in incoords if crd.ndim == maxdim][0] assert res.shape == (coord.shape[0],) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('incoords', - [3 * [coords[0]]] + list(comb(coords[1:], 3))) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize( + "incoords", [3 * [coords[0]]] + list(comb(coords[1:], 3)) + ) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_output_type_calc_angles(self, incoords, box, backend): res = distances.calc_angles(*incoords, box=box, backend=backend) maxdim = max([crd.ndim for crd in incoords]) @@ -1829,10 +2295,11 @@ def test_output_type_calc_angles(self, incoords, box, backend): coord = [crd for crd in incoords if crd.ndim == maxdim][0] assert res.shape == (coord.shape[0],) - @pytest.mark.parametrize('box', boxes) - @pytest.mark.parametrize('incoords', - [4 * [coords[0]]] + list(comb(coords[1:], 4))) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes) + @pytest.mark.parametrize( + "incoords", [4 * [coords[0]]] + list(comb(coords[1:], 4)) + ) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_output_type_calc_dihedrals(self, incoords, box, backend): res = distances.calc_dihedrals(*incoords, box=box, backend=backend) maxdim = max([crd.ndim for crd in incoords]) @@ -1844,9 +2311,9 @@ def test_output_type_calc_dihedrals(self, incoords, box, backend): coord = [crd for crd in incoords if crd.ndim == maxdim][0] assert res.shape == (coord.shape[0],) - @pytest.mark.parametrize('box', boxes[:2]) - @pytest.mark.parametrize('incoords', coords) - @pytest.mark.parametrize('backend', ['serial', 'openmp']) + @pytest.mark.parametrize("box", boxes[:2]) + @pytest.mark.parametrize("incoords", coords) + @pytest.mark.parametrize("backend", ["serial", "openmp"]) def test_output_type_apply_PBC(self, incoords, box, backend): res = distances.apply_PBC(incoords, box, backend=backend) assert type(res) == np.ndarray @@ -1864,37 +2331,60 @@ def backend_selection_pos(): return positions, result - @pytest.mark.parametrize('backend', [ - "serial", "Serial", "SeRiAL", "SERIAL", - "openmp", "OpenMP", "oPENmP", "OPENMP", - ]) + @pytest.mark.parametrize( + "backend", + [ + "serial", + "Serial", + "SeRiAL", + "SERIAL", + "openmp", + "OpenMP", + "oPENmP", + "OPENMP", + ], + ) def test_case_insensitivity(self, backend, backend_selection_pos): positions, result = backend_selection_pos try: - distances._run("calc_self_distance_array", args=(positions, result), - backend=backend) + distances._run( + "calc_self_distance_array", + args=(positions, result), + backend=backend, + ) except RuntimeError: pytest.fail("Failed to understand backend {0}".format(backend)) def test_wront_backend(self, backend_selection_pos): positions, result = backend_selection_pos with pytest.raises(ValueError): - distances._run("calc_self_distance_array", args=(positions, result), - backend="not implemented stuff") + distances._run( + "calc_self_distance_array", + args=(positions, result), + backend="not implemented stuff", + ) + def test_used_openmpflag(): assert isinstance(distances.USED_OPENMP, bool) # test both orthognal and triclinic boxes -@pytest.mark.parametrize('box', (np.eye(3) * 10, np.array([[10, 0, 0], [2, 10, 0], [2, 2, 10]]))) +@pytest.mark.parametrize( + "box", (np.eye(3) * 10, np.array([[10, 0, 0], [2, 10, 0], [2, 2, 10]])) +) # try shifts of -2 to +2 in each dimension, and all combinations of shifts -@pytest.mark.parametrize('shift', itertools.product(range(-2, 3), range(-2, 3), range(-2, 3))) -@pytest.mark.parametrize('dtype', (np.float32, np.float64)) +@pytest.mark.parametrize( + "shift", itertools.product(range(-2, 3), range(-2, 3), range(-2, 3)) +) +@pytest.mark.parametrize("dtype", (np.float32, np.float64)) def test_minimize_vectors(box, shift, dtype): # test vectors pointing in all directions # these currently all obey minimum convention as they're much smaller than the box - vec = np.array(list(itertools.product(range(-1, 2), range(-1, 2), range(-1, 2))), dtype=dtype) + vec = np.array( + list(itertools.product(range(-1, 2), range(-1, 2), range(-1, 2))), + dtype=dtype, + ) box = box.astype(dtype) # box is 3x3 representation diff --git a/testsuite/MDAnalysisTests/lib/test_log.py b/testsuite/MDAnalysisTests/lib/test_log.py index cab2994a87d..541660ca4c7 100644 --- a/testsuite/MDAnalysisTests/lib/test_log.py +++ b/testsuite/MDAnalysisTests/lib/test_log.py @@ -32,22 +32,22 @@ def test_output(self, capsys): for i in ProgressBar(list(range(10))): pass out, err = capsys.readouterr() - expected = u'100%|██████████' - actual = err.strip().split('\r')[-1] + expected = "100%|██████████" + actual = err.strip().split("\r")[-1] assert actual[:15] == expected def test_disable(self, capsys): for i in ProgressBar(list(range(10)), disable=True): pass out, err = capsys.readouterr() - expected = '' - actual = err.strip().split('\r')[-1] + expected = "" + actual = err.strip().split("\r")[-1] assert actual == expected def test_verbose_disable(self, capsys): for i in ProgressBar(list(range(10)), verbose=False): pass out, err = capsys.readouterr() - expected = '' - actual = err.strip().split('\r')[-1] + expected = "" + actual = err.strip().split("\r")[-1] assert actual == expected diff --git a/testsuite/MDAnalysisTests/lib/test_neighborsearch.py b/testsuite/MDAnalysisTests/lib/test_neighborsearch.py index 7ae209485ba..29a179350d9 100644 --- a/testsuite/MDAnalysisTests/lib/test_neighborsearch.py +++ b/testsuite/MDAnalysisTests/lib/test_neighborsearch.py @@ -30,7 +30,6 @@ from MDAnalysisTests.datafiles import PSF, DCD - @pytest.fixture def universe(): u = mda.Universe(PSF, DCD) @@ -43,8 +42,9 @@ def test_search(universe): """simply check that for a centered protein in a large box periodic and non-periodic return the same result""" ns = NeighborSearch.AtomNeighborSearch(universe.atoms) - pns = NeighborSearch.AtomNeighborSearch(universe.atoms, - universe.atoms.dimensions) + pns = NeighborSearch.AtomNeighborSearch( + universe.atoms, universe.atoms.dimensions + ) ns_res = ns.search(universe.atoms[20], 20) pns_res = pns.search(universe.atoms[20], 20) @@ -54,9 +54,9 @@ def test_search(universe): def test_zero(universe): """Check if empty atomgroup, residue, segments are returned""" ns = NeighborSearch.AtomNeighborSearch(universe.atoms[:10]) - ns_res = ns.search(universe.atoms[20], 0.1, level='A') + ns_res = ns.search(universe.atoms[20], 0.1, level="A") assert ns_res == universe.atoms[[]] - ns_res = ns.search(universe.atoms[20], 0.1, level='R') + ns_res = ns.search(universe.atoms[20], 0.1, level="R") assert ns_res == universe.atoms[[]].residues - ns_res = ns.search(universe.atoms[20], 0.1, level='S') + ns_res = ns.search(universe.atoms[20], 0.1, level="S") assert ns_res == universe.atoms[[]].segments diff --git a/testsuite/MDAnalysisTests/lib/test_nsgrid.py b/testsuite/MDAnalysisTests/lib/test_nsgrid.py index 69e7fa1f89f..582e780172b 100644 --- a/testsuite/MDAnalysisTests/lib/test_nsgrid.py +++ b/testsuite/MDAnalysisTests/lib/test_nsgrid.py @@ -31,7 +31,12 @@ import MDAnalysis as mda from MDAnalysisTests.datafiles import ( - GRO, Martini_membrane_gro, PDB, PDB_xvf, SURFACE_PDB, SURFACE_TRR + GRO, + Martini_membrane_gro, + PDB, + PDB_xvf, + SURFACE_PDB, + SURFACE_TRR, ) from MDAnalysis.lib import nsgrid from MDAnalysis.transformations.translate import center_in_box @@ -42,23 +47,32 @@ def universe(): u = mda.Universe(GRO) return u + def run_grid_search(u, ref_id, cutoff=3): coords = u.atoms.positions searchcoords = u.atoms.positions[ref_id] - if searchcoords.shape == (3, ): + if searchcoords.shape == (3,): searchcoords = searchcoords[None, :] # Run grid search searcher = nsgrid.FastNS(cutoff, coords, box=u.dimensions) return searcher.search(searchcoords) -@pytest.mark.parametrize('box', [ - np.zeros(3), # Bad shape - np.zeros((3, 3)), # Collapsed box - np.array([[0, 0, 0], [0, 1, 0], [0, 0, 1]]), # 2D box - np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), # Box provided as array of integers - np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float64), # Box provided as array of double -]) + +@pytest.mark.parametrize( + "box", + [ + np.zeros(3), # Bad shape + np.zeros((3, 3)), # Collapsed box + np.array([[0, 0, 0], [0, 1, 0], [0, 0, 1]]), # 2D box + np.array( + [[1, 0, 0], [0, 1, 0], [0, 0, 1]] + ), # Box provided as array of integers + np.array( + [[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float64 + ), # Box provided as array of double + ], +) def test_pbc_box(box): """Check that PBC box accepts only well-formated boxes""" coords = np.array([[1.0, 1.0, 1.0]], dtype=np.float32) @@ -67,9 +81,13 @@ def test_pbc_box(box): nsgrid.FastNS(4.0, coords, box=box) -@pytest.mark.parametrize('cutoff, match', ((-4, "Cutoff must be positive"), - (100000, - "Cutoff 100000 too large for box"))) +@pytest.mark.parametrize( + "cutoff, match", + ( + (-4, "Cutoff must be positive"), + (100000, "Cutoff 100000 too large for box"), + ), +) def test_nsgrid_badcutoff(universe, cutoff, match): with pytest.raises(ValueError, match=match): run_grid_search(universe, 0, cutoff) @@ -91,16 +109,38 @@ def test_nsgrid_PBC_rect(): """Check that nsgrid works with rect boxes and PBC""" ref_id = 191 # Atomid are from gmx select so there start from 1 and not 0. hence -1! - results = np.array([191, 192, 672, 682, 683, 684, 995, 996, 2060, 2808, 3300, 3791, - 3792]) - 1 + results = ( + np.array( + [ + 191, + 192, + 672, + 682, + 683, + 684, + 995, + 996, + 2060, + 2808, + 3300, + 3791, + 3792, + ] + ) + - 1 + ) universe = mda.Universe(Martini_membrane_gro) cutoff = 7 # FastNS is called differently to max coverage - searcher = nsgrid.FastNS(cutoff, universe.atoms.positions, box=universe.dimensions) + searcher = nsgrid.FastNS( + cutoff, universe.atoms.positions, box=universe.dimensions + ) - results_grid = searcher.search(universe.atoms.positions[ref_id][None, :]).get_pairs() + results_grid = searcher.search( + universe.atoms.positions[ref_id][None, :] + ).get_pairs() other_ix = sorted(i for (_, i) in results_grid) assert len(results) == len(results_grid) @@ -111,8 +151,25 @@ def test_nsgrid_PBC(universe): """Check that grid search works when PBC is needed""" # Atomid are from gmx select so there start from 1 and not 0. hence -1! ref_id = 13937 - results = np.array([4398, 4401, 13938, 13939, 13940, 13941, 17987, 23518, 23519, 23521, 23734, - 47451]) - 1 + results = ( + np.array( + [ + 4398, + 4401, + 13938, + 13939, + 13940, + 13941, + 17987, + 23518, + 23519, + 23521, + 23734, + 47451, + ] + ) + - 1 + ) results_grid = run_grid_search(universe, ref_id).get_pairs() @@ -126,23 +183,59 @@ def test_nsgrid_pairs(universe): """Check that grid search returns the proper pairs""" ref_id = 13937 - neighbors = np.array([4398, 4401, 13938, 13939, 13940, 13941, 17987, 23518, 23519, 23521, 23734, - 47451]) - 1 # Atomid are from gmx select so there start from 1 and not 0. hence -1! + neighbors = ( + np.array( + [ + 4398, + 4401, + 13938, + 13939, + 13940, + 13941, + 17987, + 23518, + 23519, + 23521, + 23734, + 47451, + ] + ) + - 1 + ) # Atomid are from gmx select so there start from 1 and not 0. hence -1! results = [] results = np.array(results) results_grid = run_grid_search(universe, ref_id).get_pairs() - assert_equal(np.sort(neighbors, axis=0), np.sort(results_grid[:, 1], axis=0)) + assert_equal( + np.sort(neighbors, axis=0), np.sort(results_grid[:, 1], axis=0) + ) def test_nsgrid_pair_distances(universe): """Check that grid search returns the proper pair distances""" ref_id = 13937 - results = np.array([0.0, 0.270, 0.285, 0.096, 0.096, 0.015, 0.278, 0.268, 0.179, 0.259, 0.290, - 0.270]) * 10 # These distances where obtained by gmx distance so they are in nm + results = ( + np.array( + [ + 0.0, + 0.270, + 0.285, + 0.096, + 0.096, + 0.015, + 0.278, + 0.268, + 0.179, + 0.259, + 0.290, + 0.270, + ] + ) + * 10 + ) # These distances where obtained by gmx distance so they are in nm results_grid = run_grid_search(universe, ref_id).get_pair_distances() @@ -153,32 +246,57 @@ def test_nsgrid_distances(universe): """Check that grid search returns the proper distances""" # These distances where obtained by gmx distance so they are in nm ref_id = 13937 - results = np.array([0.0, 0.270, 0.285, 0.096, 0.096, 0.015, 0.278, 0.268, 0.179, 0.259, 0.290, - 0.270]) * 10 + results = ( + np.array( + [ + 0.0, + 0.270, + 0.285, + 0.096, + 0.096, + 0.015, + 0.278, + 0.268, + 0.179, + 0.259, + 0.290, + 0.270, + ] + ) + * 10 + ) results_grid = run_grid_search(universe, ref_id).get_pair_distances() assert_allclose(np.sort(results), np.sort(results_grid), atol=1e-2) -@pytest.mark.parametrize('box, results', - ((None, [3, 13, 24]), - (np.array([10., 10., 10., 90., 90., 90.]), [3, 13, 24, 39, 67]), - (np.array([10., 10., 10., 60., 75., 90.]), [3, 13, 24, 39, 60, 79]))) +@pytest.mark.parametrize( + "box, results", + ( + (None, [3, 13, 24]), + (np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0]), [3, 13, 24, 39, 67]), + ( + np.array([10.0, 10.0, 10.0, 60.0, 75.0, 90.0]), + [3, 13, 24, 39, 60, 79], + ), + ), +) def test_nsgrid_search(box, results): np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(100, 3))*(10.)).astype(np.float32) + points = ( + np.random.uniform(low=0, high=1.0, size=(100, 3)) * (10.0) + ).astype(np.float32) cutoff = 2.0 - query = np.array([1., 1., 1.], dtype=np.float32).reshape((1, 3)) + query = np.array([1.0, 1.0, 1.0], dtype=np.float32).reshape((1, 3)) if box is None: pseudobox = np.zeros(6, dtype=np.float32) all_coords = np.concatenate([points, query]) lmax = all_coords.max(axis=0) lmin = all_coords.min(axis=0) - pseudobox[:3] = 1.1*(lmax - lmin) - pseudobox[3:] = 90. + pseudobox[:3] = 1.1 * (lmax - lmin) + pseudobox[3:] = 90.0 shiftpoints, shiftquery = points.copy(), query.copy() shiftpoints -= lmin shiftquery -= lmin @@ -191,15 +309,20 @@ def test_nsgrid_search(box, results): assert_equal(np.sort(indices), results) -@pytest.mark.parametrize('box, result', - ((None, 21), - (np.array([0., 0., 0., 90., 90., 90.]), 21), - (np.array([10., 10., 10., 90., 90., 90.]), 26), - (np.array([10., 10., 10., 60., 75., 90.]), 33))) +@pytest.mark.parametrize( + "box, result", + ( + (None, 21), + (np.array([0.0, 0.0, 0.0, 90.0, 90.0, 90.0]), 21), + (np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0]), 26), + (np.array([10.0, 10.0, 10.0, 60.0, 75.0, 90.0]), 33), + ), +) def test_nsgrid_selfsearch(box, result): np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(100, 3))*(10.)).astype(np.float32) + points = ( + np.random.uniform(low=0, high=1.0, size=(100, 3)) * (10.0) + ).astype(np.float32) cutoff = 1.0 if box is None or np.allclose(box[:3], 0): # create a pseudobox @@ -209,8 +332,8 @@ def test_nsgrid_selfsearch(box, result): pseudobox = np.zeros(6, dtype=np.float32) lmax = points.max(axis=0) lmin = points.min(axis=0) - pseudobox[:3] = 1.1*(lmax - lmin) - pseudobox[3:] = 90. + pseudobox[:3] = 1.1 * (lmax - lmin) + pseudobox[3:] = 90.0 shiftref = points.copy() shiftref -= lmin searcher = nsgrid.FastNS(cutoff, shiftref, box=pseudobox, pbc=False) @@ -221,12 +344,15 @@ def test_nsgrid_selfsearch(box, result): pairs = searchresults.get_pairs() assert_equal(len(pairs), result) + def test_nsgrid_probe_close_to_box_boundary(): # FastNS.search used to segfault with this box, cutoff and reference # coordinate prior to PR #2136, so we ensure that this remains fixed. # See Issue #2132 for further information. ref = np.array([[55.783722, 44.190044, -54.16671]], dtype=np.float32) - box = np.array([53.785854, 43.951054, 57.17597, 90., 90., 90.], dtype=np.float32) + box = np.array( + [53.785854, 43.951054, 57.17597, 90.0, 90.0, 90.0], dtype=np.float32 + ) cutoff = 3.0 # search within a configuration where we know the expected outcome: conf = np.ones((1, 3), dtype=np.float32) @@ -236,7 +362,7 @@ def test_nsgrid_probe_close_to_box_boundary(): expected_pairs = np.zeros((1, 2), dtype=np.int64) expected_dists = np.array([2.3689647], dtype=np.float64) assert_equal(results.get_pairs(), expected_pairs) - assert_allclose(results.get_pair_distances(), expected_dists, rtol=1.e-6) + assert_allclose(results.get_pair_distances(), expected_dists, rtol=1.0e-6) def test_zero_max_dist(): @@ -245,7 +371,7 @@ def test_zero_max_dist(): ref = np.array([1.0, 1.0, 1.0], dtype=np.float32) conf = np.array([2.0, 1.0, 1.0], dtype=np.float32) - box = np.array([10., 10., 10., 90., 90., 90.], dtype=np.float32) + box = np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0], dtype=np.float32) res = mda.lib.distances._nsgrid_capped(ref, conf, box=box, max_cutoff=0.0) @@ -259,7 +385,7 @@ def u_pbc_triclinic(): def test_around_res(u_pbc_triclinic): # sanity check for issue 2656, shouldn't segfault (obviously) - ag = u_pbc_triclinic.select_atoms('around 0.0 resid 3') + ag = u_pbc_triclinic.select_atoms("around 0.0 resid 3") assert len(ag) == 0 @@ -267,7 +393,7 @@ def test_around_overlapping(): # check that around 0.0 catches when atoms *are* superimposed u = mda.Universe.empty(60, trajectory=True) xyz = np.zeros((60, 3)) - x = np.tile(np.arange(12), (5,))+np.repeat(np.arange(5)*100, 12) + x = np.tile(np.arange(12), (5,)) + np.repeat(np.arange(5) * 100, 12) # x is 5 images of 12 atoms xyz[:, 0] = x # y and z are 0 @@ -279,7 +405,7 @@ def test_around_overlapping(): # u.atoms[12:].positions, # box=u.dimensions) # assert np.count_nonzero(np.any(dist <= 0.0, axis=0)) == 48 - assert u.select_atoms('around 0.0 index 0:11').n_atoms == 48 + assert u.select_atoms("around 0.0 index 0:11").n_atoms == 48 def test_issue_2229_part1(): @@ -306,7 +432,9 @@ def test_issue_2229_part2(): u.atoms[0].position = [0, 0, 29.29] u.atoms[1].position = [0, 0, 28.23] - g = mda.lib.nsgrid.FastNS(3.0, u.atoms[[0]].positions, box=u.dimensions, pbc=False) + g = mda.lib.nsgrid.FastNS( + 3.0, u.atoms[[0]].positions, box=u.dimensions, pbc=False + ) assert len(g.search(u.atoms[[1]].positions).get_pairs()) == 1 g = mda.lib.nsgrid.FastNS(3.0, u.atoms[[1]].positions, box=u.dimensions) @@ -317,12 +445,12 @@ def test_issue_2919(): # regression test reported in issue 2919 # other methods will also give 1115 or 2479 results u = mda.Universe(PDB_xvf) - ag = u.select_atoms('index 0') + ag = u.select_atoms("index 0") u.trajectory.ts = center_in_box(ag)(u.trajectory.ts) box = u.dimensions - reference = u.select_atoms('protein') - configuration = u.select_atoms('not protein') + reference = u.select_atoms("protein") + configuration = u.select_atoms("not protein") for cutoff, expected in [(2.8, 1115), (3.2, 2497)]: pairs, distances = mda.lib.distances.capped_distance( @@ -330,7 +458,7 @@ def test_issue_2919(): configuration.positions, max_cutoff=cutoff, box=box, - method='nsgrid', + method="nsgrid", return_distances=True, ) assert len(pairs) == expected @@ -348,7 +476,7 @@ def test_issue_2345(): idx = g.self_search().get_pairs() # count number of contacts for each atom - for (i, j) in idx: + for i, j in idx: cn[i].append(j) cn[j].append(i) c = Counter(len(v) for v in cn.values()) @@ -365,29 +493,31 @@ def test_issue_2670(): # the coordinates for this test to make any sense: u.atoms.positions = u.atoms.positions * 1.0e-3 - ag1 = u.select_atoms('resid 2 3') + ag1 = u.select_atoms("resid 2 3") # should return nothing as nothing except resid 3 is within 0.0 or resid 3 - assert len(ag1.select_atoms('around 0.0 resid 3')) == 0 + assert len(ag1.select_atoms("around 0.0 resid 3")) == 0 # force atom 0 of resid 1 to overlap with atom 0 of resid 3 u.residues[0].atoms[0].position = u.residues[2].atoms[0].position - ag2 = u.select_atoms('resid 1 3') + ag2 = u.select_atoms("resid 1 3") # should return the one atom overlap - assert len(ag2.select_atoms('around 0.0 resid 3')) == 1 + assert len(ag2.select_atoms("around 0.0 resid 3")) == 1 def high_mem_tests_enabled(): - """ Returns true if ENABLE_HIGH_MEM_UNIT_TESTS is set to true.""" + """Returns true if ENABLE_HIGH_MEM_UNIT_TESTS is set to true.""" env = os.getenv("ENABLE_HIGH_MEM_UNIT_TESTS", default="false").lower() - if env == 'true': + if env == "true": return True return False -reason = ("Turned off by default. The test can be enabled by setting " - "the ENABLE_HIGH_MEM_UNIT_TESTS " - "environment variable. Make sure you have at least 10GB of RAM.") +reason = ( + "Turned off by default. The test can be enabled by setting " + "the ENABLE_HIGH_MEM_UNIT_TESTS " + "environment variable. Make sure you have at least 10GB of RAM." +) # Tests that with a tiny cutoff to box ratio, the number of grids is capped @@ -396,11 +526,12 @@ def high_mem_tests_enabled(): @pytest.mark.skipif(not high_mem_tests_enabled(), reason=reason) def test_issue_3183(): np.random.seed(90003) - points = (np.random.uniform(low=0, high=1.0, - size=(100, 3)) * (10.)).astype(np.float32) + points = ( + np.random.uniform(low=0, high=1.0, size=(100, 3)) * (10.0) + ).astype(np.float32) cutoff = 2.0 - query = np.array([1., 1., 1.], dtype=np.float32).reshape((1, 3)) - box = np.array([10000., 10000., 10000., 90., 90., 90.]) + query = np.array([1.0, 1.0, 1.0], dtype=np.float32).reshape((1, 3)) + box = np.array([10000.0, 10000.0, 10000.0, 90.0, 90.0, 90.0]) searcher = nsgrid.FastNS(cutoff, points, box) searchresults = searcher.search(query) diff --git a/testsuite/MDAnalysisTests/lib/test_pkdtree.py b/testsuite/MDAnalysisTests/lib/test_pkdtree.py index f92a87e73e9..ec4e586b380 100644 --- a/testsuite/MDAnalysisTests/lib/test_pkdtree.py +++ b/testsuite/MDAnalysisTests/lib/test_pkdtree.py @@ -31,21 +31,33 @@ # fractional coordinates for data points -f_dataset = np.array([[0.2, 0.2, 0.2], # center of the box - [0.5, 0.5, 0.5], - [0.11, 0.11, 0.11], - [1.1, -1.1, 1.1], # wrapped to [1, 9, 1] - [2.1, 2.1, 0.3]], # wrapped to [1, 1, 3] - dtype=np.float32) - - -@pytest.mark.parametrize('b, cut, result', ( - (None, 1.0, - 'Donot provide cutoff distance' - ' for non PBC aware calculations'), - ([10, 10, 10, 90, 90, 90], None, - 'Provide a cutoff distance with' - ' tree.set_coords(...)'))) +f_dataset = np.array( + [ + [0.2, 0.2, 0.2], # center of the box + [0.5, 0.5, 0.5], + [0.11, 0.11, 0.11], + [1.1, -1.1, 1.1], # wrapped to [1, 9, 1] + [2.1, 2.1, 0.3], # wrapped to [1, 1, 3] + ], + dtype=np.float32, +) + + +@pytest.mark.parametrize( + "b, cut, result", + ( + ( + None, + 1.0, + "Donot provide cutoff distance" " for non PBC aware calculations", + ), + ( + [10, 10, 10, 90, 90, 90], + None, + "Provide a cutoff distance with" " tree.set_coords(...)", + ), + ), +) def test_setcoords(b, cut, result): coords = np.array([[1, 1, 1], [2, 2, 2]], dtype=np.float32) if b is not None: @@ -64,16 +76,19 @@ def test_searchfail(): query = np.array([1, 1, 1], dtype=np.float32) tree = PeriodicKDTree(box=b) tree.set_coords(coords, cutoff=cutoff) - match = 'Set cutoff greater or equal to the radius.' + match = "Set cutoff greater or equal to the radius." with pytest.raises(RuntimeError, match=match): tree.search(query, search_radius) -@pytest.mark.parametrize('b, q, result', ( - ([10, 10, 10, 90, 90, 90], [0.5, -0.1, 1.1], []), - ([10, 10, 10, 90, 90, 90], [2.1, -3.1, 0.1], [2, 3, 4]), - ([10, 10, 10, 45, 60, 90], [2.1, -3.1, 0.1], [2, 3]) - )) +@pytest.mark.parametrize( + "b, q, result", + ( + ([10, 10, 10, 90, 90, 90], [0.5, -0.1, 1.1], []), + ([10, 10, 10, 90, 90, 90], [2.1, -3.1, 0.1], [2, 3, 4]), + ([10, 10, 10, 45, 60, 90], [2.1, -3.1, 0.1], [2, 3]), + ), +) def test_search(b, q, result): b = np.array(b, dtype=np.float32) q = transform_StoR(np.array(q, dtype=np.float32), b) @@ -95,16 +110,19 @@ def test_nopbc(): assert_equal(indices, [0, 2]) -@pytest.mark.parametrize('b, radius, result', ( - ([10, 10, 10, 90, 90, 90], 2.0, [[0, 2], - [0, 4], - [2, 4]]), - ([10, 10, 10, 45, 60, 90], 2.0, [[0, 4], - [2, 4]]), - ([10, 10, 10, 45, 60, 90], 4.5, - 'Set cutoff greater or equal to the radius.'), - ([10, 10, 10, 45, 60, 90], 0.1, []) - )) +@pytest.mark.parametrize( + "b, radius, result", + ( + ([10, 10, 10, 90, 90, 90], 2.0, [[0, 2], [0, 4], [2, 4]]), + ([10, 10, 10, 45, 60, 90], 2.0, [[0, 4], [2, 4]]), + ( + [10, 10, 10, 45, 60, 90], + 4.5, + "Set cutoff greater or equal to the radius.", + ), + ([10, 10, 10, 45, 60, 90], 0.1, []), + ), +) def test_searchpairs(b, radius, result): b = np.array(b, dtype=np.float32) cutoff = 2.0 @@ -119,8 +137,7 @@ def test_searchpairs(b, radius, result): assert_equal(len(indices), len(result)) -@pytest.mark.parametrize('radius, result', ((0.1, []), - (0.3, [[0, 2]]))) +@pytest.mark.parametrize("radius, result", ((0.1, []), (0.3, [[0, 2]]))) def test_ckd_searchpairs_nopbc(radius, result): coords = f_dataset.copy() tree = PeriodicKDTree() @@ -129,6 +146,7 @@ def test_ckd_searchpairs_nopbc(radius, result): assert_equal(indices, result) +# fmt: off @pytest.mark.parametrize('b, q, result', ( ([10, 10, 10, 90, 90, 90], [0.5, -0.1, 1.1], []), ([10, 10, 10, 90, 90, 90], [2.1, -3.1, 0.1], [[0, 2], @@ -142,6 +160,7 @@ def test_ckd_searchpairs_nopbc(radius, result): ([10, 10, 10, 45, 60, 90], [2.1, -3.1, 0.1], [[0, 2], [0, 3]]) )) +# fmt: on def test_searchtree(b, q, result): b = np.array(b, dtype=np.float32) cutoff = 3.0 diff --git a/testsuite/MDAnalysisTests/lib/test_qcprot.py b/testsuite/MDAnalysisTests/lib/test_qcprot.py index a62ae73f971..5750155c495 100644 --- a/testsuite/MDAnalysisTests/lib/test_qcprot.py +++ b/testsuite/MDAnalysisTests/lib/test_qcprot.py @@ -30,7 +30,7 @@ from MDAnalysisTests.datafiles import PSF, DCD -@pytest.mark.parametrize('dtype', [np.float64, np.float32]) +@pytest.mark.parametrize("dtype", [np.float64, np.float32]) class TestQCProt: def test_dummy(self, dtype): a = np.array([[1.0, 1.0, 2.0]], dtype=dtype) @@ -47,7 +47,7 @@ def test_dummy(self, dtype): def test_regression(self, dtype): u = mda.Universe(PSF, DCD) - prot = u.select_atoms('protein') + prot = u.select_atoms("protein") weights = prot.masses.astype(dtype) weights /= np.mean(weights) p1 = prot.positions.astype(dtype) @@ -57,10 +57,20 @@ def test_regression(self, dtype): r = qcprot.CalcRMSDRotationalMatrix(p1, p2, len(prot), rot, weights) - rot_ref = np.array([0.999998, 0.001696, 0.001004, - -0.001698, 0.999998, 0.001373, - -0.001002, -0.001375, 0.999999], - dtype=dtype) + rot_ref = np.array( + [ + 0.999998, + 0.001696, + 0.001004, + -0.001698, + 0.999998, + 0.001373, + -0.001002, + -0.001375, + 0.999999, + ], + dtype=dtype, + ) err = 0.001 if dtype is np.float32 else 0.000001 assert r == pytest.approx(0.6057544485785074, abs=err) diff --git a/testsuite/MDAnalysisTests/lib/test_util.py b/testsuite/MDAnalysisTests/lib/test_util.py index 839b0ef61e4..5db9b9afd42 100644 --- a/testsuite/MDAnalysisTests/lib/test_util.py +++ b/testsuite/MDAnalysisTests/lib/test_util.py @@ -32,50 +32,83 @@ import shutil import numpy as np -from numpy.testing import (assert_equal, assert_almost_equal, - assert_array_almost_equal, assert_array_equal, - assert_allclose) +from numpy.testing import ( + assert_equal, + assert_almost_equal, + assert_array_almost_equal, + assert_array_equal, + assert_allclose, +) from itertools import combinations_with_replacement as comb_wr import MDAnalysis as mda import MDAnalysis.lib.util as util import MDAnalysis.lib.mdamath as mdamath -from MDAnalysis.lib.util import (cached, static_variables, warn_if_not_unique, - check_coords, store_init_arguments, - check_atomgroup_not_empty,) +from MDAnalysis.lib.util import ( + cached, + static_variables, + warn_if_not_unique, + check_coords, + store_init_arguments, + check_atomgroup_not_empty, +) from MDAnalysis.core.topologyattrs import Bonds from MDAnalysis.exceptions import NoDataError, DuplicateWarning from MDAnalysis.core.groups import AtomGroup -from MDAnalysisTests.datafiles import (PSF, DCD, - Make_Whole, TPR, GRO, fullerene, two_water_gro, +from MDAnalysisTests.datafiles import ( + PSF, + DCD, + Make_Whole, + TPR, + GRO, + fullerene, + two_water_gro, ) + def test_absence_cutil(): - with patch.dict('sys.modules', {'MDAnalysis.lib._cutil':None}): + with patch.dict("sys.modules", {"MDAnalysis.lib._cutil": None}): import importlib + with pytest.raises(ImportError): - importlib.reload(sys.modules['MDAnalysis.lib.util']) + importlib.reload(sys.modules["MDAnalysis.lib.util"]) + def test_presence_cutil(): mock = Mock() - with patch.dict('sys.modules', {'MDAnalysis.lib._cutil':mock}): + with patch.dict("sys.modules", {"MDAnalysis.lib._cutil": mock}): try: import MDAnalysis.lib._cutil except ImportError: - pytest.fail(msg='''MDAnalysis.lib._cutil should not raise - an ImportError if cutil is available.''') + pytest.fail( + msg="""MDAnalysis.lib._cutil should not raise + an ImportError if cutil is available.""" + ) + def convert_aa_code_long_data(): aa = [ - ('H', - ('HIS', 'HISA', 'HISB', 'HSE', 'HSD', 'HIS1', 'HIS2', 'HIE', 'HID')), - ('K', ('LYS', 'LYSH', 'LYN')), - ('A', ('ALA',)), - ('D', ('ASP', 'ASPH', 'ASH')), - ('E', ('GLU', 'GLUH', 'GLH')), - ('N', ('ASN',)), - ('Q', ('GLN',)), - ('C', ('CYS', 'CYSH', 'CYS1', 'CYS2')), + ( + "H", + ( + "HIS", + "HISA", + "HISB", + "HSE", + "HSD", + "HIS1", + "HIS2", + "HIE", + "HID", + ), + ), + ("K", ("LYS", "LYSH", "LYN")), + ("A", ("ALA",)), + ("D", ("ASP", "ASPH", "ASH")), + ("E", ("GLU", "GLUH", "GLH")), + ("N", ("ASN",)), + ("Q", ("GLN",)), + ("C", ("CYS", "CYSH", "CYS1", "CYS2")), ] for resname1, strings in aa: for resname3 in strings: @@ -85,15 +118,27 @@ def convert_aa_code_long_data(): class TestStringFunctions(object): # (1-letter, (canonical 3 letter, other 3/4 letter, ....)) aa = [ - ('H', - ('HIS', 'HISA', 'HISB', 'HSE', 'HSD', 'HIS1', 'HIS2', 'HIE', 'HID')), - ('K', ('LYS', 'LYSH', 'LYN')), - ('A', ('ALA',)), - ('D', ('ASP', 'ASPH', 'ASH')), - ('E', ('GLU', 'GLUH', 'GLH')), - ('N', ('ASN',)), - ('Q', ('GLN',)), - ('C', ('CYS', 'CYSH', 'CYS1', 'CYS2')), + ( + "H", + ( + "HIS", + "HISA", + "HISB", + "HSE", + "HSD", + "HIS1", + "HIS2", + "HIE", + "HID", + ), + ), + ("K", ("LYS", "LYSH", "LYN")), + ("A", ("ALA",)), + ("D", ("ASP", "ASPH", "ASH")), + ("E", ("GLU", "GLUH", "GLH")), + ("N", ("ASN",)), + ("Q", ("GLN",)), + ("C", ("CYS", "CYSH", "CYS1", "CYS2")), ] residues = [ @@ -104,33 +149,31 @@ class TestStringFunctions(object): ("M1:CA", ("MET", 1, "CA")), ] - @pytest.mark.parametrize('rstring, residue', residues) + @pytest.mark.parametrize("rstring, residue", residues) def test_parse_residue(self, rstring, residue): assert util.parse_residue(rstring) == residue def test_parse_residue_ValueError(self): with pytest.raises(ValueError): - util.parse_residue('ZZZ') + util.parse_residue("ZZZ") - @pytest.mark.parametrize('resname3, resname1', convert_aa_code_long_data()) + @pytest.mark.parametrize("resname3, resname1", convert_aa_code_long_data()) def test_convert_aa_3to1(self, resname3, resname1): assert util.convert_aa_code(resname3) == resname1 - @pytest.mark.parametrize('resname1, strings', aa) + @pytest.mark.parametrize("resname1, strings", aa) def test_convert_aa_1to3(self, resname1, strings): assert util.convert_aa_code(resname1) == strings[0] - @pytest.mark.parametrize('x', ( - 'XYZXYZ', - '£' - )) + @pytest.mark.parametrize("x", ("XYZXYZ", "£")) def test_ValueError(self, x): with pytest.raises(ValueError): util.convert_aa_code(x) -def test_greedy_splitext(inp="foo/bar/boing.2.pdb.bz2", - ref=["foo/bar/boing", ".2.pdb.bz2"]): +def test_greedy_splitext( + inp="foo/bar/boing.2.pdb.bz2", ref=["foo/bar/boing", ".2.pdb.bz2"] +): inp = os.path.normpath(inp) ref[0] = os.path.normpath(ref[0]) ref[1] = os.path.normpath(ref[1]) @@ -139,17 +182,20 @@ def test_greedy_splitext(inp="foo/bar/boing.2.pdb.bz2", assert ext == ref[1], "extension incorrect" -@pytest.mark.parametrize('iterable, value', [ - ([1, 2, 3], True), - ([], True), - ((1, 2, 3), True), - ((), True), - (range(3), True), - (np.array([1, 2, 3]), True), - (123, False), - ("byte string", False), - (u"unicode string", False) -]) +@pytest.mark.parametrize( + "iterable, value", + [ + ([1, 2, 3], True), + ([], True), + ((1, 2, 3), True), + ((), True), + (range(3), True), + (np.array([1, 2, 3]), True), + (123, False), + ("byte string", False), + ("unicode string", False), + ], +) def test_iterable(iterable, value): assert util.iterable(iterable) == value @@ -160,13 +206,16 @@ class TestFilename(object): ext = "pdb" filename2 = "foo.pdb" - @pytest.mark.parametrize('name, ext, keep, actual_name', [ - (filename, None, False, filename), - (filename, ext, False, filename2), - (filename, ext, True, filename), - (root, ext, False, filename2), - (root, ext, True, filename2) - ]) + @pytest.mark.parametrize( + "name, ext, keep, actual_name", + [ + (filename, None, False, filename), + (filename, ext, False, filename2), + (filename, ext, True, filename), + (root, ext, False, filename2), + (root, ext, True, filename2), + ], + ) def test_string(self, name, ext, keep, actual_name): file_name = util.filename(name, ext, keep) assert file_name == actual_name @@ -186,61 +235,65 @@ class TestGeometryFunctions(object): a = np.array([np.cos(np.pi / 3), np.sin(np.pi / 3), 0]) null = np.zeros(3) - @pytest.mark.parametrize('x_axis, y_axis, value', [ - # Unit vectors - (e1, e2, np.pi / 2), - (e1, a, np.pi / 3), - # Angle vectors - (2 * e1, e2, np.pi / 2), - (-2 * e1, e2, np.pi - np.pi / 2), - (23.3 * e1, a, np.pi / 3), - # Null vector - (e1, null, np.nan), - # Coleniar - (a, a, 0.0) - ]) + @pytest.mark.parametrize( + "x_axis, y_axis, value", + [ + # Unit vectors + (e1, e2, np.pi / 2), + (e1, a, np.pi / 3), + # Angle vectors + (2 * e1, e2, np.pi / 2), + (-2 * e1, e2, np.pi - np.pi / 2), + (23.3 * e1, a, np.pi / 3), + # Null vector + (e1, null, np.nan), + # Coleniar + (a, a, 0.0), + ], + ) def test_vectors(self, x_axis, y_axis, value): assert_allclose(mdamath.angle(x_axis, y_axis), value) - @pytest.mark.parametrize('x_axis, y_axis, value', [ - (-2.3456e7 * e1, 3.4567e-6 * e1, np.pi), - (2.3456e7 * e1, 3.4567e-6 * e1, 0.0) - ]) + @pytest.mark.parametrize( + "x_axis, y_axis, value", + [ + (-2.3456e7 * e1, 3.4567e-6 * e1, np.pi), + (2.3456e7 * e1, 3.4567e-6 * e1, 0.0), + ], + ) def test_angle_pi(self, x_axis, y_axis, value): assert_almost_equal(mdamath.angle(x_axis, y_axis), value) - @pytest.mark.parametrize('x', np.linspace(0, np.pi, 20)) + @pytest.mark.parametrize("x", np.linspace(0, np.pi, 20)) def test_angle_range(self, x): - r = 1000. + r = 1000.0 v = r * np.array([np.cos(x), np.sin(x), 0]) assert_almost_equal(mdamath.angle(self.e1, v), x, 6) - @pytest.mark.parametrize('vector, value', [ - (e3, 1), - (a, np.linalg.norm(a)), - (null, 0.0) - ]) + @pytest.mark.parametrize( + "vector, value", [(e3, 1), (a, np.linalg.norm(a)), (null, 0.0)] + ) def test_norm(self, vector, value): assert mdamath.norm(vector) == value - @pytest.mark.parametrize('x', np.linspace(0, np.pi, 20)) + @pytest.mark.parametrize("x", np.linspace(0, np.pi, 20)) def test_norm_range(self, x): - r = 1000. + r = 1000.0 v = r * np.array([np.cos(x), np.sin(x), 0]) assert_almost_equal(mdamath.norm(v), r, 6) - @pytest.mark.parametrize('vec1, vec2, value', [ - (e1, e2, e3), - (e1, null, 0.0) - ]) + @pytest.mark.parametrize( + "vec1, vec2, value", [(e1, e2, e3), (e1, null, 0.0)] + ) def test_normal(self, vec1, vec2, value): assert_allclose(mdamath.normal(vec1, vec2), value) # add more non-trivial tests def test_angle_lower_clip(self): a = np.array([0.1, 0, 0.2]) - x = np.dot(a**0.5, -(a**0.5)) / \ - (mdamath.norm(a**0.5) * mdamath.norm(-(a**0.5))) + x = np.dot(a**0.5, -(a**0.5)) / ( + mdamath.norm(a**0.5) * mdamath.norm(-(a**0.5)) + ) assert x < -1.0 assert mdamath.angle(a, -(a)) == np.pi assert mdamath.angle(a**0.5, -(a**0.5)) == np.pi @@ -329,9 +382,10 @@ def ref_tribox(self, tri_vecs): box = np.zeros(6, dtype=np.float32) return box - @pytest.mark.parametrize('lengths', comb_wr([-1, 0, 1, 2], 3)) - @pytest.mark.parametrize('angles', - comb_wr([-10, 0, 20, 70, 90, 120, 180], 3)) + @pytest.mark.parametrize("lengths", comb_wr([-1, 0, 1, 2], 3)) + @pytest.mark.parametrize( + "angles", comb_wr([-10, 0, 20, 70, 90, 120, 180], 3) + ) def test_triclinic_vectors(self, lengths, angles): box = lengths + angles ref = self.ref_trivecs(box) @@ -340,11 +394,11 @@ def test_triclinic_vectors(self, lengths, angles): # check for default dtype: assert res.dtype == np.float32 # belts and braces, make sure upper triangle is always zero: - assert not(res[0, 1] or res[0, 2] or res[1, 2]) + assert not (res[0, 1] or res[0, 2] or res[1, 2]) - @pytest.mark.parametrize('alpha', (60, 90)) - @pytest.mark.parametrize('beta', (60, 90)) - @pytest.mark.parametrize('gamma', (60, 90)) + @pytest.mark.parametrize("alpha", (60, 90)) + @pytest.mark.parametrize("beta", (60, 90)) + @pytest.mark.parametrize("gamma", (60, 90)) def test_triclinic_vectors_right_angle_zeros(self, alpha, beta, gamma): angles = [alpha, beta, gamma] box = [10, 20, 30] + angles @@ -375,7 +429,7 @@ def test_triclinic_vectors_right_angle_zeros(self, alpha, beta, gamma): else: assert mat[1, 0] and mat[2, 0] and mat[2, 1] - @pytest.mark.parametrize('dtype', (int, float, np.float32, np.float64)) + @pytest.mark.parametrize("dtype", (int, float, np.float32, np.float64)) def test_triclinic_vectors_retval(self, dtype): # valid box box = [1, 1, 1, 70, 80, 90] @@ -408,26 +462,33 @@ def test_triclinic_vectors_box_cycle(self): for g in range(10, 91, 10): ref = np.array([1, 1, 1, a, b, g], dtype=np.float32) res = mdamath.triclinic_box( - *mdamath.triclinic_vectors(ref)) + *mdamath.triclinic_vectors(ref) + ) if not np.all(res == 0.0): assert_almost_equal(res, ref, 5) - @pytest.mark.parametrize('angles', ([70, 70, 70], - [70, 70, 90], - [70, 90, 70], - [90, 70, 70], - [70, 90, 90], - [90, 70, 90], - [90, 90, 70])) + @pytest.mark.parametrize( + "angles", + ( + [70, 70, 70], + [70, 70, 90], + [70, 90, 70], + [90, 70, 70], + [70, 90, 90], + [90, 70, 90], + [90, 90, 70], + ), + ) def test_triclinic_vectors_box_cycle_exact(self, angles): # These cycles were inexact prior to PR #2201 ref = np.array([10.1, 10.1, 10.1] + angles, dtype=np.float32) res = mdamath.triclinic_box(*mdamath.triclinic_vectors(ref)) assert_allclose(res, ref) - @pytest.mark.parametrize('lengths', comb_wr([-1, 0, 1, 2], 3)) - @pytest.mark.parametrize('angles', - comb_wr([-10, 0, 20, 70, 90, 120, 180], 3)) + @pytest.mark.parametrize("lengths", comb_wr([-1, 0, 1, 2], 3)) + @pytest.mark.parametrize( + "angles", comb_wr([-10, 0, 20, 70, 90, 120, 180], 3) + ) def test_triclinic_box(self, lengths, angles): tri_vecs = self.ref_trivecs_unsafe(lengths + angles) ref = self.ref_tribox(tri_vecs) @@ -435,14 +496,17 @@ def test_triclinic_box(self, lengths, angles): assert_array_equal(res, ref) assert res.dtype == ref.dtype - @pytest.mark.parametrize('lengths', comb_wr([-1, 0, 1, 2], 3)) - @pytest.mark.parametrize('angles', - comb_wr([-10, 0, 20, 70, 90, 120, 180], 3)) + @pytest.mark.parametrize("lengths", comb_wr([-1, 0, 1, 2], 3)) + @pytest.mark.parametrize( + "angles", comb_wr([-10, 0, 20, 70, 90, 120, 180], 3) + ) def test_box_volume(self, lengths, angles): box = np.array(lengths + angles, dtype=np.float32) - assert_almost_equal(mdamath.box_volume(box), - np.linalg.det(self.ref_trivecs(box)), - decimal=5) + assert_almost_equal( + mdamath.box_volume(box), + np.linalg.det(self.ref_trivecs(box)), + decimal=5, + ) def test_sarrus_det(self): comb = comb_wr(np.linspace(-133.7, 133.7, num=5), 9) @@ -459,7 +523,7 @@ def test_sarrus_det(self): assert_almost_equal(res, ref, 7) assert ref.dtype == res.dtype == np.float64 - @pytest.mark.parametrize('shape', ((0,), (3, 2), (2, 3), (1, 1, 3, 1))) + @pytest.mark.parametrize("shape", ((0,), (3, 2), (2, 3), (1, 1, 3, 1))) def test_sarrus_det_wrong_shape(self, shape): matrix = np.zeros(shape) with pytest.raises(ValueError): @@ -545,18 +609,32 @@ def test_double_precision_box(self): residue_segindex=[0], trajectory=True, velocities=False, - forces=False) + forces=False, + ) ts = u.trajectory.ts ts.frame = 0 ts.dimensions = [10, 10, 10, 90, 90, 90] # assert ts.dimensions.dtype == np.float64 # not applicable since #2213 - ts.positions = np.array([[1, 1, 1, ], [9, 9, 9]], dtype=np.float32) + ts.positions = np.array( + [ + [1, 1, 1], + [9, 9, 9], + ], + dtype=np.float32, + ) u.add_TopologyAttr(Bonds([(0, 1)])) mdamath.make_whole(u.atoms) - assert_array_almost_equal(u.atoms.positions, - np.array([[1, 1, 1, ], [-1, -1, -1]], - dtype=np.float32)) + assert_array_almost_equal( + u.atoms.positions, + np.array( + [ + [1, 1, 1], + [-1, -1, -1], + ], + dtype=np.float32, + ), + ) @staticmethod @pytest.fixture() @@ -571,7 +649,7 @@ def test_no_bonds(self): mdamath.make_whole(ag) def test_zero_box_size(self, universe, ag): - universe.dimensions = [0., 0., 0., 90., 90., 90.] + universe.dimensions = [0.0, 0.0, 0.0, 90.0, 90.0, 90.0] with pytest.raises(ValueError): mdamath.make_whole(ag) @@ -593,14 +671,26 @@ def test_solve_1(self, universe, ag): mdamath.make_whole(ag) assert_array_almost_equal(universe.atoms[:4].positions, refpos) - assert_array_almost_equal(universe.atoms[4].position, - np.array([110.0, 50.0, 0.0]), decimal=self.prec) - assert_array_almost_equal(universe.atoms[5].position, - np.array([110.0, 60.0, 0.0]), decimal=self.prec) - assert_array_almost_equal(universe.atoms[6].position, - np.array([110.0, 40.0, 0.0]), decimal=self.prec) - assert_array_almost_equal(universe.atoms[7].position, - np.array([120.0, 50.0, 0.0]), decimal=self.prec) + assert_array_almost_equal( + universe.atoms[4].position, + np.array([110.0, 50.0, 0.0]), + decimal=self.prec, + ) + assert_array_almost_equal( + universe.atoms[5].position, + np.array([110.0, 60.0, 0.0]), + decimal=self.prec, + ) + assert_array_almost_equal( + universe.atoms[6].position, + np.array([110.0, 40.0, 0.0]), + decimal=self.prec, + ) + assert_array_almost_equal( + universe.atoms[7].position, + np.array([120.0, 50.0, 0.0]), + decimal=self.prec, + ) def test_solve_2(self, universe, ag): # use but specify the center atom @@ -610,14 +700,26 @@ def test_solve_2(self, universe, ag): mdamath.make_whole(ag, reference_atom=universe.residues[0].atoms[4]) assert_array_almost_equal(universe.atoms[4:8].positions, refpos) - assert_array_almost_equal(universe.atoms[0].position, - np.array([-20.0, 50.0, 0.0]), decimal=self.prec) - assert_array_almost_equal(universe.atoms[1].position, - np.array([-10.0, 50.0, 0.0]), decimal=self.prec) - assert_array_almost_equal(universe.atoms[2].position, - np.array([-10.0, 60.0, 0.0]), decimal=self.prec) - assert_array_almost_equal(universe.atoms[3].position, - np.array([-10.0, 40.0, 0.0]), decimal=self.prec) + assert_array_almost_equal( + universe.atoms[0].position, + np.array([-20.0, 50.0, 0.0]), + decimal=self.prec, + ) + assert_array_almost_equal( + universe.atoms[1].position, + np.array([-10.0, 50.0, 0.0]), + decimal=self.prec, + ) + assert_array_almost_equal( + universe.atoms[2].position, + np.array([-10.0, 60.0, 0.0]), + decimal=self.prec, + ) + assert_array_almost_equal( + universe.atoms[3].position, + np.array([-10.0, 40.0, 0.0]), + decimal=self.prec, + ) def test_solve_3(self, universe): # put in a chunk that doesn't need any work @@ -638,12 +740,15 @@ def test_solve_4(self, universe): mdamath.make_whole(chunk) assert_array_almost_equal(universe.atoms[7].position, refpos) - assert_array_almost_equal(universe.atoms[4].position, - np.array([110.0, 50.0, 0.0])) - assert_array_almost_equal(universe.atoms[5].position, - np.array([110.0, 60.0, 0.0])) - assert_array_almost_equal(universe.atoms[6].position, - np.array([110.0, 40.0, 0.0])) + assert_array_almost_equal( + universe.atoms[4].position, np.array([110.0, 50.0, 0.0]) + ) + assert_array_almost_equal( + universe.atoms[5].position, np.array([110.0, 60.0, 0.0]) + ) + assert_array_almost_equal( + universe.atoms[6].position, np.array([110.0, 40.0, 0.0]) + ) def test_double_frag_short_bonds(self, universe, ag): # previous bug where if two fragments are given @@ -655,7 +760,7 @@ def test_double_frag_short_bonds(self, universe, ag): def test_make_whole_triclinic(self): u = mda.Universe(TPR, GRO) - thing = u.select_atoms('not resname SOL NA+') + thing = u.select_atoms("not resname SOL NA+") mdamath.make_whole(thing) blengths = thing.bonds.values() @@ -667,18 +772,20 @@ def test_make_whole_fullerene(self): u = mda.Universe(fullerene) bbox = u.atoms.bbox() - u.dimensions = np.r_[bbox[1] - bbox[0], [90]*3] + u.dimensions = np.r_[bbox[1] - bbox[0], [90] * 3] blengths = u.atoms.bonds.values() # kaboom u.atoms[::2].translate([u.dimensions[0], -2 * u.dimensions[1], 0.0]) u.atoms[1::2].translate( - [0.0, 7 * u.dimensions[1], -5 * u.dimensions[2]]) + [0.0, 7 * u.dimensions[1], -5 * u.dimensions[2]] + ) mdamath.make_whole(u.atoms) assert_array_almost_equal( - u.atoms.bonds.values(), blengths, decimal=self.prec) + u.atoms.bonds.values(), blengths, decimal=self.prec + ) def test_make_whole_multiple_molecules(self): u = mda.Universe(two_water_gro, guess_bonds=True) @@ -700,36 +807,36 @@ def __init__(self): self.ref6 = 6.0 # For universe-validated caches # One-line lambda-like class - self.universe = type('Universe', (), dict())() - self.universe._cache = {'_valid': {}} + self.universe = type("Universe", (), dict())() + self.universe._cache = {"_valid": {}} - @cached('val1') + @cached("val1") def val1(self): return self.ref1 # Do one with property decorator as these are used together often @property - @cached('val2') + @cached("val2") def val2(self): return self.ref2 # Check use of property setters @property - @cached('val3') + @cached("val3") def val3(self): return self.ref3 @val3.setter def val3(self, new): - self._clear_caches('val3') - self._fill_cache('val3', new) + self._clear_caches("val3") + self._fill_cache("val3", new) @val3.deleter def val3(self): - self._clear_caches('val3') + self._clear_caches("val3") # Check that args are passed through to underlying functions - @cached('val4') + @cached("val4") def val4(self, n1, n2): return self._init_val_4(n1, n2) @@ -737,7 +844,7 @@ def _init_val_4(self, m1, m2): return self.ref4 + m1 + m2 # Args and Kwargs - @cached('val5') + @cached("val5") def val5(self, n, s=None): return self._init_val_5(n, s=s) @@ -746,7 +853,7 @@ def _init_val_5(self, n, s=None): # Property decorator and universally-validated cache @property - @cached('val6', universe_validation=True) + @cached("val6", universe_validation=True) def val6(self): return self.ref5 + 1.0 @@ -772,40 +879,40 @@ def obj(self): def test_val1_lookup(self, obj): obj._clear_caches() - assert 'val1' not in obj._cache + assert "val1" not in obj._cache assert obj.val1() == obj.ref1 ret = obj.val1() - assert 'val1' in obj._cache - assert obj._cache['val1'] == ret - assert obj.val1() is obj._cache['val1'] + assert "val1" in obj._cache + assert obj._cache["val1"] == ret + assert obj.val1() is obj._cache["val1"] def test_val1_inject(self, obj): # Put something else into the cache and check it gets returned # this tests that the cache is blindly being used obj._clear_caches() ret = obj.val1() - assert 'val1' in obj._cache + assert "val1" in obj._cache assert ret == obj.ref1 new = 77.0 - obj._fill_cache('val1', new) + obj._fill_cache("val1", new) assert obj.val1() == new # Managed property def test_val2_lookup(self, obj): obj._clear_caches() - assert 'val2' not in obj._cache + assert "val2" not in obj._cache assert obj.val2 == obj.ref2 ret = obj.val2 - assert 'val2' in obj._cache - assert obj._cache['val2'] == ret + assert "val2" in obj._cache + assert obj._cache["val2"] == ret def test_val2_inject(self, obj): obj._clear_caches() ret = obj.val2 - assert 'val2' in obj._cache + assert "val2" in obj._cache assert ret == obj.ref2 new = 77.0 - obj._fill_cache('val2', new) + obj._fill_cache("val2", new) assert obj.val2 == new # Setter on cached attribute @@ -816,18 +923,18 @@ def test_val3_set(self, obj): new = 99.0 obj.val3 = new assert obj.val3 == new - assert obj._cache['val3'] == new + assert obj._cache["val3"] == new def test_val3_del(self, obj): # Check that deleting the property removes it from cache, obj._clear_caches() assert obj.val3 == obj.ref3 - assert 'val3' in obj._cache + assert "val3" in obj._cache del obj.val3 - assert 'val3' not in obj._cache + assert "val3" not in obj._cache # But allows it to work as usual afterwards assert obj.val3 == obj.ref3 - assert 'val3' in obj._cache + assert "val3" in obj._cache # Pass args def test_val4_args(self, obj): @@ -840,27 +947,27 @@ def test_val4_args(self, obj): # Pass args and kwargs def test_val5_kwargs(self, obj): obj._clear_caches() - assert obj.val5(5, s='abc') == 5 * 'abc' + assert obj.val5(5, s="abc") == 5 * "abc" - assert obj.val5(5, s='!!!') == 5 * 'abc' + assert obj.val5(5, s="!!!") == 5 * "abc" # property decorator, with universe validation def test_val6_universe_validation(self, obj): obj._clear_caches() - assert not hasattr(obj, '_cache_key') - assert 'val6' not in obj._cache - assert 'val6' not in obj.universe._cache['_valid'] + assert not hasattr(obj, "_cache_key") + assert "val6" not in obj._cache + assert "val6" not in obj.universe._cache["_valid"] ret = obj.val6 # Trigger caching assert obj.val6 == obj.ref6 assert ret is obj.val6 - assert 'val6' in obj._cache - assert 'val6' in obj.universe._cache['_valid'] - assert obj._cache_key in obj.universe._cache['_valid']['val6'] - assert obj._cache['val6'] is ret + assert "val6" in obj._cache + assert "val6" in obj.universe._cache["_valid"] + assert obj._cache_key in obj.universe._cache["_valid"]["val6"] + assert obj._cache["val6"] is ret # Invalidate cache at universe level - obj.universe._cache['_valid']['val6'].clear() + obj.universe._cache["_valid"]["val6"].clear() ret2 = obj.val6 assert ret2 is obj.val6 assert ret2 is not ret @@ -874,18 +981,19 @@ def test_val6_universe_validation(self, obj): class TestConvFloat(object): - @pytest.mark.parametrize('s, output', [ - ('0.45', 0.45), - ('.45', 0.45), - ('a.b', 'a.b') - ]) + @pytest.mark.parametrize( + "s, output", [("0.45", 0.45), (".45", 0.45), ("a.b", "a.b")] + ) def test_float(self, s, output): assert util.conv_float(s) == output - @pytest.mark.parametrize('input, output', [ - (('0.45', '0.56', '6.7'), [0.45, 0.56, 6.7]), - (('0.45', 'a.b', '!!'), [0.45, 'a.b', '!!']) - ]) + @pytest.mark.parametrize( + "input, output", + [ + (("0.45", "0.56", "6.7"), [0.45, 0.56, 6.7]), + (("0.45", "a.b", "!!"), [0.45, "a.b", "!!"]), + ], + ) def test_map(self, input, output): ret = [util.conv_float(el) for el in input] assert ret == output @@ -894,7 +1002,7 @@ def test_map(self, input, output): class TestFixedwidthBins(object): def test_keys(self): ret = util.fixedwidth_bins(0.5, 1.0, 2.0) - for k in ['Nbins', 'delta', 'min', 'max']: + for k in ["Nbins", "delta", "min", "max"]: assert k in ret def test_ValueError(self): @@ -902,49 +1010,63 @@ def test_ValueError(self): util.fixedwidth_bins(0.1, 5.0, 4.0) @pytest.mark.parametrize( - 'delta, xmin, xmax, output_Nbins, output_delta, output_min, output_max', + "delta, xmin, xmax, output_Nbins, output_delta, output_min, output_max", [ (0.1, 4.0, 5.0, 10, 0.1, 4.0, 5.0), - (0.4, 4.0, 5.0, 3, 0.4, 3.9, 5.1) - ]) - def test_usage(self, delta, xmin, xmax, output_Nbins, output_delta, - output_min, output_max): + (0.4, 4.0, 5.0, 3, 0.4, 3.9, 5.1), + ], + ) + def test_usage( + self, + delta, + xmin, + xmax, + output_Nbins, + output_delta, + output_min, + output_max, + ): ret = util.fixedwidth_bins(delta, xmin, xmax) - assert ret['Nbins'] == output_Nbins - assert ret['delta'] == output_delta - assert ret['min'], output_min - assert ret['max'], output_max + assert ret["Nbins"] == output_Nbins + assert ret["delta"] == output_delta + assert ret["min"], output_min + assert ret["max"], output_max @pytest.fixture def atoms(): from MDAnalysisTests import make_Universe + u = make_Universe(extras=("masses",), size=(3, 1, 1)) return u.atoms -@pytest.mark.parametrize('weights,result', - [ - (None, None), - ("mass", np.array([5.1, 4.2, 3.3])), - (np.array([12.0, 1.0, 12.0]), - np.array([12.0, 1.0, 12.0])), - ([12.0, 1.0, 12.0], np.array([12.0, 1.0, 12.0])), - (range(3), np.arange(3, dtype=int)), - ]) +@pytest.mark.parametrize( + "weights,result", + [ + (None, None), + ("mass", np.array([5.1, 4.2, 3.3])), + (np.array([12.0, 1.0, 12.0]), np.array([12.0, 1.0, 12.0])), + ([12.0, 1.0, 12.0], np.array([12.0, 1.0, 12.0])), + (range(3), np.arange(3, dtype=int)), + ], +) def test_check_weights_ok(atoms, weights, result): assert_array_equal(util.get_weights(atoms, weights), result) -@pytest.mark.parametrize('weights', - [42, - "geometry", - np.array(1.0), - np.array([12.0, 1.0, 12.0, 1.0]), - [12.0, 1.0], - np.array([[12.0, 1.0, 12.0]]), - np.array([[12.0, 1.0, 12.0], [12.0, 1.0, 12.0]]), - ]) +@pytest.mark.parametrize( + "weights", + [ + 42, + "geometry", + np.array(1.0), + np.array([12.0, 1.0, 12.0, 1.0]), + [12.0, 1.0], + np.array([[12.0, 1.0, 12.0]]), + np.array([[12.0, 1.0, 12.0], [12.0, 1.0, 12.0]]), + ], +) def test_check_weights_raises_ValueError(atoms, weights): with pytest.raises(ValueError): util.get_weights(atoms, weights) @@ -956,195 +1078,303 @@ class TestGuessFormat(object): Tests also getting the appropriate Parser and Reader from a given filename """ + # list of known formats, followed by the desired Parser and Reader # None indicates that there isn't a Reader for this format # All formats call fallback to the MinimalParser formats = [ - ('CHAIN', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.chain.ChainReader), - ('CONFIG', mda.topology.DLPolyParser.ConfigParser, - mda.coordinates.DLPoly.ConfigReader), - ('CRD', mda.topology.CRDParser.CRDParser, mda.coordinates.CRD.CRDReader), - ('DATA', mda.topology.LAMMPSParser.DATAParser, - mda.coordinates.LAMMPS.DATAReader), - ('DCD', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.DCD.DCDReader), - ('DMS', mda.topology.DMSParser.DMSParser, mda.coordinates.DMS.DMSReader), - ('GMS', mda.topology.GMSParser.GMSParser, mda.coordinates.GMS.GMSReader), - ('GRO', mda.topology.GROParser.GROParser, mda.coordinates.GRO.GROReader), - ('HISTORY', mda.topology.DLPolyParser.HistoryParser, - mda.coordinates.DLPoly.HistoryReader), - ('INPCRD', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.INPCRD.INPReader), - ('LAMMPS', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.LAMMPS.DCDReader), - ('MDCRD', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.TRJ.TRJReader), - ('MMTF', mda.topology.MMTFParser.MMTFParser, - mda.coordinates.MMTF.MMTFReader), - ('MOL2', mda.topology.MOL2Parser.MOL2Parser, - mda.coordinates.MOL2.MOL2Reader), - ('NC', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.TRJ.NCDFReader), - ('NCDF', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.TRJ.NCDFReader), - ('PDB', mda.topology.PDBParser.PDBParser, mda.coordinates.PDB.PDBReader), - ('PDBQT', mda.topology.PDBQTParser.PDBQTParser, - mda.coordinates.PDBQT.PDBQTReader), - ('PRMTOP', mda.topology.TOPParser.TOPParser, None), - ('PQR', mda.topology.PQRParser.PQRParser, mda.coordinates.PQR.PQRReader), - ('PSF', mda.topology.PSFParser.PSFParser, None), - ('RESTRT', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.INPCRD.INPReader), - ('TOP', mda.topology.TOPParser.TOPParser, None), - ('TPR', mda.topology.TPRParser.TPRParser, None), - ('TRJ', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.TRJ.TRJReader), - ('TRR', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.TRR.TRRReader), - ('XML', mda.topology.HoomdXMLParser.HoomdXMLParser, None), - ('XPDB', mda.topology.ExtendedPDBParser.ExtendedPDBParser, - mda.coordinates.PDB.ExtendedPDBReader), - ('XTC', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.XTC.XTCReader), - ('XYZ', mda.topology.XYZParser.XYZParser, mda.coordinates.XYZ.XYZReader), - ('TRZ', mda.topology.MinimalParser.MinimalParser, - mda.coordinates.TRZ.TRZReader), + ( + "CHAIN", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.chain.ChainReader, + ), + ( + "CONFIG", + mda.topology.DLPolyParser.ConfigParser, + mda.coordinates.DLPoly.ConfigReader, + ), + ( + "CRD", + mda.topology.CRDParser.CRDParser, + mda.coordinates.CRD.CRDReader, + ), + ( + "DATA", + mda.topology.LAMMPSParser.DATAParser, + mda.coordinates.LAMMPS.DATAReader, + ), + ( + "DCD", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.DCD.DCDReader, + ), + ( + "DMS", + mda.topology.DMSParser.DMSParser, + mda.coordinates.DMS.DMSReader, + ), + ( + "GMS", + mda.topology.GMSParser.GMSParser, + mda.coordinates.GMS.GMSReader, + ), + ( + "GRO", + mda.topology.GROParser.GROParser, + mda.coordinates.GRO.GROReader, + ), + ( + "HISTORY", + mda.topology.DLPolyParser.HistoryParser, + mda.coordinates.DLPoly.HistoryReader, + ), + ( + "INPCRD", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.INPCRD.INPReader, + ), + ( + "LAMMPS", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.LAMMPS.DCDReader, + ), + ( + "MDCRD", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.TRJ.TRJReader, + ), + ( + "MMTF", + mda.topology.MMTFParser.MMTFParser, + mda.coordinates.MMTF.MMTFReader, + ), + ( + "MOL2", + mda.topology.MOL2Parser.MOL2Parser, + mda.coordinates.MOL2.MOL2Reader, + ), + ( + "NC", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.TRJ.NCDFReader, + ), + ( + "NCDF", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.TRJ.NCDFReader, + ), + ( + "PDB", + mda.topology.PDBParser.PDBParser, + mda.coordinates.PDB.PDBReader, + ), + ( + "PDBQT", + mda.topology.PDBQTParser.PDBQTParser, + mda.coordinates.PDBQT.PDBQTReader, + ), + ("PRMTOP", mda.topology.TOPParser.TOPParser, None), + ( + "PQR", + mda.topology.PQRParser.PQRParser, + mda.coordinates.PQR.PQRReader, + ), + ("PSF", mda.topology.PSFParser.PSFParser, None), + ( + "RESTRT", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.INPCRD.INPReader, + ), + ("TOP", mda.topology.TOPParser.TOPParser, None), + ("TPR", mda.topology.TPRParser.TPRParser, None), + ( + "TRJ", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.TRJ.TRJReader, + ), + ( + "TRR", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.TRR.TRRReader, + ), + ("XML", mda.topology.HoomdXMLParser.HoomdXMLParser, None), + ( + "XPDB", + mda.topology.ExtendedPDBParser.ExtendedPDBParser, + mda.coordinates.PDB.ExtendedPDBReader, + ), + ( + "XTC", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.XTC.XTCReader, + ), + ( + "XYZ", + mda.topology.XYZParser.XYZParser, + mda.coordinates.XYZ.XYZReader, + ), + ( + "TRZ", + mda.topology.MinimalParser.MinimalParser, + mda.coordinates.TRZ.TRZReader, + ), ] # list of possible compressed extensions # include no extension too! - compressed_extensions = ['.bz2', '.gz'] + compressed_extensions = [".bz2", ".gz"] - @pytest.mark.parametrize('extention', - [format_tuple[0].upper() for format_tuple in - formats] + - [format_tuple[0].lower() for format_tuple in - formats]) + @pytest.mark.parametrize( + "extention", + [format_tuple[0].upper() for format_tuple in formats] + + [format_tuple[0].lower() for format_tuple in formats], + ) def test_get_extention(self, extention): """Check that get_ext works""" - file_name = 'file.{0}'.format(extention) + file_name = "file.{0}".format(extention) a, b = util.get_ext(file_name) - assert a == 'file' + assert a == "file" assert b == extention.lower() - @pytest.mark.parametrize('extention', - [format_tuple[0].upper() for format_tuple in - formats] + - [format_tuple[0].lower() for format_tuple in - formats]) + @pytest.mark.parametrize( + "extention", + [format_tuple[0].upper() for format_tuple in formats] + + [format_tuple[0].lower() for format_tuple in formats], + ) def test_compressed_without_compression_extention(self, extention): """Check that format suffixed by compressed extension works""" - file_name = 'file.{0}'.format(extention) + file_name = "file.{0}".format(extention) a = util.format_from_filename_extension(file_name) # expect answer to always be uppercase assert a == extention.upper() - @pytest.mark.parametrize('extention', - [format_tuple[0].upper() for format_tuple in - formats] + - [format_tuple[0].lower() for format_tuple in - formats]) - @pytest.mark.parametrize('compression_extention', compressed_extensions) + @pytest.mark.parametrize( + "extention", + [format_tuple[0].upper() for format_tuple in formats] + + [format_tuple[0].lower() for format_tuple in formats], + ) + @pytest.mark.parametrize("compression_extention", compressed_extensions) def test_compressed(self, extention, compression_extention): """Check that format suffixed by compressed extension works""" - file_name = 'file.{0}{1}'.format(extention, compression_extention) + file_name = "file.{0}{1}".format(extention, compression_extention) a = util.format_from_filename_extension(file_name) # expect answer to always be uppercase assert a == extention.upper() - @pytest.mark.parametrize('extention', - [format_tuple[0].upper() for format_tuple in - formats] + [format_tuple[0].lower() for - format_tuple in formats]) + @pytest.mark.parametrize( + "extention", + [format_tuple[0].upper() for format_tuple in formats] + + [format_tuple[0].lower() for format_tuple in formats], + ) def test_guess_format(self, extention): - file_name = 'file.{0}'.format(extention) + file_name = "file.{0}".format(extention) a = util.guess_format(file_name) # expect answer to always be uppercase assert a == extention.upper() - @pytest.mark.parametrize('extention', - [format_tuple[0].upper() for format_tuple in - formats] + [format_tuple[0].lower() for - format_tuple in formats]) - @pytest.mark.parametrize('compression_extention', compressed_extensions) + @pytest.mark.parametrize( + "extention", + [format_tuple[0].upper() for format_tuple in formats] + + [format_tuple[0].lower() for format_tuple in formats], + ) + @pytest.mark.parametrize("compression_extention", compressed_extensions) def test_guess_format_compressed(self, extention, compression_extention): - file_name = 'file.{0}{1}'.format(extention, compression_extention) + file_name = "file.{0}{1}".format(extention, compression_extention) a = util.guess_format(file_name) # expect answer to always be uppercase assert a == extention.upper() - @pytest.mark.parametrize('extention, parser', - [(format_tuple[0], format_tuple[1]) for - format_tuple in formats if - format_tuple[1] is not None] - ) + @pytest.mark.parametrize( + "extention, parser", + [ + (format_tuple[0], format_tuple[1]) + for format_tuple in formats + if format_tuple[1] is not None + ], + ) def test_get_parser(self, extention, parser): - file_name = 'file.{0}'.format(extention) + file_name = "file.{0}".format(extention) a = mda.topology.core.get_parser_for(file_name) assert a == parser - @pytest.mark.parametrize('extention, parser', - [(format_tuple[0], format_tuple[1]) for - format_tuple in formats if - format_tuple[1] is not None] - ) - @pytest.mark.parametrize('compression_extention', compressed_extensions) - def test_get_parser_compressed(self, extention, parser, - compression_extention): - file_name = 'file.{0}{1}'.format(extention, compression_extention) + @pytest.mark.parametrize( + "extention, parser", + [ + (format_tuple[0], format_tuple[1]) + for format_tuple in formats + if format_tuple[1] is not None + ], + ) + @pytest.mark.parametrize("compression_extention", compressed_extensions) + def test_get_parser_compressed( + self, extention, parser, compression_extention + ): + file_name = "file.{0}{1}".format(extention, compression_extention) a = mda.topology.core.get_parser_for(file_name) assert a == parser - @pytest.mark.parametrize('extention', - [(format_tuple[0], format_tuple[1]) for - format_tuple in formats if - format_tuple[1] is None] - ) + @pytest.mark.parametrize( + "extention", + [ + (format_tuple[0], format_tuple[1]) + for format_tuple in formats + if format_tuple[1] is None + ], + ) def test_get_parser_invalid(self, extention): - file_name = 'file.{0}'.format(extention) + file_name = "file.{0}".format(extention) with pytest.raises(ValueError): mda.topology.core.get_parser_for(file_name) - @pytest.mark.parametrize('extention, reader', - [(format_tuple[0], format_tuple[2]) for - format_tuple in formats if - format_tuple[2] is not None] - ) + @pytest.mark.parametrize( + "extention, reader", + [ + (format_tuple[0], format_tuple[2]) + for format_tuple in formats + if format_tuple[2] is not None + ], + ) def test_get_reader(self, extention, reader): - file_name = 'file.{0}'.format(extention) + file_name = "file.{0}".format(extention) a = mda.coordinates.core.get_reader_for(file_name) assert a == reader - @pytest.mark.parametrize('extention, reader', - [(format_tuple[0], format_tuple[2]) for - format_tuple in formats if - format_tuple[2] is not None] - ) - @pytest.mark.parametrize('compression_extention', compressed_extensions) - def test_get_reader_compressed(self, extention, reader, - compression_extention): - file_name = 'file.{0}{1}'.format(extention, compression_extention) + @pytest.mark.parametrize( + "extention, reader", + [ + (format_tuple[0], format_tuple[2]) + for format_tuple in formats + if format_tuple[2] is not None + ], + ) + @pytest.mark.parametrize("compression_extention", compressed_extensions) + def test_get_reader_compressed( + self, extention, reader, compression_extention + ): + file_name = "file.{0}{1}".format(extention, compression_extention) a = mda.coordinates.core.get_reader_for(file_name) assert a == reader - @pytest.mark.parametrize('extention', - [(format_tuple[0], format_tuple[2]) for - format_tuple in formats if - format_tuple[2] is None] - ) + @pytest.mark.parametrize( + "extention", + [ + (format_tuple[0], format_tuple[2]) + for format_tuple in formats + if format_tuple[2] is None + ], + ) def test_get_reader_invalid(self, extention): - file_name = 'file.{0}'.format(extention) + file_name = "file.{0}".format(extention) with pytest.raises(ValueError): mda.coordinates.core.get_reader_for(file_name) def test_check_compressed_format_TypeError(self): with pytest.raises(TypeError): - util.check_compressed_format(1234, 'bz2') + util.check_compressed_format(1234, "bz2") def test_format_from_filename_TypeError(self): with pytest.raises(TypeError): @@ -1152,7 +1382,7 @@ def test_format_from_filename_TypeError(self): def test_guess_format_stream_ValueError(self): # This stream has no name, so can't guess format - s = StringIO('this is a very fun file') + s = StringIO("this is a very fun file") with pytest.raises(ValueError): util.guess_format(s) @@ -1166,22 +1396,23 @@ class TestUniqueRows(object): def test_unique_rows_2(self): a = np.array([[0, 1], [1, 2], [2, 1], [0, 1], [0, 1], [2, 1]]) - assert_array_equal(util.unique_rows(a), - np.array([[0, 1], [1, 2], [2, 1]])) + assert_array_equal( + util.unique_rows(a), np.array([[0, 1], [1, 2], [2, 1]]) + ) def test_unique_rows_3(self): a = np.array([[0, 1, 2], [0, 1, 2], [2, 3, 4], [0, 1, 2]]) - assert_array_equal(util.unique_rows(a), - np.array([[0, 1, 2], [2, 3, 4]])) + assert_array_equal( + util.unique_rows(a), np.array([[0, 1, 2], [2, 3, 4]]) + ) def test_unique_rows_with_view(self): # unique_rows doesn't work when flags['OWNDATA'] is False, # happens when second dimension is created through broadcast a = np.array([1, 2]) - assert_array_equal(util.unique_rows(a[None, :]), - np.array([[1, 2]])) + assert_array_equal(util.unique_rows(a[None, :]), np.array([[1, 2]])) class TestGetWriterFor(object): @@ -1192,7 +1423,7 @@ def test_no_filename_argument(self): mda.coordinates.core.get_writer_for() def test_precedence(self): - writer = mda.coordinates.core.get_writer_for('test.pdb', 'GRO') + writer = mda.coordinates.core.get_writer_for("test.pdb", "GRO") assert writer == mda.coordinates.GRO.GROWriter # Make sure ``get_writer_for`` uses *format* if provided @@ -1200,7 +1431,7 @@ def test_missing_extension(self): # Make sure ``get_writer_for`` behave as expected if *filename* # has no extension with pytest.raises(ValueError): - mda.coordinates.core.get_writer_for(filename='test', format=None) + mda.coordinates.core.get_writer_for(filename="test", format=None) def test_extension_empty_string(self): """ @@ -1210,29 +1441,30 @@ def test_extension_empty_string(self): valid formats. """ with pytest.raises(ValueError): - mda.coordinates.core.get_writer_for(filename='test', format='') + mda.coordinates.core.get_writer_for(filename="test", format="") def test_file_no_extension(self): """No format given""" with pytest.raises(ValueError): - mda.coordinates.core.get_writer_for('outtraj') + mda.coordinates.core.get_writer_for("outtraj") def test_wrong_format(self): # Make sure ``get_writer_for`` fails if the format is unknown with pytest.raises(TypeError): - mda.coordinates.core.get_writer_for(filename="fail_me", - format='UNK') + mda.coordinates.core.get_writer_for( + filename="fail_me", format="UNK" + ) def test_compressed_extension(self): - for ext in ('.gz', '.bz2'): - fn = 'test.gro' + ext + for ext in (".gz", ".bz2"): + fn = "test.gro" + ext writer = mda.coordinates.core.get_writer_for(filename=fn) assert writer == mda.coordinates.GRO.GROWriter # Make sure ``get_writer_for`` works with compressed file file names def test_compressed_extension_fail(self): - for ext in ('.gz', '.bz2'): - fn = 'test.unk' + ext + for ext in (".gz", ".bz2"): + fn = "test.unk" + ext # Make sure ``get_writer_for`` fails if an unknown format is compressed with pytest.raises(TypeError): mda.coordinates.core.get_writer_for(filename=fn) @@ -1240,83 +1472,131 @@ def test_compressed_extension_fail(self): def test_non_string_filename(self): # Does ``get_writer_for`` fails with non string filename, no format with pytest.raises(ValueError): - mda.coordinates.core.get_writer_for(filename=StringIO(), - format=None) + mda.coordinates.core.get_writer_for( + filename=StringIO(), format=None + ) def test_multiframe_failure(self): # does ``get_writer_for`` fail with invalid format and multiframe not None with pytest.raises(TypeError): - mda.coordinates.core.get_writer_for(filename="fail_me", - format='UNK', multiframe=True) - mda.coordinates.core.get_writer_for(filename="fail_me", - format='UNK', multiframe=False) + mda.coordinates.core.get_writer_for( + filename="fail_me", format="UNK", multiframe=True + ) + mda.coordinates.core.get_writer_for( + filename="fail_me", format="UNK", multiframe=False + ) def test_multiframe_nonsense(self): with pytest.raises(ValueError): - mda.coordinates.core.get_writer_for(filename='this.gro', - multiframe='sandwich') + mda.coordinates.core.get_writer_for( + filename="this.gro", multiframe="sandwich" + ) formats = [ # format name, related class, singleframe, multiframe - ('CRD', mda.coordinates.CRD.CRDWriter, True, False), - ('DATA', mda.coordinates.LAMMPS.DATAWriter, True, False), - ('DCD', mda.coordinates.DCD.DCDWriter, True, True), + ("CRD", mda.coordinates.CRD.CRDWriter, True, False), + ("DATA", mda.coordinates.LAMMPS.DATAWriter, True, False), + ("DCD", mda.coordinates.DCD.DCDWriter, True, True), # ('ENT', mda.coordinates.PDB.PDBWriter, True, False), - ('GRO', mda.coordinates.GRO.GROWriter, True, False), - ('LAMMPS', mda.coordinates.LAMMPS.DCDWriter, True, True), - ('MOL2', mda.coordinates.MOL2.MOL2Writer, True, True), - ('NCDF', mda.coordinates.TRJ.NCDFWriter, True, True), - ('NULL', mda.coordinates.null.NullWriter, True, True), + ("GRO", mda.coordinates.GRO.GROWriter, True, False), + ("LAMMPS", mda.coordinates.LAMMPS.DCDWriter, True, True), + ("MOL2", mda.coordinates.MOL2.MOL2Writer, True, True), + ("NCDF", mda.coordinates.TRJ.NCDFWriter, True, True), + ("NULL", mda.coordinates.null.NullWriter, True, True), # ('PDB', mda.coordinates.PDB.PDBWriter, True, True), special case, done separately - ('PDBQT', mda.coordinates.PDBQT.PDBQTWriter, True, False), - ('PQR', mda.coordinates.PQR.PQRWriter, True, False), - ('TRR', mda.coordinates.TRR.TRRWriter, True, True), - ('XTC', mda.coordinates.XTC.XTCWriter, True, True), - ('XYZ', mda.coordinates.XYZ.XYZWriter, True, True), - ('TRZ', mda.coordinates.TRZ.TRZWriter, True, True), + ("PDBQT", mda.coordinates.PDBQT.PDBQTWriter, True, False), + ("PQR", mda.coordinates.PQR.PQRWriter, True, False), + ("TRR", mda.coordinates.TRR.TRRWriter, True, True), + ("XTC", mda.coordinates.XTC.XTCWriter, True, True), + ("XYZ", mda.coordinates.XYZ.XYZWriter, True, True), + ("TRZ", mda.coordinates.TRZ.TRZWriter, True, True), ] - @pytest.mark.parametrize('format, writer', - [(format_tuple[0], format_tuple[1]) for - format_tuple in formats if - format_tuple[2] is True]) + @pytest.mark.parametrize( + "format, writer", + [ + (format_tuple[0], format_tuple[1]) + for format_tuple in formats + if format_tuple[2] is True + ], + ) def test_singleframe(self, format, writer): - assert mda.coordinates.core.get_writer_for('this', format=format, - multiframe=False) == writer + assert ( + mda.coordinates.core.get_writer_for( + "this", format=format, multiframe=False + ) + == writer + ) - @pytest.mark.parametrize('format', [(format_tuple[0], format_tuple[1]) for - format_tuple in formats if - format_tuple[2] is False]) + @pytest.mark.parametrize( + "format", + [ + (format_tuple[0], format_tuple[1]) + for format_tuple in formats + if format_tuple[2] is False + ], + ) def test_singleframe_fails(self, format): with pytest.raises(TypeError): - mda.coordinates.core.get_writer_for('this', format=format, - multiframe=False) + mda.coordinates.core.get_writer_for( + "this", format=format, multiframe=False + ) - @pytest.mark.parametrize('format, writer', - [(format_tuple[0], format_tuple[1]) for - format_tuple in formats if - format_tuple[3] is True]) + @pytest.mark.parametrize( + "format, writer", + [ + (format_tuple[0], format_tuple[1]) + for format_tuple in formats + if format_tuple[3] is True + ], + ) def test_multiframe(self, format, writer): - assert mda.coordinates.core.get_writer_for('this', format=format, - multiframe=True) == writer + assert ( + mda.coordinates.core.get_writer_for( + "this", format=format, multiframe=True + ) + == writer + ) - @pytest.mark.parametrize('format', - [format_tuple[0] for format_tuple in formats if - format_tuple[3] is False]) + @pytest.mark.parametrize( + "format", + [ + format_tuple[0] + for format_tuple in formats + if format_tuple[3] is False + ], + ) def test_multiframe_fails(self, format): with pytest.raises(TypeError): - mda.coordinates.core.get_writer_for('this', format=format, - multiframe=True) + mda.coordinates.core.get_writer_for( + "this", format=format, multiframe=True + ) def test_get_writer_for_pdb(self): - assert mda.coordinates.core.get_writer_for('this', format='PDB', - multiframe=False) == mda.coordinates.PDB.PDBWriter - assert mda.coordinates.core.get_writer_for('this', format='PDB', - multiframe=True) == mda.coordinates.PDB.MultiPDBWriter - assert mda.coordinates.core.get_writer_for('this', format='ENT', - multiframe=False) == mda.coordinates.PDB.PDBWriter - assert mda.coordinates.core.get_writer_for('this', format='ENT', - multiframe=True) == mda.coordinates.PDB.MultiPDBWriter + assert ( + mda.coordinates.core.get_writer_for( + "this", format="PDB", multiframe=False + ) + == mda.coordinates.PDB.PDBWriter + ) + assert ( + mda.coordinates.core.get_writer_for( + "this", format="PDB", multiframe=True + ) + == mda.coordinates.PDB.MultiPDBWriter + ) + assert ( + mda.coordinates.core.get_writer_for( + "this", format="ENT", multiframe=False + ) + == mda.coordinates.PDB.PDBWriter + ) + assert ( + mda.coordinates.core.get_writer_for( + "this", format="ENT", multiframe=True + ) + == mda.coordinates.PDB.MultiPDBWriter + ) class TestBlocksOf(object): @@ -1326,17 +1606,24 @@ def test_blocks_of_1(self): view = util.blocks_of(arr, 1, 1) assert view.shape == (4, 1, 1) - assert_array_almost_equal(view, - np.array([[[0]], [[5]], [[10]], [[15]]])) + assert_array_almost_equal( + view, np.array([[[0]], [[5]], [[10]], [[15]]]) + ) # Change my view, check changes are reflected in arr view[:] = 1001 - assert_array_almost_equal(arr, - np.array([[1001, 1, 2, 3], - [4, 1001, 6, 7], - [8, 9, 1001, 11], - [12, 13, 14, 1001]])) + assert_array_almost_equal( + arr, + np.array( + [ + [1001, 1, 2, 3], + [4, 1001, 6, 7], + [8, 9, 1001, 11], + [12, 13, 14, 1001], + ] + ), + ) def test_blocks_of_2(self): arr = np.arange(16).reshape(4, 4) @@ -1344,17 +1631,24 @@ def test_blocks_of_2(self): view = util.blocks_of(arr, 2, 2) assert view.shape == (2, 2, 2) - assert_array_almost_equal(view, np.array([[[0, 1], [4, 5]], - [[10, 11], [14, 15]]])) + assert_array_almost_equal( + view, np.array([[[0, 1], [4, 5]], [[10, 11], [14, 15]]]) + ) view[0] = 100 view[1] = 200 - assert_array_almost_equal(arr, - np.array([[100, 100, 2, 3], - [100, 100, 6, 7], - [8, 9, 200, 200], - [12, 13, 200, 200]])) + assert_array_almost_equal( + arr, + np.array( + [ + [100, 100, 2, 3], + [100, 100, 6, 7], + [8, 9, 200, 200], + [12, 13, 200, 200], + ] + ), + ) def test_blocks_of_3(self): # testing non square array @@ -1380,11 +1674,14 @@ def test_blocks_of_ValueError(self): util.blocks_of(arr[:, ::2], 2, 1) # non-contiguous input -@pytest.mark.parametrize('arr,answer', [ - ([2, 3, 4, 7, 8, 9, 10, 15, 16], [[2, 3, 4], [7, 8, 9, 10], [15, 16]]), - ([11, 12, 13, 14, 15, 16], [[11, 12, 13, 14, 15, 16]]), - ([1, 2, 2, 2, 3, 6], [[1, 2, 2, 2, 3], [6]]) -]) +@pytest.mark.parametrize( + "arr,answer", + [ + ([2, 3, 4, 7, 8, 9, 10, 15, 16], [[2, 3, 4], [7, 8, 9, 10], [15, 16]]), + ([11, 12, 13, 14, 15, 16], [[11, 12, 13, 14, 15, 16]]), + ([1, 2, 2, 2, 3, 6], [[1, 2, 2, 2, 3], [6]]), + ], +) def test_group_same_or_consecutive_integers(arr, answer): assert_equal(util.group_same_or_consecutive_integers(arr), answer) @@ -1397,22 +1694,22 @@ def ns(): def test_getitem(self, ns): ns.this = 42 - assert ns['this'] == 42 + assert ns["this"] == 42 def test_getitem_KeyError(self, ns): with pytest.raises(KeyError): - dict.__getitem__(ns, 'this') + dict.__getitem__(ns, "this") def test_setitem(self, ns): - ns['this'] = 42 + ns["this"] = 42 - assert ns['this'] == 42 + assert ns["this"] == 42 def test_delitem(self, ns): - ns['this'] = 42 - assert 'this' in ns - del ns['this'] - assert 'this' not in ns + ns["this"] = 42 + assert "this" in ns + del ns["this"] + assert "this" not in ns def test_delitem_AttributeError(self, ns): with pytest.raises(AttributeError): @@ -1424,55 +1721,58 @@ def test_setattr(self, ns): assert ns.this == 42 def test_getattr(self, ns): - ns['this'] = 42 + ns["this"] = 42 assert ns.this == 42 def test_getattr_AttributeError(self, ns): with pytest.raises(AttributeError): - getattr(ns, 'this') + getattr(ns, "this") def test_delattr(self, ns): - ns['this'] = 42 + ns["this"] = 42 - assert 'this' in ns + assert "this" in ns del ns.this - assert 'this' not in ns + assert "this" not in ns def test_eq(self, ns): - ns['this'] = 42 + ns["this"] = 42 ns2 = util.Namespace() - ns2['this'] = 42 + ns2["this"] = 42 assert ns == ns2 def test_len(self, ns): assert len(ns) == 0 - ns['this'] = 1 - ns['that'] = 2 + ns["this"] = 1 + ns["that"] = 2 assert len(ns) == 2 def test_iter(self, ns): - ns['this'] = 12 - ns['that'] = 24 - ns['other'] = 48 + ns["this"] = 12 + ns["that"] = 24 + ns["other"] = 48 seen = [] for val in ns: seen.append(val) - for val in ['this', 'that', 'other']: + for val in ["this", "that", "other"]: assert val in seen class TestTruncateInteger(object): - @pytest.mark.parametrize('a, b', [ - ((1234, 1), 4), - ((1234, 2), 34), - ((1234, 3), 234), - ((1234, 4), 1234), - ((1234, 5), 1234), - ]) + @pytest.mark.parametrize( + "a, b", + [ + ((1234, 1), 4), + ((1234, 2), 34), + ((1234, 3), 234), + ((1234, 4), 1234), + ((1234, 5), 1234), + ], + ) def test_ltruncate_int(self, a, b): assert util.ltruncate_int(*a) == b @@ -1480,9 +1780,9 @@ def test_ltruncate_int(self, a, b): class TestFlattenDict(object): def test_flatten_dict(self): d = { - 'A': {1: ('a', 'b', 'c')}, - 'B': {2: ('c', 'd', 'e')}, - 'C': {3: ('f', 'g', 'h')} + "A": {1: ("a", "b", "c")}, + "B": {2: ("c", "d", "e")}, + "C": {3: ("f", "g", "h")}, } result = util.flatten_dict(d) @@ -1495,53 +1795,57 @@ def test_flatten_dict(self): class TestStaticVariables(object): - """Tests concerning the decorator @static_variables - """ + """Tests concerning the decorator @static_variables""" def test_static_variables(self): x = [0] - @static_variables(foo=0, bar={'test': x}) + @static_variables(foo=0, bar={"test": x}) def myfunc(): assert myfunc.foo == 0 assert type(myfunc.bar) is type(dict()) - if 'test2' not in myfunc.bar: - myfunc.bar['test2'] = "a" + if "test2" not in myfunc.bar: + myfunc.bar["test2"] = "a" else: - myfunc.bar['test2'] += "a" - myfunc.bar['test'][0] += 1 - return myfunc.bar['test'] + myfunc.bar["test2"] += "a" + myfunc.bar["test"][0] += 1 + return myfunc.bar["test"] - assert hasattr(myfunc, 'foo') - assert hasattr(myfunc, 'bar') + assert hasattr(myfunc, "foo") + assert hasattr(myfunc, "bar") y = myfunc() assert y is x assert x[0] == 1 - assert myfunc.bar['test'][0] == 1 - assert myfunc.bar['test2'] == "a" + assert myfunc.bar["test"][0] == 1 + assert myfunc.bar["test2"] == "a" x = [0] y = myfunc() assert y is not x - assert myfunc.bar['test'][0] == 2 - assert myfunc.bar['test2'] == "aa" + assert myfunc.bar["test"][0] == 2 + assert myfunc.bar["test2"] == "aa" class TestWarnIfNotUnique(object): - """Tests concerning the decorator @warn_if_not_unique - """ + """Tests concerning the decorator @warn_if_not_unique""" def warn_msg(self, func, group, group_name): - msg = ("{}.{}(): {} {} contains duplicates. Results might be " - "biased!".format(group.__class__.__name__, func.__name__, - group_name, group.__repr__())) + msg = ( + "{}.{}(): {} {} contains duplicates. Results might be " + "biased!".format( + group.__class__.__name__, + func.__name__, + group_name, + group.__repr__(), + ) + ) return msg def test_warn_if_not_unique(self, atoms): # Check that the warn_if_not_unique decorator has a "static variable" # warn_if_not_unique.warned: - assert hasattr(warn_if_not_unique, 'warned') + assert hasattr(warn_if_not_unique, "warned") assert warn_if_not_unique.warned is False def test_warn_if_not_unique_once_outer(self, atoms): @@ -1667,8 +1971,11 @@ def test_warn_if_not_unique_unnamed(self, atoms): def func(group): pass - msg = self.warn_msg(func, atoms + atoms[0], - "'unnamed {}'".format(atoms.__class__.__name__)) + msg = self.warn_msg( + func, + atoms + atoms[0], + "'unnamed {}'".format(atoms.__class__.__name__), + ) with pytest.warns(DuplicateWarning) as w: func(atoms + atoms[0]) # Check warning message: @@ -1702,8 +2009,7 @@ def func(group): class TestCheckCoords(object): - """Tests concerning the decorator @check_coords - """ + """Tests concerning the decorator @check_coords""" prec = 6 @@ -1712,7 +2018,7 @@ def test_default_options(self): b_in = np.ones(3, dtype=np.float32) b_in2 = np.ones((2, 3), dtype=np.float32) - @check_coords('a', 'b') + @check_coords("a", "b") def func(a, b): # check that enforce_copy is True by default: assert a is not a_in @@ -1739,24 +2045,36 @@ def atomgroup(self): return u.atoms # check atomgroup handling with every option except allow_atomgroup - @pytest.mark.parametrize('enforce_copy', [True, False]) - @pytest.mark.parametrize('enforce_dtype', [True, False]) - @pytest.mark.parametrize('allow_single', [True, False]) - @pytest.mark.parametrize('convert_single', [True, False]) - @pytest.mark.parametrize('reduce_result_if_single', [True, False]) - @pytest.mark.parametrize('check_lengths_match', [True, False]) - def test_atomgroup(self, atomgroup, enforce_copy, enforce_dtype, - allow_single, convert_single, reduce_result_if_single, - check_lengths_match): + @pytest.mark.parametrize("enforce_copy", [True, False]) + @pytest.mark.parametrize("enforce_dtype", [True, False]) + @pytest.mark.parametrize("allow_single", [True, False]) + @pytest.mark.parametrize("convert_single", [True, False]) + @pytest.mark.parametrize("reduce_result_if_single", [True, False]) + @pytest.mark.parametrize("check_lengths_match", [True, False]) + def test_atomgroup( + self, + atomgroup, + enforce_copy, + enforce_dtype, + allow_single, + convert_single, + reduce_result_if_single, + check_lengths_match, + ): ag1 = atomgroup ag2 = atomgroup - @check_coords('ag1', 'ag2', enforce_copy=enforce_copy, - enforce_dtype=enforce_dtype, allow_single=allow_single, - convert_single=convert_single, - reduce_result_if_single=reduce_result_if_single, - check_lengths_match=check_lengths_match, - allow_atomgroup=True) + @check_coords( + "ag1", + "ag2", + enforce_copy=enforce_copy, + enforce_dtype=enforce_dtype, + allow_single=allow_single, + convert_single=convert_single, + reduce_result_if_single=reduce_result_if_single, + check_lengths_match=check_lengths_match, + allow_atomgroup=True, + ) def func(ag1, ag2): assert_allclose(ag1, ag2) assert isinstance(ag1, np.ndarray) @@ -1766,11 +2084,11 @@ def func(ag1, ag2): res = func(ag1, ag2) - assert_allclose(res, atomgroup.positions*2) + assert_allclose(res, atomgroup.positions * 2) def test_atomgroup_not_allowed(self, atomgroup): - @check_coords('ag1', allow_atomgroup=False) + @check_coords("ag1", allow_atomgroup=False) def func(ag1): return ag1 @@ -1779,7 +2097,7 @@ def func(ag1): def test_atomgroup_not_allowed_default(self, atomgroup): - @check_coords('ag1') + @check_coords("ag1") def func_default(ag1): return ag1 @@ -1793,7 +2111,7 @@ def test_enforce_copy(self): c_2d = np.zeros((1, 6), dtype=np.float32)[:, ::2] d_2d = np.zeros((1, 3), dtype=np.int64) - @check_coords('a', 'b', 'c', 'd', enforce_copy=False) + @check_coords("a", "b", "c", "d", enforce_copy=False) def func(a, b, c, d): # Assert that if enforce_copy is False: # no copy is made if input shape, order, and dtype are correct: @@ -1824,7 +2142,7 @@ def func(a, b, c, d): def test_no_allow_single(self): - @check_coords('a', allow_single=False) + @check_coords("a", allow_single=False) def func(a): pass @@ -1836,7 +2154,7 @@ def test_no_convert_single(self): a_1d = np.arange(-3, 0, dtype=np.float32) - @check_coords('a', enforce_copy=False, convert_single=False) + @check_coords("a", enforce_copy=False, convert_single=False) def func(a): # assert no conversion and no copy were performed: assert a is a_1d @@ -1852,8 +2170,12 @@ def test_no_reduce_result_if_single(self): a_1d = np.zeros(3, dtype=np.float32) # Test without shape conversion: - @check_coords('a', enforce_copy=False, convert_single=False, - reduce_result_if_single=False) + @check_coords( + "a", + enforce_copy=False, + convert_single=False, + reduce_result_if_single=False, + ) def func(a): return a @@ -1862,7 +2184,7 @@ def func(a): assert res is a_1d # Test with shape conversion: - @check_coords('a', enforce_copy=False, reduce_result_if_single=False) + @check_coords("a", enforce_copy=False, reduce_result_if_single=False) def func(a): return a @@ -1875,7 +2197,7 @@ def test_no_check_lengths_match(self): a_2d = np.zeros((1, 3), dtype=np.float32) b_2d = np.zeros((3, 3), dtype=np.float32) - @check_coords('a', 'b', enforce_copy=False, check_lengths_match=False) + @check_coords("a", "b", enforce_copy=False, check_lengths_match=False) def func(a, b): return a, b @@ -1889,52 +2211,59 @@ def test_atomgroup_mismatched_lengths(self): ag1 = u.select_atoms("index 0 to 10") ag2 = u.atoms - @check_coords('ag1', 'ag2', check_lengths_match=True, - allow_atomgroup=True) + @check_coords( + "ag1", "ag2", check_lengths_match=True, allow_atomgroup=True + ) def func(ag1, ag2): return ag1, ag2 - with pytest.raises(ValueError, match="must contain the same number of " - "coordinates"): + with pytest.raises( + ValueError, match="must contain the same number of " "coordinates" + ): _, _ = func(ag1, ag2) def test_invalid_input(self): - a_inv_dtype = np.array([['hello', 'world', '!']]) - a_inv_type = [[0., 0., 0.]] + a_inv_dtype = np.array([["hello", "world", "!"]]) + a_inv_type = [[0.0, 0.0, 0.0]] a_inv_shape_1d = np.zeros(6, dtype=np.float32) a_inv_shape_2d = np.zeros((3, 2), dtype=np.float32) - @check_coords('a') + @check_coords("a") def func(a): pass with pytest.raises(TypeError) as err: func(a_inv_dtype) - assert err.msg.startswith("func(): a.dtype must be convertible to " - "float32, got ") + assert err.msg.startswith( + "func(): a.dtype must be convertible to " "float32, got " + ) with pytest.raises(TypeError) as err: func(a_inv_type) - assert err.msg == ("func(): Parameter 'a' must be a numpy.ndarray, " - "got .") + assert err.msg == ( + "func(): Parameter 'a' must be a numpy.ndarray, " + "got ." + ) with pytest.raises(ValueError) as err: func(a_inv_shape_1d) - assert err.msg == ("func(): a.shape must be (3,) or (n, 3), got " - "(6,).") + assert err.msg == ( + "func(): a.shape must be (3,) or (n, 3), got " "(6,)." + ) with pytest.raises(ValueError) as err: func(a_inv_shape_2d) - assert err.msg == ("func(): a.shape must be (3,) or (n, 3), got " - "(3, 2).") + assert err.msg == ( + "func(): a.shape must be (3,) or (n, 3), got " "(3, 2)." + ) def test_usage_with_kwargs(self): a_2d = np.zeros((1, 3), dtype=np.float32) - @check_coords('a', enforce_copy=False) + @check_coords("a", enforce_copy=False) def func(a, b, c=0): return a, b, c @@ -1946,7 +2275,7 @@ def func(a, b, c=0): def test_wrong_func_call(self): - @check_coords('a', enforce_copy=False) + @check_coords("a", enforce_copy=False) def func(a, b, c=0): pass @@ -2000,32 +2329,41 @@ def func(): # usage without arguments: with pytest.raises(ValueError) as err: + @check_coords() def func(): pass - assert err.msg == ("Decorator check_coords() cannot be used " - "without positional arguments.") + assert err.msg == ( + "Decorator check_coords() cannot be used " + "without positional arguments." + ) # usage with defaultarg: with pytest.raises(ValueError) as err: - @check_coords('a') + + @check_coords("a") def func(a=1): pass - assert err.msg == ("In decorator check_coords(): Name 'a' doesn't " - "correspond to any positional argument of the " - "decorated function func().") + assert err.msg == ( + "In decorator check_coords(): Name 'a' doesn't " + "correspond to any positional argument of the " + "decorated function func()." + ) # usage with invalid parameter name: with pytest.raises(ValueError) as err: - @check_coords('b') + + @check_coords("b") def func(a): pass - assert err.msg == ("In decorator check_coords(): Name 'b' doesn't " - "correspond to any positional argument of the " - "decorated function func().") + assert err.msg == ( + "In decorator check_coords(): Name 'b' doesn't " + "correspond to any positional argument of the " + "decorated function func()." + ) @pytest.mark.parametrize("old_name", (None, "MDAnalysis.Universe")) @@ -2050,10 +2388,14 @@ def AlternateUniverse(anything): """ return True - oldfunc = util.deprecate(AlternateUniverse, old_name=old_name, - new_name=new_name, - release=release, remove=remove, - message=message) + oldfunc = util.deprecate( + AlternateUniverse, + old_name=old_name, + new_name=new_name, + release=release, + remove=remove, + message=message, + ) # match_expr changed to match (Issue 2329) with pytest.warns(DeprecationWarning, match="`.+` is deprecated"): oldfunc(42) @@ -2071,13 +2413,15 @@ def AlternateUniverse(anything): default_message = "`{0}` is deprecated!".format(name) else: default_message = "`{0}` is deprecated, use `{1}` instead!".format( - name, new_name) + name, new_name + ) deprecation_line_2 = default_message assert re.search(deprecation_line_2, doc) if remove: deprecation_line_3 = "`{0}` will be removed in release {1}".format( - name, remove) + name, remove + ) assert re.search(deprecation_line_3, doc) # check that the old docs are still present @@ -2092,16 +2436,21 @@ def test_deprecate_missing_release_ValueError(): def test_set_function_name(name="bar"): def foo(): pass + util._set_function_name(foo, name) assert foo.__name__ == name -@pytest.mark.parametrize("text", - ("", - "one line text", - " one line with leading space", - "multiline\n\n with some\n leading space", - " multiline\n\n with all\n leading space")) +@pytest.mark.parametrize( + "text", + ( + "", + "one line text", + " one line with leading space", + "multiline\n\n with some\n leading space", + " multiline\n\n with all\n leading space", + ), +) def test_dedent_docstring(text): doc = util.dedent_docstring(text) for line in doc.splitlines(): @@ -2112,56 +2461,61 @@ class TestCheckBox(object): prec = 6 ref_ortho = np.ones(3, dtype=np.float32) - ref_tri_vecs = np.array([[1, 0, 0], [0, 1, 0], [0, 2 ** 0.5, 2 ** 0.5]], - dtype=np.float32) - - @pytest.mark.parametrize('box', - ([1, 1, 1, 90, 90, 90], - (1, 1, 1, 90, 90, 90), - ['1', '1', 1, 90, '90', '90'], - ('1', '1', 1, 90, '90', '90'), - np.array(['1', '1', 1, 90, '90', '90']), - np.array([1, 1, 1, 90, 90, 90], - dtype=np.float32), - np.array([1, 1, 1, 90, 90, 90], - dtype=np.float64), - np.array([1, 1, 1, 1, 1, 1, - 90, 90, 90, 90, 90, 90], - dtype=np.float32)[::2])) + ref_tri_vecs = np.array( + [[1, 0, 0], [0, 1, 0], [0, 2**0.5, 2**0.5]], dtype=np.float32 + ) + + @pytest.mark.parametrize( + "box", + ( + [1, 1, 1, 90, 90, 90], + (1, 1, 1, 90, 90, 90), + ["1", "1", 1, 90, "90", "90"], + ("1", "1", 1, 90, "90", "90"), + np.array(["1", "1", 1, 90, "90", "90"]), + np.array([1, 1, 1, 90, 90, 90], dtype=np.float32), + np.array([1, 1, 1, 90, 90, 90], dtype=np.float64), + np.array( + [1, 1, 1, 1, 1, 1, 90, 90, 90, 90, 90, 90], dtype=np.float32 + )[::2], + ), + ) def test_check_box_ortho(self, box): boxtype, checked_box = util.check_box(box) - assert boxtype == 'ortho' + assert boxtype == "ortho" assert_allclose(checked_box, self.ref_ortho) assert checked_box.dtype == np.float32 - assert checked_box.flags['C_CONTIGUOUS'] + assert checked_box.flags["C_CONTIGUOUS"] def test_check_box_None(self): with pytest.raises(ValueError, match="Box is None"): util.check_box(None) - @pytest.mark.parametrize('box', - ([1, 1, 2, 45, 90, 90], - (1, 1, 2, 45, 90, 90), - ['1', '1', 2, 45, '90', '90'], - ('1', '1', 2, 45, '90', '90'), - np.array(['1', '1', 2, 45, '90', '90']), - np.array([1, 1, 2, 45, 90, 90], - dtype=np.float32), - np.array([1, 1, 2, 45, 90, 90], - dtype=np.float64), - np.array([1, 1, 1, 1, 2, 2, - 45, 45, 90, 90, 90, 90], - dtype=np.float32)[::2])) + @pytest.mark.parametrize( + "box", + ( + [1, 1, 2, 45, 90, 90], + (1, 1, 2, 45, 90, 90), + ["1", "1", 2, 45, "90", "90"], + ("1", "1", 2, 45, "90", "90"), + np.array(["1", "1", 2, 45, "90", "90"]), + np.array([1, 1, 2, 45, 90, 90], dtype=np.float32), + np.array([1, 1, 2, 45, 90, 90], dtype=np.float64), + np.array( + [1, 1, 1, 1, 2, 2, 45, 45, 90, 90, 90, 90], dtype=np.float32 + )[::2], + ), + ) def test_check_box_tri_vecs(self, box): boxtype, checked_box = util.check_box(box) - assert boxtype == 'tri_vecs' + assert boxtype == "tri_vecs" assert_almost_equal(checked_box, self.ref_tri_vecs, self.prec) assert checked_box.dtype == np.float32 - assert checked_box.flags['C_CONTIGUOUS'] + assert checked_box.flags["C_CONTIGUOUS"] def test_check_box_wrong_data(self): with pytest.raises(ValueError): - wrongbox = ['invalid', 1, 1, 90, 90, 90] + wrongbox = ["invalid", 1, 1, 90, 90, 90] boxtype, checked_box = util.check_box(wrongbox) def test_check_box_wrong_shape(self): @@ -2174,6 +2528,7 @@ class StoredClass: """ A simple class that takes positional and keyword arguments of various types """ + @store_init_arguments def __init__(self, a, b, /, *args, c="foo", d="bar", e="foobar", **kwargs): self.a = a @@ -2186,22 +2541,21 @@ def __init__(self, a, b, /, *args, c="foo", d="bar", e="foobar", **kwargs): def copy(self): kwargs = copy.deepcopy(self._kwargs) - args = kwargs.pop('args', tuple()) - new = self.__class__(kwargs.pop('a'), kwargs.pop('b'), - *args, **kwargs) + args = kwargs.pop("args", tuple()) + new = self.__class__(kwargs.pop("a"), kwargs.pop("b"), *args, **kwargs) return new class TestStoreInitArguments: def test_store_arguments_default(self): - store = StoredClass('parsnips', ['roast']) - assert store.a == store._kwargs['a'] == 'parsnips' - assert store.b is store._kwargs['b'] == ['roast'] - assert store._kwargs['c'] == 'foo' - assert store._kwargs['d'] == 'bar' - assert store._kwargs['e'] == 'foobar' - assert 'args' not in store._kwargs.keys() - assert 'kwargs' not in store._kwargs.keys() + store = StoredClass("parsnips", ["roast"]) + assert store.a == store._kwargs["a"] == "parsnips" + assert store.b is store._kwargs["b"] == ["roast"] + assert store._kwargs["c"] == "foo" + assert store._kwargs["d"] == "bar" + assert store._kwargs["e"] == "foobar" + assert "args" not in store._kwargs.keys() + assert "kwargs" not in store._kwargs.keys() assert store.args is () store2 = store.copy() @@ -2209,29 +2563,39 @@ def test_store_arguments_default(self): assert store2.__dict__["b"] is not store.__dict__["b"] def test_store_arguments_withkwargs(self): - store = StoredClass('parsnips', 'roast', 'honey', 'glaze', c='richard', - d='has', e='a', f='recipe', g='allegedly') - assert store.a == store._kwargs['a'] == "parsnips" - assert store.b == store._kwargs['b'] == "roast" - assert store.c == store._kwargs['c'] == "richard" - assert store.d == store._kwargs['d'] == "has" - assert store.e == store._kwargs['e'] == "a" - assert store.kwargs['f'] == store._kwargs['f'] == "recipe" - assert store.kwargs['g'] == store._kwargs['g'] == "allegedly" - assert store.args[0] == store._kwargs['args'][0] == "honey" - assert store.args[1] == store._kwargs['args'][1] == "glaze" + store = StoredClass( + "parsnips", + "roast", + "honey", + "glaze", + c="richard", + d="has", + e="a", + f="recipe", + g="allegedly", + ) + assert store.a == store._kwargs["a"] == "parsnips" + assert store.b == store._kwargs["b"] == "roast" + assert store.c == store._kwargs["c"] == "richard" + assert store.d == store._kwargs["d"] == "has" + assert store.e == store._kwargs["e"] == "a" + assert store.kwargs["f"] == store._kwargs["f"] == "recipe" + assert store.kwargs["g"] == store._kwargs["g"] == "allegedly" + assert store.args[0] == store._kwargs["args"][0] == "honey" + assert store.args[1] == store._kwargs["args"][1] == "glaze" store2 = store.copy() assert store2.__dict__ == store.__dict__ -@pytest.mark.xfail(os.name == 'nt', - reason="util.which does not get right binary on Windows") +@pytest.mark.xfail( + os.name == "nt", reason="util.which does not get right binary on Windows" +) def test_which(): wmsg = "This method is deprecated" with pytest.warns(DeprecationWarning, match=wmsg): - assert util.which('python') == shutil.which('python') + assert util.which("python") == shutil.which("python") @pytest.mark.parametrize( diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index b53e8782e10..359430e131e 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -160,6 +160,7 @@ include = ''' ( setup\.py | MDAnalysisTests/auxiliary/.*\.py +| MDAnalysisTests/lib/.*\.py | MDAnalysisTests/transformations/.*\.py ) ''' From 83f702fbc951740cfd464c916c1a45c013518315 Mon Sep 17 00:00:00 2001 From: Jia-Xin Zhu <53895049+ChiahsinChu@users.noreply.github.com> Date: Sat, 7 Dec 2024 05:59:45 +0800 Subject: [PATCH 49/57] Allow use-defined precision in `XYZWriter` (#4771) Co-authored-by: Oliver Beckstein --- package/AUTHORS | 1 + package/CHANGELOG | 3 ++- package/MDAnalysis/coordinates/XYZ.py | 22 +++++++++++++++---- .../MDAnalysisTests/coordinates/test_xyz.py | 11 ++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/package/AUTHORS b/package/AUTHORS index 5871ec8f74f..1917e6d7059 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -244,6 +244,7 @@ Chronological list of authors - Fabian Zills - Laksh Krishna Sharma - Matthew Davies + - Jia-Xin Zhu External code diff --git a/package/CHANGELOG b/package/CHANGELOG index 5bce69eaec0..7f78ac7982c 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,13 +14,14 @@ The rules for this file: ------------------------------------------------------------------------------- -??/??/?? IAlibay +??/??/?? IAlibay, ChiahsinChu * 2.9.0 Fixes Enhancements + * Added `precision` for XYZWriter (Issue #4775, PR #4771) Changes diff --git a/package/MDAnalysis/coordinates/XYZ.py b/package/MDAnalysis/coordinates/XYZ.py index c4d0a695c4c..946792ab55d 100644 --- a/package/MDAnalysis/coordinates/XYZ.py +++ b/package/MDAnalysis/coordinates/XYZ.py @@ -140,8 +140,15 @@ class XYZWriter(base.WriterBase): # these are assumed! units = {'time': 'ps', 'length': 'Angstrom'} - def __init__(self, filename, n_atoms=None, convert_units=True, - remark=None, **kwargs): + def __init__( + self, + filename, + n_atoms=None, + convert_units=True, + remark=None, + precision=5, + **kwargs, + ): """Initialize the XYZ trajectory writer Parameters @@ -161,6 +168,10 @@ def __init__(self, filename, n_atoms=None, convert_units=True, remark: str (optional) single line of text ("molecule name"). By default writes MDAnalysis version and frame + precision: int (optional) + set precision of saved trjactory to this number of decimal places. + + .. versionadded:: 2.9.0 .. versionchanged:: 1.0.0 @@ -175,6 +186,7 @@ def __init__(self, filename, n_atoms=None, convert_units=True, self.remark = remark self.n_atoms = n_atoms self.convert_units = convert_units + self.precision = precision # can also be gz, bz2 self._xyz = util.anyopen(self.filename, 'wt') @@ -296,8 +308,10 @@ def _write_next_frame(self, ts=None): # Write content for atom, (x, y, z) in zip(self.atomnames, coordinates): - self._xyz.write("{0!s:>8} {1:10.5f} {2:10.5f} {3:10.5f}\n" - "".format(atom, x, y, z)) + self._xyz.write( + "{0!s:>8} {1:10.{p}f} {2:10.{p}f} {3:10.{p}f}\n" + "".format(atom, x, y, z, p=self.precision) + ) class XYZReader(base.ReaderBase): diff --git a/testsuite/MDAnalysisTests/coordinates/test_xyz.py b/testsuite/MDAnalysisTests/coordinates/test_xyz.py index 6612746f1c9..ac68e4312cf 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_xyz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_xyz.py @@ -122,6 +122,17 @@ def test_remark(self, universe, remarkout, remarkin, ref, tmpdir): assert lines[1].strip() == remarkin + def test_precision(self, universe, tmpdir): + outfile = "write-precision.xyz" + precision = 10 + + with tmpdir.as_cwd(): + universe.atoms.write(outfile, precision=precision) + with open(outfile, "r") as xyzout: + lines = xyzout.readlines() + # check that the precision is set correctly + assert len(lines[2].split()[1].split(".")[1]) == precision + class XYZ_BZ_Reference(XYZReference): def __init__(self): From a27591c073657c427bc7b2d25202bb531d04d8bf Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Mon, 9 Dec 2024 09:25:59 +0100 Subject: [PATCH 50/57] Check for enpty coordinates in RDKit converter (#4824) * prevents assignment of coordinates if all positions are zero --- package/CHANGELOG | 5 +++-- package/MDAnalysis/converters/RDKit.py | 9 ++++++--- testsuite/MDAnalysisTests/converters/test_rdkit.py | 14 +++++++++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 7f78ac7982c..83bff740024 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,14 +14,15 @@ The rules for this file: ------------------------------------------------------------------------------- -??/??/?? IAlibay, ChiahsinChu +??/??/?? IAlibay, ChiahsinChu, RMeli * 2.9.0 Fixes Enhancements - * Added `precision` for XYZWriter (Issue #4775, PR #4771) + * Add check and warning for empty (all zero) coordinates in RDKit converter (PR #4824) + * Added `precision` for XYZWriter (Issue #4775, PR #4771) Changes diff --git a/package/MDAnalysis/converters/RDKit.py b/package/MDAnalysis/converters/RDKit.py index 85f55b7900d..aa78a7ea0c8 100644 --- a/package/MDAnalysis/converters/RDKit.py +++ b/package/MDAnalysis/converters/RDKit.py @@ -363,9 +363,12 @@ def convert(self, obj, cache=True, NoImplicit=True, max_iter=200, # add a conformer for the current Timestep if hasattr(ag, "positions"): - if np.isnan(ag.positions).any(): - warnings.warn("NaN detected in coordinates, the output " - "molecule will not have 3D coordinates assigned") + if np.isnan(ag.positions).any() or np.allclose( + ag.positions, 0.0, rtol=0.0, atol=1e-12 + ): + warnings.warn("NaN or empty coordinates detected in coordinates, " + "the output molecule will not have 3D coordinates " + "assigned") else: # assign coordinates conf = Chem.Conformer(mol.GetNumAtoms()) diff --git a/testsuite/MDAnalysisTests/converters/test_rdkit.py b/testsuite/MDAnalysisTests/converters/test_rdkit.py index 16793a44848..1d56c4c5f62 100644 --- a/testsuite/MDAnalysisTests/converters/test_rdkit.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit.py @@ -331,7 +331,7 @@ def test_nan_coords(self): xyz = u.atoms.positions xyz[0][2] = np.nan u.atoms.positions = xyz - with pytest.warns(UserWarning, match="NaN detected"): + with pytest.warns(UserWarning, match="NaN .* detected"): mol = u.atoms.convert_to("RDKIT") with pytest.raises(ValueError, match="Bad Conformer Id"): mol.GetConformer() @@ -692,6 +692,18 @@ def test_reorder_atoms(self, smi): expected = [a.GetSymbol() for a in mol.GetAtoms()] assert values == expected + @pytest.mark.parametrize("smi", [ + "O=S(C)(C)=NC", + ]) + def test_warn_empty_coords(self, smi): + mol = Chem.MolFromSmiles(smi) + mol = Chem.AddHs(mol) + # remove bond order and charges info + pdb = Chem.MolToPDBBlock(mol) + u = mda.Universe(StringIO(pdb), format="PDB") + with pytest.warns(match="NaN or empty coordinates detected"): + u.atoms.convert_to.rdkit() + def test_pdb_names(self): u = mda.Universe(PDB_helix) mol = u.atoms.convert_to.rdkit() From f4e0f0b695419eab7ae169310069e91579667d58 Mon Sep 17 00:00:00 2001 From: Tanish Yelgoe <143334319+tanishy7777@users.noreply.github.com> Date: Fri, 13 Dec 2024 21:24:46 +0530 Subject: [PATCH 51/57] Fixes deprecation warning in nuclinfo.phase_cp() (#4831) * fix #4339 * Fixes deprecation warning for use of math.atan2 in nuclinfo.phase_cp() (Changed the function atan2() to np.atan2() to avoid converting array to scalar) * Replaced all atan2 with np.arctan2 in package/MDAnalysis/analysis/nuclinfo.py * Updated AUTHORS and CHANGELOG --- package/AUTHORS | 1 + package/CHANGELOG | 3 ++- package/MDAnalysis/analysis/nuclinfo.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package/AUTHORS b/package/AUTHORS index 1917e6d7059..719eb7b70ce 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -245,6 +245,7 @@ Chronological list of authors - Laksh Krishna Sharma - Matthew Davies - Jia-Xin Zhu + - Tanish Yelgoe External code diff --git a/package/CHANGELOG b/package/CHANGELOG index 83bff740024..6af9f6a13ce 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,11 +14,12 @@ The rules for this file: ------------------------------------------------------------------------------- -??/??/?? IAlibay, ChiahsinChu, RMeli +??/??/?? IAlibay, ChiahsinChu, RMeli, tanishy7777 * 2.9.0 Fixes + * Fixes deprecation warning Array to scalar convertion. Replaced atan2() with np.arctan2() (Issue #4339) Enhancements * Add check and warning for empty (all zero) coordinates in RDKit converter (PR #4824) diff --git a/package/MDAnalysis/analysis/nuclinfo.py b/package/MDAnalysis/analysis/nuclinfo.py index 0a8a3f6aa48..39be7191d8a 100644 --- a/package/MDAnalysis/analysis/nuclinfo.py +++ b/package/MDAnalysis/analysis/nuclinfo.py @@ -93,7 +93,7 @@ """ import numpy as np -from math import pi, sin, cos, atan2, sqrt, pow +from math import pi, sin, cos, sqrt, pow from MDAnalysis.lib import mdamath @@ -299,7 +299,7 @@ def phase_cp(universe, seg, i): + (r3_d * cos(4 * pi * 2.0 / 5.0)) + (r4_d * cos(4 * pi * 3.0 / 5.0)) + (r5_d * cos(4 * pi * 4.0 / 5.0))) * sqrt(2.0 / 5.0) - phase_ang = (atan2(D, C) + (pi / 2.)) * 180. / pi + phase_ang = (np.arctan2(D, C) + (pi / 2.)) * 180. / pi return phase_ang % 360 @@ -368,7 +368,7 @@ def phase_as(universe, seg, i): + (data4 * cos(2 * 2 * pi * (4 - 1.) / 5.)) + (data5 * cos(2 * 2 * pi * (5 - 1.) / 5.))) * 2. / 5. - phase_ang = atan2(B, A) * 180. / pi + phase_ang = np.arctan2(B, A) * 180. / pi return phase_ang % 360 From 0eddf3b073c3ddbe726027e0db993e20b7b71c73 Mon Sep 17 00:00:00 2001 From: Tanish Yelgoe <143334319+tanishy7777@users.noreply.github.com> Date: Sat, 14 Dec 2024 00:06:30 +0530 Subject: [PATCH 52/57] Removing mutable data structures and function calls as default arguments in the entire codebase (#4834) --- package/CHANGELOG | 2 + package/MDAnalysis/analysis/align.py | 5 +- package/MDAnalysis/analysis/base.py | 4 +- .../analysis/encore/clustering/cluster.py | 19 +-- .../reduce_dimensionality.py | 19 +-- .../MDAnalysis/analysis/encore/similarity.py | 126 ++++++++++-------- package/MDAnalysis/analysis/helix_analysis.py | 19 ++- package/MDAnalysis/core/universe.py | 15 ++- package/MDAnalysis/topology/ITPParser.py | 4 +- 9 files changed, 131 insertions(+), 82 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 6af9f6a13ce..15fe7cd10b6 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -20,6 +20,8 @@ The rules for this file: Fixes * Fixes deprecation warning Array to scalar convertion. Replaced atan2() with np.arctan2() (Issue #4339) + * Replaced mutable defaults with None and initialized them within + the function to prevent shared state. (Issue #4655) Enhancements * Add check and warning for empty (all zero) coordinates in RDKit converter (PR #4824) diff --git a/package/MDAnalysis/analysis/align.py b/package/MDAnalysis/analysis/align.py index fd7f15a8226..acf87dbf653 100644 --- a/package/MDAnalysis/analysis/align.py +++ b/package/MDAnalysis/analysis/align.py @@ -1592,9 +1592,12 @@ def get_matching_atoms(ag1, ag2, tol_mass=0.1, strict=False, match_atoms=True): rsize_mismatches = np.absolute(rsize1 - rsize2) mismatch_mask = (rsize_mismatches > 0) if np.any(mismatch_mask): - def get_atoms_byres(g, match_mask=np.logical_not(mismatch_mask)): + + def get_atoms_byres(g, match_mask=None): # not pretty... but need to do things on a per-atom basis in # order to preserve original selection + if match_mask is None: + match_mask = np.logical_not(mismatch_mask) ag = g.atoms good = ag.residues.resids[match_mask] # resid for each residue resids = ag.resids # resid for each atom diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index f940af58e82..4e7f58dc0bd 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -479,7 +479,7 @@ def _conclude(self): def _compute(self, indexed_frames: np.ndarray, verbose: bool = None, - *, progressbar_kwargs={}) -> "AnalysisBase": + *, progressbar_kwargs=None) -> "AnalysisBase": """Perform the calculation on a balanced slice of frames that have been setup prior to that using _setup_computation_groups() @@ -500,6 +500,8 @@ def _compute(self, indexed_frames: np.ndarray, .. versionadded:: 2.8.0 """ + if progressbar_kwargs is None: + progressbar_kwargs = {} logger.info("Choosing frames to analyze") # if verbose unchanged, use class default verbose = getattr(self, "_verbose", False) if verbose is None else verbose diff --git a/package/MDAnalysis/analysis/encore/clustering/cluster.py b/package/MDAnalysis/analysis/encore/clustering/cluster.py index 1c43f2cfd75..3bffa490236 100644 --- a/package/MDAnalysis/analysis/encore/clustering/cluster.py +++ b/package/MDAnalysis/analysis/encore/clustering/cluster.py @@ -44,13 +44,15 @@ from . import ClusteringMethod -def cluster(ensembles, - method = ClusteringMethod.AffinityPropagationNative(), - select="name CA", - distance_matrix=None, - allow_collapsed_result=True, - ncores=1, - **kwargs): +def cluster( + ensembles, + method=None, + select="name CA", + distance_matrix=None, + allow_collapsed_result=True, + ncores=1, + **kwargs, +): """Cluster frames from one or more ensembles, using one or more clustering methods. The function optionally takes pre-calculated distances matrices as an argument. Note that not all clustering procedure can work @@ -154,7 +156,8 @@ def cluster(ensembles, [array([1, 1, 1, 1, 2]), array([1, 1, 1, 1, 1])] """ - + if method is None: + method = ClusteringMethod.AffinityPropagationNative() # Internally, ensembles are always transformed to a list of lists if ensembles is not None: if not hasattr(ensembles, '__iter__'): diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py index 281d681203f..82e805c91bf 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py @@ -44,13 +44,15 @@ StochasticProximityEmbeddingNative) -def reduce_dimensionality(ensembles, - method=StochasticProximityEmbeddingNative(), - select="name CA", - distance_matrix=None, - allow_collapsed_result=True, - ncores=1, - **kwargs): +def reduce_dimensionality( + ensembles, + method=None, + select="name CA", + distance_matrix=None, + allow_collapsed_result=True, + ncores=1, + **kwargs, +): """ Reduce dimensions in frames from one or more ensembles, using one or more dimensionality reduction methods. The function optionally takes @@ -152,7 +154,8 @@ def reduce_dimensionality(ensembles, encore.StochasticProximityEmbeddingNative(dimension=2)]) """ - + if method is None: + method = StochasticProximityEmbeddingNative() if ensembles is not None: if not hasattr(ensembles, '__iter__'): ensembles = [ensembles] diff --git a/package/MDAnalysis/analysis/encore/similarity.py b/package/MDAnalysis/analysis/encore/similarity.py index 4fe6f0e35a5..c9a8ff1486c 100644 --- a/package/MDAnalysis/analysis/encore/similarity.py +++ b/package/MDAnalysis/analysis/encore/similarity.py @@ -952,20 +952,17 @@ def hes(ensembles, return values, details -def ces(ensembles, - select="name CA", - clustering_method=AffinityPropagationNative( - preference=-1.0, - max_iter=500, - convergence_iter=50, - damping=0.9, - add_noise=True), - distance_matrix=None, - estimate_error=False, - bootstrapping_samples=10, - ncores=1, - calc_diagonal=False, - allow_collapsed_result=True): +def ces( + ensembles, + select="name CA", + clustering_method=None, + distance_matrix=None, + estimate_error=False, + bootstrapping_samples=10, + ncores=1, + calc_diagonal=False, + allow_collapsed_result=True, +): """ Calculates the Clustering Ensemble Similarity (CES) between ensembles @@ -1084,6 +1081,14 @@ def ces(ensembles, [0.25331629 0. ]] """ + if clustering_method is None: + clustering_method = AffinityPropagationNative( + preference=-1.0, + max_iter=500, + convergence_iter=50, + damping=0.9, + add_noise=True, + ) for ensemble in ensembles: ensemble.transfer_to_memory() @@ -1218,22 +1223,18 @@ def ces(ensembles, return values, details -def dres(ensembles, - select="name CA", - dimensionality_reduction_method = StochasticProximityEmbeddingNative( - dimension=3, - distance_cutoff = 1.5, - min_lam=0.1, - max_lam=2.0, - ncycle=100, - nstep=10000), - distance_matrix=None, - nsamples=1000, - estimate_error=False, - bootstrapping_samples=100, - ncores=1, - calc_diagonal=False, - allow_collapsed_result=True): +def dres( + ensembles, + select="name CA", + dimensionality_reduction_method=None, + distance_matrix=None, + nsamples=1000, + estimate_error=False, + bootstrapping_samples=100, + ncores=1, + calc_diagonal=False, + allow_collapsed_result=True, +): """ Calculates the Dimensional Reduction Ensemble Similarity (DRES) between @@ -1354,6 +1355,15 @@ def dres(ensembles, :mod:`MDAnalysis.analysis.encore.dimensionality_reduction.reduce_dimensionality`` """ + if dimensionality_reduction_method is None: + dimensionality_reduction_method = StochasticProximityEmbeddingNative( + dimension=3, + distance_cutoff=1.5, + min_lam=0.1, + max_lam=2.0, + ncycle=100, + nstep=10000, + ) for ensemble in ensembles: ensemble.transfer_to_memory() @@ -1484,16 +1494,13 @@ def dres(ensembles, return values, details -def ces_convergence(original_ensemble, - window_size, - select="name CA", - clustering_method=AffinityPropagationNative( - preference=-1.0, - max_iter=500, - convergence_iter=50, - damping=0.9, - add_noise=True), - ncores=1): +def ces_convergence( + original_ensemble, + window_size, + select="name CA", + clustering_method=None, + ncores=1, +): """ Use the CES to evaluate the convergence of the ensemble/trajectory. CES will be calculated between the whole trajectory contained in an @@ -1559,6 +1566,14 @@ def ces_convergence(original_ensemble, [0. ]] """ + if clustering_method is None: + clustering_method = AffinityPropagationNative( + preference=-1.0, + max_iter=500, + convergence_iter=50, + damping=0.9, + add_noise=True, + ) ensembles = prepare_ensembles_for_convergence_increasing_window( original_ensemble, window_size, select=select) @@ -1584,20 +1599,14 @@ def ces_convergence(original_ensemble, return out -def dres_convergence(original_ensemble, - window_size, - select="name CA", - dimensionality_reduction_method = \ - StochasticProximityEmbeddingNative( - dimension=3, - distance_cutoff=1.5, - min_lam=0.1, - max_lam=2.0, - ncycle=100, - nstep=10000 - ), - nsamples=1000, - ncores=1): +def dres_convergence( + original_ensemble, + window_size, + select="name CA", + dimensionality_reduction_method=None, + nsamples=1000, + ncores=1, +): """ Use the DRES to evaluate the convergence of the ensemble/trajectory. DRES will be calculated between the whole trajectory contained in an @@ -1660,6 +1669,15 @@ def dres_convergence(original_ensemble, much the trajectory keeps on resampling the same ares of the conformational space, and therefore of convergence. """ + if dimensionality_reduction_method is None: + dimensionality_reduction_method = StochasticProximityEmbeddingNative( + dimension=3, + distance_cutoff=1.5, + min_lam=0.1, + max_lam=2.0, + ncycle=100, + nstep=10000, + ) ensembles = prepare_ensembles_for_convergence_increasing_window( original_ensemble, window_size, select=select) diff --git a/package/MDAnalysis/analysis/helix_analysis.py b/package/MDAnalysis/analysis/helix_analysis.py index 9c287fb7508..1b1bdf9ce3f 100644 --- a/package/MDAnalysis/analysis/helix_analysis.py +++ b/package/MDAnalysis/analysis/helix_analysis.py @@ -186,7 +186,7 @@ def local_screw_angles(global_axis, ref_axis, helix_directions): return np.rad2deg(to_ortho) -def helix_analysis(positions, ref_axis=[0, 0, 1]): +def helix_analysis(positions, ref_axis=(0, 0, 1)): r""" Calculate helix properties from atomic coordinates. @@ -387,11 +387,18 @@ class HELANAL(AnalysisBase): 'local_screw_angles': (-2,), } - def __init__(self, universe, select='name CA', ref_axis=[0, 0, 1], - verbose=False, flatten_single_helix=True, - split_residue_sequences=True): - super(HELANAL, self).__init__(universe.universe.trajectory, - verbose=verbose) + def __init__( + self, + universe, + select="name CA", + ref_axis=(0, 0, 1), + verbose=False, + flatten_single_helix=True, + split_residue_sequences=True, + ): + super(HELANAL, self).__init__( + universe.universe.trajectory, verbose=verbose + ) selections = util.asiterable(select) atomgroups = [universe.select_atoms(s) for s in selections] consecutive = [] diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index 7fed2cde8c6..504d07c02c3 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -1474,9 +1474,16 @@ def _fragdict(self): return fragdict @classmethod - def from_smiles(cls, smiles, sanitize=True, addHs=True, - generate_coordinates=True, numConfs=1, - rdkit_kwargs={}, **kwargs): + def from_smiles( + cls, + smiles, + sanitize=True, + addHs=True, + generate_coordinates=True, + numConfs=1, + rdkit_kwargs=None, + **kwargs, + ): """Create a Universe from a SMILES string with rdkit Parameters @@ -1530,6 +1537,8 @@ def from_smiles(cls, smiles, sanitize=True, addHs=True, .. versionadded:: 2.0.0 """ + if rdkit_kwargs is None: + rdkit_kwargs = {} try: from rdkit import Chem from rdkit.Chem import AllChem diff --git a/package/MDAnalysis/topology/ITPParser.py b/package/MDAnalysis/topology/ITPParser.py index 83b43711c8a..9968f854df7 100644 --- a/package/MDAnalysis/topology/ITPParser.py +++ b/package/MDAnalysis/topology/ITPParser.py @@ -429,8 +429,10 @@ def shift_indices(self, atomid=0, resid=0, molnum=0, cgnr=0, n_res=0, n_atoms=0) return atom_order, new_params, molnums, self.moltypes, residx - def add_param(self, line, container, n_funct=2, funct_values=[]): + def add_param(self, line, container, n_funct=2, funct_values=None): """Add defined GROMACS directive lines, only if the funct is in ``funct_values``""" + if funct_values is None: + funct_values = [] values = line.split() funct = int(values[n_funct]) if funct in funct_values: From fa734782a94d9d2f6a56143398db78cd1cd6aa95 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Fri, 13 Dec 2024 13:59:53 -0700 Subject: [PATCH 53/57] [CI] switched from macOS-12 to macOS-13 GH images (#4836) fix #4835 --- .github/workflows/deploy.yaml | 2 +- .github/workflows/gh-ci-cron.yaml | 2 +- .github/workflows/gh-ci.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 7171ff3e82b..898ad0b0ac5 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -38,7 +38,7 @@ jobs: matrix: buildplat: - [ubuntu-22.04, manylinux_x86_64, x86_64] - - [macos-12, macosx_*, x86_64] + - [macos-13, macosx_*, x86_64] - [windows-2019, win_amd64, AMD64] - [macos-14, macosx_*, arm64] python: ["cp310", "cp311", "cp312", "cp313"] diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 34aadb7c941..230a99dbb73 100644 --- a/.github/workflows/gh-ci-cron.yaml +++ b/.github/workflows/gh-ci-cron.yaml @@ -146,7 +146,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, macos-12] + os: [ubuntu-20.04, macos-13] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index d389356450e..999a974ff9d 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -38,7 +38,7 @@ jobs: full-deps: false codecov: true - name: macOS_monterey_py311 - os: macOS-12 + os: macos-13 python-version: "3.12" full-deps: true codecov: true From c6bfa09dea0b94df6d0ed79c59fc19e3b16a0ea2 Mon Sep 17 00:00:00 2001 From: Tanish Yelgoe <143334319+tanishy7777@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:54:17 +0530 Subject: [PATCH 54/57] Fixes MDAnalysis.analysis.density.convert_density() method has an invalid default unit value (#4833) --- package/CHANGELOG | 1 + package/MDAnalysis/analysis/density.py | 2 +- testsuite/MDAnalysisTests/analysis/test_density.py | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 15fe7cd10b6..6fb34a869ca 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -19,6 +19,7 @@ The rules for this file: * 2.9.0 Fixes + * Fixes invalid default unit from Angstrom to Angstrom^{-3} for convert_density() function. (Issue #4829) * Fixes deprecation warning Array to scalar convertion. Replaced atan2() with np.arctan2() (Issue #4339) * Replaced mutable defaults with None and initialized them within the function to prevent shared state. (Issue #4655) diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index 8f3f0b33647..f5c270e7dcc 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -826,7 +826,7 @@ def convert_length(self, unit='Angstrom'): self.units['length'] = unit self._update() # needed to recalculate midpoints and origin - def convert_density(self, unit='Angstrom'): + def convert_density(self, unit='Angstrom^{-3}'): """Convert the density to the physical units given by `unit`. Parameters diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index b00a8234c17..c99b671e3db 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -384,6 +384,12 @@ def test_warn_results_deprecated(self, universe): with pytest.warns(DeprecationWarning, match=wmsg): assert_equal(D.density.grid, D.results.density.grid) + def test_density_analysis_conversion_default_unit(self): + u = mda.Universe(TPR, XTC) + ow = u.select_atoms("name OW") + D = mda.analysis.density.DensityAnalysis(ow, delta=1.0) + D.run() + D.results.density.convert_density() class TestGridImport(object): From 7f686ca0daa8441f0ce144e956c719b972294989 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Sun, 15 Dec 2024 00:06:04 +0100 Subject: [PATCH 55/57] 'MDAnalysis.analysis.nucleicacids' parallelization (#4727) - Fixes #4670 - Parallelization of the backend support to the class `NucPairDist` in nucleicacids.py - Addition of parallelization tests in test_nucleicacids.py and fixtures in conftest.py - Updated Changelog --- package/CHANGELOG | 11 ++++---- package/MDAnalysis/analysis/nucleicacids.py | 26 ++++++++++++++----- .../MDAnalysisTests/analysis/conftest.py | 8 ++++++ .../analysis/test_nucleicacids.py | 12 ++++----- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 6fb34a869ca..5a1510d9ce2 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,7 +14,7 @@ The rules for this file: ------------------------------------------------------------------------------- -??/??/?? IAlibay, ChiahsinChu, RMeli, tanishy7777 +??/??/?? IAlibay, ChiahsinChu, RMeli, tanishy7777, talagayev * 2.9.0 @@ -25,6 +25,7 @@ Fixes the function to prevent shared state. (Issue #4655) Enhancements + * Enable parallelization for analysis.nucleicacids.NucPairDist (Issue #4670) * Add check and warning for empty (all zero) coordinates in RDKit converter (PR #4824) * Added `precision` for XYZWriter (Issue #4775, PR #4771) @@ -98,11 +99,11 @@ 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) - * enables parallelization for analysis.bat.BAT (Issue #4663) - * enable parallelization for analysis.dihedrals.{Dihedral,Ramachandran,Janin} + * Explicitly mark `analysis.pca.PCA` as not parallelizable (Issue #4680) + * Enables parallelization for analysis.bat.BAT (Issue #4663) + * Enable parallelization for analysis.dihedrals.{Dihedral,Ramachandran,Janin} (Issue #4673) - * enables parallelization for analysis.dssp.dssp.DSSP (Issue #4674) + * Enables parallelization for analysis.dssp.dssp.DSSP (Issue #4674) * Enables parallelization for analysis.hydrogenbonds.hbond_analysis.HydrogenBondAnalysis (Issue #4664) * 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) diff --git a/package/MDAnalysis/analysis/nucleicacids.py b/package/MDAnalysis/analysis/nucleicacids.py index 9bdbe8d1124..b0f5013e799 100644 --- a/package/MDAnalysis/analysis/nucleicacids.py +++ b/package/MDAnalysis/analysis/nucleicacids.py @@ -70,7 +70,7 @@ import MDAnalysis as mda from .distances import calc_bonds -from .base import AnalysisBase, Results +from .base import AnalysisBase, ResultsGroup from MDAnalysis.core.groups import Residue, ResidueGroup @@ -159,13 +159,23 @@ class NucPairDist(AnalysisBase): .. versionchanged:: 2.7.0 Added static method :attr:`select_strand_atoms` as a helper for selecting atom pairs for distance analysis. + + .. versionchanged:: 2.9.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') + _s1: mda.AtomGroup _s2: mda.AtomGroup _n_sel: int - _res_dict: Dict[int, List[float]] - + def __init__(self, selection1: List[mda.AtomGroup], selection2: List[mda.AtomGroup], **kwargs) -> None: @@ -276,7 +286,7 @@ def select_strand_atoms( return (sel1, sel2) def _prepare(self) -> None: - self._res_array: np.ndarray = np.zeros( + self.results.distances: np.ndarray = np.zeros( [self.n_frames, self._n_sel] ) @@ -285,13 +295,17 @@ def _single_frame(self) -> None: self._s1.positions, self._s2.positions ) - self._res_array[self._frame_index, :] = dist + self.results.distances[self._frame_index, :] = dist def _conclude(self) -> None: - self.results['distances'] = self._res_array self.results['pair_distances'] = self.results['distances'] # TODO: remove pair_distances in 3.0.0 + def _get_aggregator(self): + return ResultsGroup(lookup={ + 'distances': ResultsGroup.ndarray_vstack, + } + ) class WatsonCrickDist(NucPairDist): r""" diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index fc3c8a480c7..a60b565f1c6 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -14,6 +14,7 @@ from MDAnalysis.analysis.hydrogenbonds.hbond_analysis import ( HydrogenBondAnalysis, ) +from MDAnalysis.analysis.nucleicacids import NucPairDist from MDAnalysis.lib.util import is_installed @@ -141,3 +142,10 @@ def client_DSSP(request): @pytest.fixture(scope='module', params=params_for_cls(HydrogenBondAnalysis)) def client_HydrogenBondAnalysis(request): return request.param + + +# MDAnalysis.analysis.nucleicacids + +@pytest.fixture(scope="module", params=params_for_cls(NucPairDist)) +def client_NucPairDist(request): + return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py b/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py index 5f90c3b0c1d..ce2ae5e4864 100644 --- a/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py +++ b/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py @@ -55,12 +55,12 @@ def test_empty_ag_error(strand): @pytest.fixture(scope='module') -def wc_rna(strand): +def wc_rna(strand, client_NucPairDist): strand1 = ResidueGroup([strand.residues[0], strand.residues[21]]) strand2 = ResidueGroup([strand.residues[1], strand.residues[22]]) WC = WatsonCrickDist(strand1, strand2) - WC.run() + WC.run(**client_NucPairDist) return WC @@ -114,23 +114,23 @@ def test_wc_dis_results_keyerrs(wc_rna, key): wc_rna.results[key] -def test_minor_dist(strand): +def test_minor_dist(strand, client_NucPairDist): strand1 = ResidueGroup([strand.residues[2], strand.residues[19]]) strand2 = ResidueGroup([strand.residues[16], strand.residues[4]]) MI = MinorPairDist(strand1, strand2) - MI.run() + MI.run(**client_NucPairDist) assert MI.results.distances[0, 0] == approx(15.06506, rel=1e-3) assert MI.results.distances[0, 1] == approx(3.219116, rel=1e-3) -def test_major_dist(strand): +def test_major_dist(strand, client_NucPairDist): strand1 = ResidueGroup([strand.residues[1], strand.residues[4]]) strand2 = ResidueGroup([strand.residues[11], strand.residues[8]]) MA = MajorPairDist(strand1, strand2) - MA.run() + MA.run(**client_NucPairDist) assert MA.results.distances[0, 0] == approx(26.884272, rel=1e-3) assert MA.results.distances[0, 1] == approx(13.578535, rel=1e-3) From 80b28c88e2cc2956177cc6603e52363fee179e23 Mon Sep 17 00:00:00 2001 From: Tanish Yelgoe <143334319+tanishy7777@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:33:28 +0530 Subject: [PATCH 56/57] Fix #4731 (#4837) --- package/MDAnalysis/lib/distances.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package/MDAnalysis/lib/distances.py b/package/MDAnalysis/lib/distances.py index 524b9f40635..2759a0ffb32 100644 --- a/package/MDAnalysis/lib/distances.py +++ b/package/MDAnalysis/lib/distances.py @@ -401,10 +401,9 @@ def self_distance_array( for i in range(n): for j in range(i + 1, n): + dist[i, j] = dist[j, i] = d[k] k += 1 - dist[i, j] = d[k] - - + .. versionchanged:: 0.13.0 Added *backend* keyword. .. versionchanged:: 0.19.0 From a3672f216aa162f2549d1712fad0118b2cc98d49 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Tue, 17 Dec 2024 02:23:19 +0100 Subject: [PATCH 57/57] Implementation of Parallelization to `MDAnalysis.analysis.contacts` (#4820) * Fixes #4660 * summary of changes: - added backends and aggregators to Contacts in analysis.contacts - added private _get_box_func method because lambdas cannot be used for parallelization - added the client_Contacts in conftest.py - added client_Contacts in run() in test_contacts.py * Update CHANGELOG --- package/CHANGELOG | 2 + package/MDAnalysis/analysis/contacts.py | 48 ++++++++-- .../MDAnalysisTests/analysis/conftest.py | 8 ++ .../MDAnalysisTests/analysis/test_contacts.py | 87 ++++++++++++------- 4 files changed, 105 insertions(+), 40 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 5a1510d9ce2..03255eb4ad3 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -25,10 +25,12 @@ Fixes the function to prevent shared state. (Issue #4655) Enhancements + * Enables parallelization for analysis.contacts.Contacts (Issue #4660) * Enable parallelization for analysis.nucleicacids.NucPairDist (Issue #4670) * Add check and warning for empty (all zero) coordinates in RDKit converter (PR #4824) * Added `precision` for XYZWriter (Issue #4775, PR #4771) + Changes Deprecations diff --git a/package/MDAnalysis/analysis/contacts.py b/package/MDAnalysis/analysis/contacts.py index 7a7e195f09a..f29fd4961e8 100644 --- a/package/MDAnalysis/analysis/contacts.py +++ b/package/MDAnalysis/analysis/contacts.py @@ -223,7 +223,7 @@ def is_any_closer(r, r0, dist=2.5): from MDAnalysis.lib.util import openany from MDAnalysis.analysis.distances import distance_array from MDAnalysis.core.groups import AtomGroup, UpdatingAtomGroup -from .base import AnalysisBase +from .base import AnalysisBase, ResultsGroup logger = logging.getLogger("MDAnalysis.analysis.contacts") @@ -376,8 +376,22 @@ class Contacts(AnalysisBase): :class:`MDAnalysis.analysis.base.Results` instance. .. versionchanged:: 2.2.0 :class:`Contacts` accepts both AtomGroup and string for `select` + .. versionchanged:: 2.9.0 + Introduced :meth:`get_supported_backends` allowing + for parallel execution on :mod:`multiprocessing` + and :mod:`dask` backends. """ + _analysis_algorithm_is_parallelizable = True + + @classmethod + def get_supported_backends(cls): + return ( + "serial", + "multiprocessing", + "dask", + ) + def __init__(self, u, select, refgroup, method="hard_cut", radius=4.5, pbc=True, kwargs=None, **basekwargs): """ @@ -444,11 +458,8 @@ def __init__(self, u, select, refgroup, method="hard_cut", radius=4.5, self.r0 = [] self.initial_contacts = [] - #get dimension of box if pbc set to True - if self.pbc: - self._get_box = lambda ts: ts.dimensions - else: - self._get_box = lambda ts: None + # get dimensions via partial for parallelization compatibility + self._get_box = functools.partial(self._get_box_func, pbc=self.pbc) if isinstance(refgroup[0], AtomGroup): refA, refB = refgroup @@ -464,7 +475,6 @@ def __init__(self, u, select, refgroup, method="hard_cut", radius=4.5, self.n_initial_contacts = self.initial_contacts[0].sum() - @staticmethod def _get_atomgroup(u, sel): select_error_message = ("selection must be either string or a " @@ -480,6 +490,28 @@ def _get_atomgroup(u, sel): else: raise TypeError(select_error_message) + @staticmethod + def _get_box_func(ts, pbc): + """Retrieve the dimensions of the simulation box based on PBC. + + Parameters + ---------- + ts : Timestep + The current timestep of the simulation, which contains the + box dimensions. + pbc : bool + A flag indicating whether periodic boundary conditions (PBC) + are enabled. If `True`, the box dimensions are returned, + else returns `None`. + + Returns + ------- + box_dimensions : ndarray or None + The dimensions of the simulation box as a NumPy array if PBC + is True, else returns `None`. + """ + return ts.dimensions if pbc else None + def _prepare(self): self.results.timeseries = np.empty((self.n_frames, len(self.r0)+1)) @@ -506,6 +538,8 @@ def timeseries(self): warnings.warn(wmsg, DeprecationWarning) return self.results.timeseries + def _get_aggregator(self): + return ResultsGroup(lookup={'timeseries': ResultsGroup.ndarray_vstack}) def _new_selections(u_orig, selections, frame): """create stand alone AGs from selections at frame""" diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index a60b565f1c6..91e9bd760b8 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -15,6 +15,7 @@ HydrogenBondAnalysis, ) from MDAnalysis.analysis.nucleicacids import NucPairDist +from MDAnalysis.analysis.contacts import Contacts from MDAnalysis.lib.util import is_installed @@ -149,3 +150,10 @@ def client_HydrogenBondAnalysis(request): @pytest.fixture(scope="module", params=params_for_cls(NucPairDist)) def client_NucPairDist(request): return request.param + + +# MDAnalysis.analysis.contacts + +@pytest.fixture(scope="module", params=params_for_cls(Contacts)) +def client_Contacts(request): + return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_contacts.py b/testsuite/MDAnalysisTests/analysis/test_contacts.py index 85546cbc3f5..6b416e27f8e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_contacts.py +++ b/testsuite/MDAnalysisTests/analysis/test_contacts.py @@ -171,8 +171,8 @@ def universe(): return mda.Universe(PSF, DCD) def _run_Contacts( - self, universe, - start=None, stop=None, step=None, **kwargs + self, universe, client_Contacts, start=None, + stop=None, step=None, **kwargs ): acidic = universe.select_atoms(self.sel_acidic) basic = universe.select_atoms(self.sel_basic) @@ -181,7 +181,8 @@ def _run_Contacts( select=(self.sel_acidic, self.sel_basic), refgroup=(acidic, basic), radius=6.0, - **kwargs).run(start=start, stop=stop, step=step) + **kwargs + ).run(**client_Contacts, start=start, stop=stop, step=step) @pytest.mark.parametrize("seltxt", [sel_acidic, sel_basic]) def test_select_valid_types(self, universe, seltxt): @@ -195,7 +196,7 @@ def test_select_valid_types(self, universe, seltxt): assert ag_from_string == ag_from_ag - def test_contacts_selections(self, universe): + def test_contacts_selections(self, universe, client_Contacts): """Test if Contacts can take both string and AtomGroup as selections. """ aga = universe.select_atoms(self.sel_acidic) @@ -210,8 +211,8 @@ def test_contacts_selections(self, universe): refgroup=(aga, agb) ) - cag.run() - csel.run() + cag.run(**client_Contacts) + csel.run(**client_Contacts) assert cag.grA == csel.grA assert cag.grB == csel.grB @@ -228,26 +229,31 @@ def test_select_wrong_types(self, universe, ag): ) as te: contacts.Contacts._get_atomgroup(universe, ag) - def test_startframe(self, universe): + def test_startframe(self, universe, client_Contacts): """test_startframe: TestContactAnalysis1: start frame set to 0 (resolution of Issue #624) """ - CA1 = self._run_Contacts(universe) + CA1 = self._run_Contacts(universe, client_Contacts=client_Contacts) assert len(CA1.results.timeseries) == universe.trajectory.n_frames - def test_end_zero(self, universe): + def test_end_zero(self, universe, client_Contacts): """test_end_zero: TestContactAnalysis1: stop frame 0 is not ignored""" - CA1 = self._run_Contacts(universe, stop=0) + CA1 = self._run_Contacts( + universe, client_Contacts=client_Contacts, stop=0 + ) assert len(CA1.results.timeseries) == 0 - def test_slicing(self, universe): + def test_slicing(self, universe, client_Contacts): start, stop, step = 10, 30, 5 - CA1 = self._run_Contacts(universe, start=start, stop=stop, step=step) + CA1 = self._run_Contacts( + universe, client_Contacts=client_Contacts, + start=start, stop=stop, step=step + ) frames = np.arange(universe.trajectory.n_frames)[start:stop:step] assert len(CA1.results.timeseries) == len(frames) - def test_villin_folded(self): + def test_villin_folded(self, client_Contacts): # one folded, one unfolded f = mda.Universe(contacts_villin_folded) u = mda.Universe(contacts_villin_unfolded) @@ -259,12 +265,12 @@ def test_villin_folded(self): select=(sel, sel), refgroup=(grF, grF), method="soft_cut") - q.run() + q.run(**client_Contacts) results = soft_cut(f, u, sel, sel) assert_allclose(q.results.timeseries[:, 1], results[:, 1], rtol=0, atol=1.5e-7) - def test_villin_unfolded(self): + def test_villin_unfolded(self, client_Contacts): # both folded f = mda.Universe(contacts_villin_folded) u = mda.Universe(contacts_villin_folded) @@ -276,13 +282,13 @@ def test_villin_unfolded(self): select=(sel, sel), refgroup=(grF, grF), method="soft_cut") - q.run() + q.run(**client_Contacts) results = soft_cut(f, u, sel, sel) assert_allclose(q.results.timeseries[:, 1], results[:, 1], rtol=0, atol=1.5e-7) - def test_hard_cut_method(self, universe): - ca = self._run_Contacts(universe) + def test_hard_cut_method(self, universe, client_Contacts): + ca = self._run_Contacts(universe, client_Contacts=client_Contacts) expected = [1., 0.58252427, 0.52427184, 0.55339806, 0.54368932, 0.54368932, 0.51456311, 0.46601942, 0.48543689, 0.52427184, 0.46601942, 0.58252427, 0.51456311, 0.48543689, 0.48543689, @@ -306,7 +312,7 @@ def test_hard_cut_method(self, universe): assert len(ca.results.timeseries) == len(expected) assert_allclose(ca.results.timeseries[:, 1], expected, rtol=0, atol=1.5e-7) - def test_radius_cut_method(self, universe): + def test_radius_cut_method(self, universe, client_Contacts): acidic = universe.select_atoms(self.sel_acidic) basic = universe.select_atoms(self.sel_basic) r = contacts.distance_array(acidic.positions, basic.positions) @@ -316,15 +322,20 @@ def test_radius_cut_method(self, universe): r = contacts.distance_array(acidic.positions, basic.positions) expected.append(contacts.radius_cut_q(r[initial_contacts], None, radius=6.0)) - ca = self._run_Contacts(universe, method='radius_cut') + ca = self._run_Contacts( + universe, client_Contacts=client_Contacts, method="radius_cut" + ) assert_array_equal(ca.results.timeseries[:, 1], expected) @staticmethod def _is_any_closer(r, r0, dist=2.5): return np.any(r < dist) - def test_own_method(self, universe): - ca = self._run_Contacts(universe, method=self._is_any_closer) + def test_own_method(self, universe, client_Contacts): + ca = self._run_Contacts( + universe, client_Contacts=client_Contacts, + method=self._is_any_closer + ) bound_expected = [1., 1., 0., 1., 1., 0., 0., 1., 0., 1., 1., 0., 0., 1., 0., 0., 0., 0., 1., 1., 0., 0., 0., 1., 0., 1., @@ -340,13 +351,20 @@ def test_own_method(self, universe): def _weird_own_method(r, r0): return 'aaa' - def test_own_method_no_array_cast(self, universe): + def test_own_method_no_array_cast(self, universe, client_Contacts): with pytest.raises(ValueError): - self._run_Contacts(universe, method=self._weird_own_method, stop=2) - - def test_non_callable_method(self, universe): + self._run_Contacts( + universe, + client_Contacts=client_Contacts, + method=self._weird_own_method, + stop=2, + ) + + def test_non_callable_method(self, universe, client_Contacts): with pytest.raises(ValueError): - self._run_Contacts(universe, method=2, stop=2) + self._run_Contacts( + universe, client_Contacts=client_Contacts, method=2, stop=2 + ) @pytest.mark.parametrize("pbc,expected", [ (True, [1., 0.43138152, 0.3989021, 0.43824337, 0.41948765, @@ -354,7 +372,7 @@ def test_non_callable_method(self, universe): (False, [1., 0.42327791, 0.39192399, 0.40950119, 0.40902613, 0.42470309, 0.41140143, 0.42897862, 0.41472684, 0.38574822]) ]) - def test_distance_box(self, pbc, expected): + def test_distance_box(self, pbc, expected, client_Contacts): u = mda.Universe(TPR, XTC) sel_basic = "(resname ARG LYS)" sel_acidic = "(resname ASP GLU)" @@ -363,13 +381,15 @@ def test_distance_box(self, pbc, expected): r = contacts.Contacts(u, select=(sel_acidic, sel_basic), refgroup=(acidic, basic), radius=6.0, pbc=pbc) - r.run() + r.run(**client_Contacts) assert_allclose(r.results.timeseries[:, 1], expected,rtol=0, atol=1.5e-7) - def test_warn_deprecated_attr(self, universe): + def test_warn_deprecated_attr(self, universe, client_Contacts): """Test for warning message emitted on using deprecated `timeseries` attribute""" - CA1 = self._run_Contacts(universe, stop=1) + CA1 = self._run_Contacts( + universe, client_Contacts=client_Contacts, stop=1 + ) wmsg = "The `timeseries` attribute was deprecated in MDAnalysis" with pytest.warns(DeprecationWarning, match=wmsg): assert_equal(CA1.timeseries, CA1.results.timeseries) @@ -385,10 +405,11 @@ def test_n_initial_contacts(self, datafiles, expected): r = contacts.Contacts(u, select=select, refgroup=refgroup) assert_equal(r.n_initial_contacts, expected) -def test_q1q2(): + +def test_q1q2(client_Contacts): u = mda.Universe(PSF, DCD) q1q2 = contacts.q1q2(u, 'name CA', radius=8) - q1q2.run() + q1q2.run(**client_Contacts) q1_expected = [1., 0.98092643, 0.97366031, 0.97275204, 0.97002725, 0.97275204, 0.96276113, 0.96730245, 0.9582198, 0.96185286,