From caa04ec8583fb01d0aa967592240e01159c23617 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Fri, 18 Oct 2024 19:24:04 -0700 Subject: [PATCH 01/58] 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 02/58] 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 03/58] 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 04/58] 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 05/58] 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 06/58] 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 07/58] 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 zyxym1qa7%fS+HB*!o}PkW&3x?_@(4OeSCUHuVknecO{Tk3T0jA1HX$J=2GJtt`DKU z5}9!h97+J4_g%vf-d8QReY!NX>y9QIQ9{jR1S!Hhwkc` z;)|lgo}Y(rIFqB4)`oieud)K6YZ0>1FOJfyp>5cJbZ^EP>8Dn1B1=@7#uUIqy+p3j zT%TBM)4-U<&5VBu8@@_yj~IIjZ#WYulYGDG^`>CjP5F*xgLKaGZz8Eh^Hg1U3W;Mg z^n-i0w{)Uqks0ZR)4F;ZG@2Q8*;sl4g*Vl z#+39h=Z{3G{`E{gaWqf#C6A;#U5BLwiAL*cEyeS5Ppuk=JqM_<=kg|i6}HIbfIU;I ze?!kvrOAHDECzN!A+ckb?PDyPLIMk)m1P|cikeMp7dgr;9F(Qj8onq_-5b>Qi+RIz zSn-iA*zFTa6*0}2ydLokh<_Ac;TJn(q_cPiGFq*8`^0*LsKA1H`w^xddxi65>C!u9 znQ;38ne`3b1i9HDyAK;(0`C+Rt^zmU1#N$~UNv7b>fNbDd}Yoz#mRYQu0y2-&$)TS z@!f}tJOiRZ;#c^eKr6Esv&tFUSLAa;6VBB-#9Yd7niEh@k7ys;sLhQCVDG$9JBRfF zlRw1%tWu#^e+}OAl%|blw*8#H@LkkF{`QG6^i)iCM)}|#oAkghE#t0KlTrcCufw5q zrdTG6bnTIt%g>3YOIbRed6^vtXe{UdBWgz^B^a)b=zTsfwj{ufS7>6~xXK$y|440@ zTGo-)ho+wIi@PikW4bZp6L~T7p!%th_r#bMU8F3b~x8Ft<5|+0fyqveAbCqBRpxI z?gtTjS05Ot$o8t`pU*f^en))S{Z_l4{qhbeKAkf;{}+{TZFEVT{Ow`X#dGiPuLFrO zK9eyQei4dD{O@3i&5WCU{Za@d-zHC>mEa>H+fGS84KZL0=S0DIwx>H86xKhcPW6BCnqPoEIHeX}tmf)mwxT(2OAdrxN2s*yid!*+6Maq{ zdZ!%P%$UascQSVD1s82(YTYUqs2z&VYX~|5HT|kV&akj>?0)}=dtIViPqB^A9L%*z zCK8uxjPm^2%+MA%{5k2(&zN&|!!n&qnwiC0n3JkhxSFS>OuL~25Yx=%6n#f@JHZ9!w#d zqA-b-TdKa7Jc3909fgiM)l7(WN`I1>>u3bgo1&_h95gW{aRDrOU791)tvZ4ZL1Y68>t@5T3DQe_=II9MJ zVH)@NoUQk>kH!O}N+9F&-P;Q&8`ea-=vYGvHS~VdS@}$NRsJNGI?uwjsP^70UKKTAKR)&cwgRIw$Yp`kC+Fp9x;#lJnIsol+}Kq z^=k?!?QskrgmOwai3IMdZ{fUJmO6%l*fFMwZDUtBFZ%J0{%_C#2yxZ^ijeu05$g>2r)NjV7e8< zCweg*pgs7h$d;6%!gcP>TVe`Z%9p3H1W_{=QklTW=4sDhpwGWf zw*Zwi|8tRIw(DYC4a-Ko%0?F>AQ!$S+GwqyPU=uk8;?_lGw6c;4 zd!;oEq3~wI?O%0kQ4XfnOZ!BqUsk=kzbR1rXYW)ZSA%@`K0j+a|DRRFek0HJ;CJ~( zG30We?I^{47S-lIU|u|r6TqmfQLHg~H{_E$COJ1tkIP0mPcq-{iWc$sqPJ+}_sR^P zZ^=l*8>DOW&eZ<&1`5jt<3u3@L(t?X;^k94s2HRIqR5FajEW4WwD8)N{xU#Zb2us> zfTmv0$OX1@e;*=x0ON@)4SREnFGh5;7~3(tYx{BSPdr?jLKwD^+~!e5y87+CGWK|; zig_B>=A;+RRr4H68tyu7CIN2%FC$>#f{(1&H}296H~L=fck|Id+}j@5nmoX#XK@rA zt?YA8nyvZB!AW_ z?{7_BaYN&J!sy*eRu|%*K?D}0|I%e+k7IY~*HCtO+8&C5;x(=JOwLk{lHZ?7MYD>3 z-hB?Bd}}Jfbn8^3{l^3x#qvNXD)3N^Y7NXVOrN(&Xmo<;w$L40!k2><;+VG9Ip*7X0)_UsAi#`lB5=E(fO?y2(ZXosa2ejyf?$;zm=NH1c;L7lF^l{6}c199azJGgcYDEN1zkzN7E*v0js6KKQ1VMmdqJ*!G$&V z1vE20fS>CNq|H4*SMpS3P3)ksBvQhP`o0=B2cx6qe$1zkE@Bw~vE#gSwUd0#jy3{x zZ)~QV*pS=FJFBM-i2zW)v2m#d7|?k`OR$I+PDB@Dr9SVy1R7%q-{7yg!?c=DyTsEcKXRnn*5Mf=j$$Lhslo z=bW($fF7P42bt@@Cb@wDF|S4H)9rE_G)vCdB{B%8Upf{0E9~_0=|EM0_+>L+7JJcn zy8vCuy#P@(+Jii4<V{Kb}po!`QN8YUUL77nZjGh4XUn7)@$?8O)NP*Bgt4u$QZ`+3>7bT zG|jjFtX%wblH9h4>io~6UA7`Z-KufdEy1UZ4;$f62*y7?3c7e=He`g}T_s~x@S6I| zAjY~Vn8DPy(GPs{W@GB=jW!#^*Kwcy!X*2D)JuMAqyGILxmSM6`2VetW%Xa;c%}bU zu)^-Y3R=kjSHX*n|Efn}u9O!7W)HtP{tPJvMx+%UXh~-yWvDtM`mS3Z4 z()~A3kte*vD-O9Qe;V5!XWcZMIde_b4_v}u1Uu@*6`A{tOmuw5r@|e_2|1S;le|PqO!84BB;o0(u~>Ql|Trl z!@^aYvZ|bOAtJrbekXs7NkTup{V$n0VG}@LYvO+>5Og5$_1|S||9*JI|F@Iuf4=|6 z-EN8f<^ueEm%l&%AJ6yy`KJ8u+yDL6n5F;k!T;M{{QdbqZEF9&HT3{t2(iDdLh%O$ zX@F>P(06cobsfEx3Gq)CzHB@xIm!;?EgWeQZ{pRKYB~lD$c54VdY9b!(K>UQ|FYf! zkAZTyHmvh?zN^{zNne({V#sAkqBuyL)0eMevUct+*@di5R=~}w(Xt@k#rqfV_T>@( z%%vjj0P!a+57wh2-k)zYojxL<08RykfvJ`v5QN}z6!|du4LYLlhm)L@iE+h&ioxZ< znYkkpkyj*!5%e~;LQ)u-8uPdrj}HCF|w*>Ljm zkLS1xsDrPw+w;+>dG^@i56aTR&(|_jOWUd^Pe4_injF0IrN4*aEKnCv$?U$d!plqO zt+9xJoI19PojN%EZQonu9WsXs~`sH`SSZhPwg zh;q%il`9;|57s1t!jZ<4eA=Uy+)nS>c*D1g#hz~o2q2!bJ=Wt1*RGlKa6X}Qy&6)e zI%je_*0{4DH;clCOwJUc0G)Yv)T{^PD{WZY+xu+(DkHMQOv{+2Anovcug>^}3)n@g zQ1~y)A^bQ_#>qB0-TJDVC2Iebr193TY9)heptl#*X+DcjIjrhxW= zO|d|s8qFHX8X%LRCa9j+hqEZy{cBN`B?&gId$BDUs8K^H7JIRh7+)F3mkLDeWxhM3 zBYL$bPAExtvoNe%1VoGOm?u}0N$TGDDK9PON`x*M`vSR*pYNH#^^R_WmKWZtn>!hK zP^wXGEXJMBk80Px7%#g?oJD!TDOP^ES4r2jICd%O0sBKP<;@aDDF^3W z$Mz9oXmtS_1cD3pAVkxl_iUUhD>g}dq{Y}^hPcqjHD(XUs&^cq5 z9>0$4os&?lJo_kh1A!G8xJ`1?uV;S?gAWeQ+H0g|R6>&0p1%i$Od70JO z7dyL$e(IPdy1=LS1yWxoCAkS|KUM4AX@t`FzJB3Z2QHAS83on7{i6(L#FPw&Q*;F0 z8U~)OF(+80K92u7!D?N%Y25i5SAdo{!wU#T--IhKUG6`PD?qJ_x$f!)8o{R~-&!ev zv49<+S{-O`6l&tyR9Y4!kK*+@EFzRbeYt$kN12R4p|q?*AQ1vR9OwqK=>1|n3+e{B z{fzXL7_U#dE2(vR%;Qz?b%iJ^^79N@3#8_F&Dp@?%UZo&dvDNN-2%A=cHN8eES`RL zZUt_9%1QAOaou*cFn>-L^R51kG<+-UO9Q^_;dF)923wPxab25D<7L)IPGBXOTPMxg z+miEK3HYJy1>G;}HzpbvG0}eT!W(`V{Tiv0j&PaYSz15_p|cS+tw!r=*f=O7O*L+Y zBg*m<55C#1Vo`p^W?0A*tpW{rwK2xl*|SW|5{y!ml?d zT`!7I%!cy;?L@U0tLhtSG*kD0S9S3@K^`BjK~la*B>|NeMem82X0^wM^9A1aTYm|q zjixY=$%mo`edh5yecIz?TS5X17O7IbO+T6f`2c!r-e5*AeHHuGZ6k>$H^z&cEX$t& ztAJcD1$WiB9uVi)F@+bZ!6v58YO=QT@Z>spFXCHYb5%qrFN-=!lEEFN9&lg%hS$_~ z5ugy+5i%6t|HDpe*=6__kuChdvI@>gB5X^3*@3+GP~;51KsAeNpC1x2+@=TJ!;J6l z;*cP|fUymBGAXXH9-?D;-ncdQuKt>Ce^om~&j4Ee`s<1fZZ-~iXG{ee>~I-){Vz4Q zBP1>_MWU}46=ubU%Pj|)lP?j=aYHCNOB9dX9zBD;od|C5xe7VlR(Po5>xYtzU0=rQ zm}@LHczi(0_+J~2Uw3RF=X35$>M@e<-AFDT36v=f5)-&lZ*D~{Lw|k}RwtGKl`n?;Gqe6NdQGJI()KncBbTf{Zj-ld`I(%)@yd#rhUaVMczpb=;POdwU3|DxcE^FQ0UOArE8&@2KNfKvcYR1h9vb96oUp)3{)Z>)YSL8O zdT$p=vjbCavbQt=?bYoF2ynM;oqH6Zq3AH{GLKa;4{kC|V1JBu^(@>(jSEVP^0<$kE#OSER$ zFIx>cKJQV!g$k@l^jZ$>6kAso)CQQ?g)y(F-l(_N-fxUt>=eNHKxr*ii;f_ydIcl0JEo&+0hd;~ zs!n)K=**L!E79^E{c*^AocdmkW)stUJ@Kv`J?*N_dR|brUFll*)sD21t3Z^-l}#^m zwWg}CNap*~j9Az!+n?a=9g!%)+MBCRl#$EZxX8x~N%8KeBXCO5BX83SSO<=&b0RDw z!$=q2out*mHT~@R+QzcUHDW5IZUm|vAMYMV=V3h2l5QwTTAoayd|3mTdQoVv7q77w z^0o>$MZIsBz1aB)O5=WCcR{o<-tkB{H&CKM`<#PJyquxmr&+{N2&*-mh}NHsE~wbs((e<@KC$@xv-F+xEJ2-rvvxJ0b^*vJ0p6+Mg$jAttrt zM0g|VZ61T&>d6+N%+M|Ie6?r!^f1L)guNX`BO^$+SMHtIqn#vIa4VN%{j@4#ktSsv zF|d4>{EK(OjcXEdj@YfS(0G$DA0Ym{iQuQ3U4M4uoDt%6qc{uQJ%{%^)p=OC* zW_bOgHIAU$Fyr@hw}K~#S2N)DUiK-Xa<)!KwG=hHau;?^=MiC@W;Lfk>e``a@+~=1 z{WIEEJwuih5JxZ5N5nQJnr7EW_<|-o0{*%U#GvnX%nVRSk|#)?*>It`sG@5J0ZNE1i5di~RkggyCB1J-#rdx{iDu`4CLJdWv6G+4jsFaO{5_(5E z(mQd3AR!bH2%&F~4hby;2<2Vw`?;U{e0auq$N6%``Eb?;2866NSD9O$F)H+-Dw8`ZnD9-mvR8`)9n7Iba{U+-0z&PACwzj{pnwRUpx72H4_m;l> z+#2y~dm<)GJFU&Tt=WW3HHC&{T)8>ERuN>TotW?<(y=Kv7-0KqI8F~KlG4t%;YJF)Sp(dT$fio8j+d?*#ltrtdV5|5@SM?>4#~`Yu*Y zoZE29tZ@Qrj7n^VR;~PDDJWmh*W5gifI^Mi&UCm`aDfI!-NiYl66~6QDo(6dNXI?3 z;3!G)^$6*8JAeeukWdsET?B|pJJU>x3JIh}>5Ks#f%vecA1}{46pB_9UKGH~GzIC{ zO*6P?E;Ar+RA`U$5gl@!nXgY^&>FOCZ({+H8uyt(RW0rn_i~GeW;;qYziTrn$I2qo zN`2HX%zdWyFj+;jM-QKEo#*M$LhthC2zp+n-IFB-TZXBm{3vg#4iAT%2cQq_CLOA zwTVMYB4CxXp|ba}vtoT?lU^9&z<~HeJ`KQpIS?m2&|7%;QP8ZI?u1DMyapH~HT27> zs;PLCn*xnGKd6nQTn*@*4(3>+95XypOK}(5Lw$9tgpG-U`EQ) zo0n?i;F$2Vz3H}oL$rgKbveX^6^mul?5T#wQ1hBs9V58iW%u9Q342Z?bx22*d`RED zj8V(5Yu>Jx4v5U3;h(MzIj!D|;Xwwg@`#ea=4|`f z&lRS_+Z!Lxu@-&S!*_l0e^PR0e7!aePnwINd6dUP91O_)dHwp{Q~v#WA^JgjBK*b-Wu%hsMn%zOQgtkB=bTQx>lx%dLdV>KPF-UKEy z+?iNGN}r#-bfJyI@j!@bwL|v7stapQT|C^Oo)35a(K~10F>xKnGdIBDG40j6<@M3Z zd_aaabIfF-F>!C`|d|6k)db)vOS&i-flKdy-C?H%Z>eP*ys6kIamE-Ec|4mnC|Zh zGDV^$EXK(;S@u$CX$OSQ?>`#~_cczK;{!@mrw1%1ToCATc&VTcu@4V&T@frZ9Z&oO z^#|T!8KG?JxLg?xdP)2cBM?&{ZCBG`@-hE1U$s%lLXZZ=t+0Xj@YqBXz&j0Zn6E~f zdEeU7YGJj^pUHjJ&}cW^qLw1N5x^~Sjbax%or%JcOT%uCgGa7_ebkV{-MO;z7m(uT zc*e=b!>I=3a6dQlc(PR6Y_7mHL!#DM>3-Z^3+(E0dSzl9MP)})&v9JCV!tplLH1=$ zKbyEBYPf2uMZL%eCbwMotVz=4=b}9dWk_!cJ2w}Hk+81*w2oO*#n7DZU@`GY>0Ia7 zP~TxsMUznjHc25%r^B$3&03F^*RmdahA_+&lD*JOkJi*nOnG&2ZURpnY|IoW+>{AmH%DmpH%Fa%z{fvT+Tm z{<*ZeJz>-;+X&_TF1DF%`R}DT3GjXfvh;;_-T|*6Y+1wt=HesB2?D!MOv^qwjRn4| zwJw1xpTbXAdDDj977`dQsa6#c$~HKTxDSldw_WbnGmFi01Z{>urC`ftS8!J6Vbk}t zdu}7oYt%0bU?{Itm%sRQREN{K+)qCF>~lH1lSgg7x_U-1SaLbJ3Z$a?Sf-T07Xlz)G(VcGJKVDoT1;!~ z45bsKu?KpJT>Vn`R9&&#OLZny32+7hgjGtHePUMKCI_8da)h{Aw0vNfliLY<44JzQ z8m2#l90vVijZ6JCNXCyB3R~H-r|T#Yo=&fbuuStkn}%dWLyg=^{W&m+xRXOgQvr*5 zNLsG*svB?1KIvxS8MWl{4215oNFDm50R~uT4}9g9wFhg}Nqi%(&czNzG)GI9) zZ&_AziEj5(Ltq*>*TeSlRXHy!!Q?=WhVSD@c=Sma#Yd7PFtZn;#1^~;S0L)>y3W*J zfTLB>3R@$5X$%06{?eo-co#)fT@ z+=+IVq-a3D?S~GtuF8u?Dk_w1w{h&4V+`@eHIHj01C$`(gbFmX$c=(ItMkU^ZRVcS zGgoFlHxBahR3$%pwxQ<63R~_(3)&oBtlmt#nB`X=<}xq#E+r>df3k0*3dX=gdB6!K zG~mwp5&}o`mPDe5`06dk3~CDCzE)2>QFNP5fGx-Ho?!UJ%^%r53fXDwEz}Sd4zD5% zZ5afD2c+Q*qO;nm${k&Oa_!YBR@L;rMgw6NQQ5KrqdO<}BuV_=Yqq(x-N>9s_?J*? zvOsfhdn8H(QeI@IHAPi5PKW|h@gg25*BC{Oi{;wVcY8` z63k`&DOg)Dm~9A~5py?;U#~dc>bvS+dsi?>IYXjxMia0^l@L){TEk)+`N8{pSk*Tl z40FA6VuO9VPfR6CuuEjO&ECJ)v3bjBxVDAHZZjjW+4ox{yGh zL2KY90yZwnOqZ&mlZUQIcn1e#{jmg^R$P&H)1C`N`d;qu$LE`aJ#{{e69ND;vip^& zmgkT;ad4tQ-WHv(FgpUxICiLDcK!=YMz4b6#;lGU7W3_AbZN=SfiKm?;Gb zaHvDe<(<}OXr$4xx7?@d(>uU2w(alb;|&Ro?;}>79UBtFG*k|M-d2`4Y?;=^UL#ZX z%n;Z4(nb%iKbtBJbNMl!68Q}a2L2nwV;^BoQuMZ!Vh{izzV2%h#)DXj6>J$SyWe_m zLWL;wf>BRqgp)-&=TV4smF=XZJJ!hKsp7tkhn06me-%z@Jc#Eyp@}2__>pI8s;aT4 zYEODUrWbqN?pETNlm`!II4KtdOS|ZO?P9Al*SuSB9Jb*vyg2%tQx8&692cC(bMEWC zM1vU<76iWeEZEBB!a%rVgX}w1>kk4oIUNQ?1GeRj?h~SigBW#>;rzfq_F2UhK5M>{ z*~xTWSlDnhlmLWS4o}-QyW!(-S9vdBw2CN?G=IO*d%Ex5=?C*omA!w&>hhaOi9-BB z0P=unsLre^2EG_;sEKV>ak=GIU0A50*5s*-3Q=;IN^6Z=f7VNtN=t#P067BD_maiq z%fw|iAF5r<|0c)i>nI=pm%*J~3eLU@xgySV_#)8bd;kt>RX-X4#A+k;EVJ!xOtnjf z#$W0^n4Uu&|XWM1=3&DRn4Qg;8D-j43sm%mcvH2&oN z$|e?R)hS!NSNO;1nAITzvXPWLOjwt&ayqPMP6exo(d+454DvS40+-r_ZHJFq**^($ z*I#&Uhq>;ky!>SgXfE|vbNbk9Gmu^lsSdoa_2@7;(KXN^#Qw>IArYK{9H!G*IC&`&TckJT+ui*i#5E^|GoGZVUVAgOJRi1)zOG<y7m~BcKX7i$>ZO-06_->tfMcgqXSZD8MQR0ZHJ-hD;@J-AxnW5&ITXr#QgOy z0$E+|x>OcK$=wsBjTxIzyMFXL>CZax_w>l5{5F~r6M;8X_F-p~4&mW2)z;^;Hsy?& zSuQI+!i&C;#M`quAAS$9NToFfnBT*BaZ>3gUCf9+$cnMHSas<|+vOu)5;pXDs%)73 zP6whoex%ACU_L))g)Lb=qcfthH?bi*F09=d=m9@FoGzOpIO_r8OAWZyw(3(m&Gr~P zD*Ld+AV$7S0$5<$QvP^2b%N=0T}k|P@alHn@dERjQHr>}hgdoqJ8nueWcInO27rfo&HTinyej z?@Ho_LxmSr7sO$Kk-&oajVw>Cdg$pYDnzSWT}7dS(sK#H?z|!`yeQ{bl0D9=-Lul| zy!q+a0-|7utG&wFk7bK%_5l_;NUAK+p**z?gITd9GMZMlPJJ&(yfhJMZIi>D*`qqvXD`rY>IgmMS;7 z1YQ`KjrhS(2n~F@%!Mz#)|+K%5*to<`b@~Mm+S`k`3f#*8)?O1+qM*pF{?MEpdgK3 zNfy_+$IW~d*#C}Ws&aUr-4<-w3cRoE?4bG8 z#xKt{X?+}bh%xa9MOl>*f{r8#k^Z`3gTh)+3e#8q(^F?52K1SnyWZ>}CD`7dS=mSx z;MOWPYwo}E4!Ey{BHg7PoQaLU{Y)$fQJ>@9w4A=OFXQ+lI>68!({{hkiQi71zU?j& zXSJW5_+sgEVnf zHe9c|m@YgdzSqfOEym24vq94J=NU!dPV5s5>eoTt3{IwoQTOJB6;_{}-@2%J?4N>) zq}^bfAzTA(-fON7|6Lvv0a(N$sF1%GQid!IaaI@SpDieuhMHFJ86-ebe+VxnGF#8do#}=blRE&WgJil{ zree{d{bdD)PF|2_ju!u3mfiW*_uB1 z*#up7|CYrfZZuPm4?>RgU#C!QnCnO`8`y`Lo^<8CUMTZ=;;)t%We_$m`VduBjMGC_ zKoWUs=C|Dm64kUuo_N<8n9Et)q8UMgOlR~CSnL=&940eM3lVByfqRuzNJ{Gg;~=z;t7y9#CSwc@h~B`<4^9UNAbD)`1A zt-b=4QEY5qpm;^P)aF=pDq#EDB)D7uEqm8}@f@>tcs%qEDXPKQ?gx5<(`togGIk93ElPTDD)@sB9V^HrJbH_LL2LKRGWydXvv>ulka z6$f*f2R}Zw-)_67Vz_12%%sAS?!9%vSVudOuI=0L7Qc>;oDuzqo%gtydo>vo2?nCH zdc_I$`bC0A628{mC;VdTJxv06s=5Fl?Awt>+8MmAv^rc^1YU?UE@SjJ+<-0jg6^56 z{24p>0gvBL(aZ4z`MP(XP^dKk_-*uEpHe>Z2QCm>?rCRhQ1^f$Eu+iIrVgsiT)(#z z%X+Dl@_Vx3dL3Ds)2iDO7|xXv!Ia%8Mk*a!VibLfO1psJ6MSNxTQV4ddn1#8TNr7D=JT zx3P)ZJ&Z#6?idwdT+`*@1qQaa*mi*7ZtS@e=oY= z0X-Vtj){&Zbc$P3@(h$kY!U+bFWPUGhi&i+!1e zhxH{K7w3Pj_hg)ObzgUwZ`!;HO|rDGqhKPP0;#HD%WeWj>k=0uJ5vS@$_5u>V^*E% zyQJjqgJ~eIB>3{OioH2QWjP$MbPJfL??~3)`H0QEu`67D5#fdoEOCLr?x)-5I0rgT z-a&r{?dCu10(kLYO!vwe5ZZnKVZ98uwkRvjIiy$rzO#oxhY2rk^GCQw#;%kOMQ0sG z$36uD#eyJ4ot{X|9^q)bgVVU26`_vfFAh-A0me>mN(p6O3`)$3 zMZyI{maNsTKK(W6_d>)=-r9q*H2dF;44Wd85-;vMwnebf8Wo;CCGwxO$nQ8jz z!0rnzilWcLEaySNQofqg5#x`t)I7_ji?AWpOu&DI7s+T&m_i!>t$g)clzAk1^CBV7 z06(%>1o8`qLiWQ1g_vIV@*Timp>H7qLB(Q zZ&m#frI8SuLrZqYH#$TD1Xd1Eb#A!vcEg<=?}gmhV2vIA1vXTOlV-)Eb#nn`zMicL z6O+!1&W_Td*r#u02ZsvahRp%!{y>%tK@JdU{hF)ox8jDW7XHYNgE!V}Dl0my~iis^iuF{S)a{e|L#gZZWn*bjOYVJ8lb^3O+ zE9ZOdpr%$22K_@kC&{q6VcrWIyy*J#6fdJvHJ@G2y~&-!ghebo<1SI9}hTC)!PY$!N*rXyW$JM z3miZ|K7pu@eDFPNd_wTb78fX|>QIeYDRO1bniNsdSBcv;7suZSn{5akq?Y;BKUZ`h zt#?2Cu@|LAxw}d&@nrhk8QnO{V}grp*dnVtj(8D5TzzY zxiNn?Xa*N`Q+;OY5?o$oY#$X3tELywc+rN-b$-<{e~FT9>e)B=v9q`Dz+S!KZ$1a= znMxN*0HTifFoax~_;p!<_(O;Bz}=^^u}x=uxE>lL>`PH6Rp|BCn$8pNICdpKVY0q< zkGNfFCD{D1F^b6DT%hGX`LQ}6!sTh|ZLg3uYy;ru#dk>bO&B#Gpz_gFwwtVPe0_UEEg!tm-Xpyiu)ixY4dlSU@>9 zk+2=b2=!O13Cn4<$A7M>8faqVv-v@um|OlUM9(YS@iUpmeq` zUvDZoRx$bUGN9oV{*cpgK-WP<>OY$VfgSahGv?0nPnVg%TL#sNz~od8Wn(yRGWH7^ z#`;sn8g4G%bT!}DbD;5INEplpX^cl?m}<|rr@f@@ATXnjds6Px28)Tb^-qm&42la& zhMmP8CVf&g)3)bN6`}Vo_vq;r2EZ2Xy*U=sKY;|RU}%o0iP=WgoXJmaB*+k45R)+Q z46_&uWN`d^{NvoAm~JIA6Q!tFAH?kPp0Gx zrULHAQh$C-?1;)sPs{u>lv*YFVnkJh2AMJ0#n72SdmgOQqm2~jR48!%*`fpe7+>r9 zDop;}K{r~X>9^y1nC`g&xP!|InRg?cX=f-U@ItYOjRMsV!~H!0%2T0LdAa#*h!-yE zx^hpFLNMayZt-sLaKg-gvIn09ea!f^QcgytHhR^GSy;0X`nOCwH_ZfYkWQ}ERqtnr z3loeV*YV!f1?>)F1-{R1=E&}Ec})A57Z-WF^ngs1@KoSPa+s*2)`zfVBOre3C#j`z z)27y6vu6r<<#`8eIRcwqwGoBvq8L^c9$1M6u1P2s(s}!6b2GIEKtCFWc1c*kmM>_A zJ;ZC9ssZ1rvHPd}ujsaCQ)l*i4#ycOhlhvBBR<;72Ko6=eQeCdPY@CP_#&*}(-z)c0NtWN%O8!){^e`8PxFp6L|W8|!jY zo-9nGeNMbGka9V^jbjb6g9URsc2W$D8~$FNGD;Eyc`TGlJ*Ld{(P$0Y40+`4DK*9J zk?!(|BxbEt(3HlsifFRh$U;&}{#(bYL2>}mefvy#&39Y9eYhd)VSv%_wtr@I*1^4I zY^ZV5+J3}YZsYck6EDh80*PO3p>D@Y?_{qUSl*k-kf+a`U>j4>vLB$fXY*E{YKS_WlauO1Ih@hpB+XX4m6b5H3Oh4}PL~|u5AFCjDO+&Qbs@}etbiF;a5B~q5OWq| z-Kl-i`I3u)oXo4Z)kHf>!B}&vicv4l6MXGmPcS}avL9ZpOALE`l7rdMN@e zb^CP&#q}xG&Cq$>Uw86H?Kk+Db%#;1QlN=mlZRZ7Cb-bdDhJZg0b{X;@Td)C4=j%2 zkay*-M=4Q-_RwcQX|M9;l&@vZNhz0xDsmfiq+$+Jya1Bz&|*V7dXVzrN2M@Sg~qA9vrV zPqv9+Z@=VbSE?GfW^Eju2QNs;^Oe)byTVHjkB@IoIm=@n_rk=4Et@vXXXfE*wJ^GU zAPBkLx6F!EcAehR4Y_o`LPtjBqgIB>cbP>w@Vs$L>G(<@lJ`;~)nQQ_PY1YebqG3& zj9(Fv+|&V~pO44>ttA2OGVGrlbp~UUXY-dyL>iy07>GM7Q0{|tthlW+aegMEcSHMQ zr~e393BI>bHQ0+Z^`)42-%xaMP2YW@ce_|&*afY+^Jpp+Z~ZE? zLm-+nML}ASxyMY8PoL~dp(|Gdr1i8vg+1A17@r>NgBpfuN10aW>za41Y4#M?yDDA( znvUGm)Hg`^z8mQka{O(otIL3?$8EHsI-GMk&mkjTjC;8CAQ|XTBsc#Z_*qPY;xffG z;}Pnjb8r2LWi#NY&4Blr)2vQ;^;E)^1(0#C3J#NPsEyYq=cFd6?PYrRIO|-nLz6{C ztA)`4I*neGyyyTsiau5Ckq?H9>=utwYgOV8zAaL-U%@&psJ(7Qfds8NDvKy>>!R?1 z4pe$6BScf0vS`0c%K|!GXWk4(MSPJ3*mh;GgErHgO8Q<+B z#F#$&dmRCc4iht|utG!?utPi<7SBKIq{SK9`kc&}WGq(_XGIKjEF;6w;EyMoWed*t$r&#c2BOgwwdA zD46p16ltv334`SoS`^cRuS4zg!yEtz_SU0y2?z8VY*_{H(06rpNq9IYmuHt%hs(I! z@90aMOehYW8>xv%KfxT=5EcyUzje?D2SM$eWON`A8bTXiI&tz)mB$eKOYJ~jVZoK_ zlkw~_Z8u#uG9|>!y|bvp)ymy_qwhuv!eg(Kf>hB@>>_wwR;m%RWz-CY=00-g2~`5$ zi1vmy(}y9gFVR8kb9+Nn*|vrQ&0!5*Mog-;?IRYF!e77R0|i)qxz9&ZGra9)1CLfAx;~8-Kw`UyIVG&&)7mQsAn*Lfvfnd0)*; zqkjAEo)qjU{!h{zV$YF)N4nd$DsdgLEqs&GfRx8@U8owgF^qMD$(t3GRy`3D#|Yv} z;<#K^{%$x+F)c<+);;2lux`i;=-iB*OnAu=8F^j2dw3qf0XL?acjI}^4hl09;H*^p z{Ww68CdL`^!>vuLB)#brhDKb8NF!M4_HOR@qBn1!f@@tl6F^?r{|PvZla+)B0V zzP<2R8}p7r>u^RkvXqu}01H+@M!M>mEX*=Mr@!lf_jA=rP#tcyX44#5g)-OJcd>w* zDkGk8EC(I=q(FUo>MD|PItD0B=l*JqOsW^eV&@yi_9GS?xQ!ot%|XN*Ldeeygcd8& z1Kis_y0Z=l9mtAwpGgYmQ%%(ve8HkdDymNVM7u`;Ic!FB{kerbEdg!J{vrD^l$_9Z zt4R8OMd*m)r&ATv*DrdZ>avB;oEDWP*OT+abNDQ&D55VxNszi|&0f>lLI{PMbdxFvaq zA>r=tw+(kQ5c4M5r4igp5tE@p zFFf^9V#jsd1zm3aaDeAnmUc3ZH}U&gIgUrC4sZEKovDvU7z)REGe}iTjjSJ)$IK+= zse0iNvg1b88Di!>r>)P1^5d$ME1gV+LXoBtq?a*%^e(K#E^J;LP#jL7W~64zvvnAN zmx~u*BMeYs!Jp)QJPUq8HT}r5+_=2>v-?zpgE#M5UiypV6R6s0_R$-~f|WVijiVZ5Y?UQPZWnag>FZN=mw5X~Vjyhfy- z>0Dh#b?yZNX6ok6fouy0{KP7($X)K`xYf|X=cNq^!LPtfGx2IuqD$q*l7kPwjoE8+ z?QMyV?5`TdC>W2PliAh0HKG-Y|JTF&l^>dp5Z)-)+iXz~;rP02w0lng^jTIU%iF}P z{DjIUPUr=xvA)iXOKrU#R~W9XQq(x%I8KXW5o3F(QgS=B{%F!_D5tF2Hs?d<`DKdU zzxsskJ|KYmU%&p-h`7ARWw-z3gIkh*|C@Er|0zGaWf&FPObe9#^wT^E;qblb3xUiv zKM2tK)lf>>H>_^c!+$!a?z23t9%?irT>iGnSq@xHc0ZG8O!C)BJo^b0hca{p&@$vER=&SI#h%S9y@tOi3TXDTA`T;lk{cZ zImN2_VukQisEzBH# zm9${#GJI)$aoE#My6GG4m$iCa;Uxw&O!MEc9imUWew{%MO}4_9+d!5-ITJWH;L_UZ zgU8>LbqP#@!N%z(|8=_9IN=Wr_|U&d)eJK%GQOIlzfVIjo2i9Tw`Z*kTO)Kg?t&fo zL48yzpW)W&@b_>I+5J=c3iE%?=FN2_A@@Hy;29{}NI&(ReWr0q_f~#JFO4rJUH87U zij=XDZ6&wA84Z5tF%7{Td4W)S3vEzeWeOa~YrQqw#^HvdwK&-S2PWtB&-_n)f zqt*FW=l$2u;<4XdSpWQAJ{c8qTKKPO=6^o@|F1^>KU|>we`{rE;RTSn)2Cu)bs-qZ zKk2gGA=N=nIJo0Ty{=UQe|_V|ol7*M5tm*&SiH-(5a)bnA%2Iy^!4lCih|trtxsz2 z)`UewX0~_*?&-?vLc9sHln8Z!eRviYyZX_YAli>T&iohLOIGuLfWgK74=}i^{}Dz8 z^dkP*l5YMFFt|tm0}L)&?7!tf{##q(is^p@#QuFi?)?9V1^t(pw$T4O!C#sq|2N#h zR;q!{f9C@H`{@Y%pLyIo6Pv8DSDYaV^gEJ{EDJ^pwn5}~)0qMQed>m9OPhm@OPG)l zKy0*zpTS$;%m7p~!p+~*(Lv~lgd?0gk3-+Z9w>q{E>AK(Wo@3Dl_ta}xQg^1C?hml zDg00V#d7@Tl4zrzgtOT?n}F-0;7gvC09!%Fa%Lq5&hKdn-6mLB;)`P+i(#E*;yGkzTX~kfAfe4x=cXib?7V~Fis$@Bp z@Y~%8)qC3N)Tde3h37YG27p_9pXz$Hja7hk)oAZ4u+7^`cWqgo!mm4&(9O|hGkS~k z;;im(mi_lTjx;Rt&}6UWMGmve&v`VcAmS^4(4PE^Hd!0v&`Pf)v3vA|9t zVNuWW?*Ru?ngHCbLfFTX+@wB4`5mwQn6V2(`)6`)Jy^)SIt^MY^Sc1oX>-I1K^gR# z&F8Er^v3Rp^wcFDYf$jjBl@qewQuPC%|}1u0r9^!qoY~KJ71*k z_A{aEwYDsy$VAqx_Q4}nd+6p3pEx_*z2!%!AWi9 z0M{2S^iUWT<<2j+=iMkL%i;T+Q%P88^cx%MfI%27a*ZMW`Z|6`l#7X{KhtPGH;GBp zI{mX@qxH3;uGiadV_z-W0Ebh(a$5A40$YBB!G(EJZW8u$Ju5)YbK`uasJPSS)&6hY zTu_pLNiQgg2T#9AL0t1^<6k870Th_KJzJyPFWI0Q1Vv8O-T?IK%W@5xAqFWbf*(1t z4(duXA^O^ILYJ?q7JCPfH$(vt0rN4&*2+Z!8Q2>#HZKR`eL6CmLElZ<))0|lO_L>$ zc%pUDPYCCa2M_mYGH7+E_LmQj>7Le>-QWgsw_?4NcI0Ft33)!v6yhBZ|Az~@j=_T- zjFxxO5kpu3y1gPuFI5S3Diyhfa^y8_jfh7)LL{@tOpnf1dv5}M=-9`@Y4}&@7id07 z1^!h+lhs&EG91srHfvE|QCctE*4TXAbWYyq)e{_}^Ul3GZn!)Hf##aO89 z!g&=nwpz-@6Ox)%X`c-H#KzN7?KbjwMc_6IUkKCW%&Z{RXN^FlvI3xZaDRl|%ZzaD zU)an-IZT4ZvR0l&W73cJbn#Tp03)g`Rae?#hQ@1?;3L`?$)xlHDu&kPsX!ENl#nU_Y( zNiGlmk?W6wqoAy(tZ9|#6=_pJq(e#pLXv&6L0OgHMUlwMnYuHMN6a;lJ8p$3ZN@iV zTkZ`*2Nk9{lWyL~5bxAk%+ssA=K`%_^7_-}?51B*8NY*wl!Yo=CQbOwYjS1-v-4Dk zhN{SPaiEiazPn_%k-x4B<6gSs<{y|ZXJp^zbn->hci_EdbW(%)L*a&6S*Q8@b4WsG z6o3c1BG6U%bR^b5yY_BK$%vAh^N37c<=_?@B+x76+9PMQY@IlEO9c|xiP}ze!CeDj z5?QhdvCdd87RTB9830)gO!-MZ=6kv{$-1Ajo6 ztKjU2y`n~;`ms7OyGl@eG6}hpMkE7ji&;_5iud-H+ut>-eD)@=CIDA}O@+JoZ_d#S zmDLQw5bpk)K$o|fR*r4!(xO{!_&TeD!q=hM<;&$e|2h5R`Dj_WxNj*B2eArtRiCUI zZY9}SIOm?I`@T55aJc!pDPh|#2tcg91Z+KEqEP@l%9pGAk;%1vCmY$-Ms(%|OzqUF z47NnVu*FP=h4LGXX3D@5xfCr1!L;DDH=WORo(w>mrGPunB-5xd#g+87_TKZ}nGHZc z^!edYK7hs?{E9~;!g26O5GiB6qQ>V3)x4pqg>^qC*yi%2Y*Q#r(olWZ&psVE`Q$SU zB>on)yjdMN!*$CO;tyKdEJO3*mwc!$Zgxg>KJ_D;J$zYWd9BA2@*`>J)0mG-GuG>l zWA=kfqTPLv&0cqIvrTXv~k<2ik=V00vR*ml>LL z84xcj@_E&Pb=>CJ7?e9^vm##`Y3vBdcl#+tuLVdcOa<^fzsSP>3Q2 z?qubgTD$a_73Qb=LT67XvE(o&-ww`)KuB=& z@Mj%3VM|vU=mxc}gcAYDaoIWwBAG|^RA|@9`}}nbFZ0$0Ar<+(NgdF77V<5cxJJlM zT#>9BjHH!VwG<=}&e$crAP%=8)&`MbYpaB8Os9~L2pAxEyCVK(Q8-qtxk7pv6t7Af z4hS+!=7mwE;bj(>9RHqq6xK#rcWL2JT-^3&&M6@hS3mOp4qF{g=Pe;6N<(MpMb zLVl>(`qAY{SQ6kH&ZdJDpKI#~`_QT&&&WOyD~=)$kE|5}1&KE@4C{W2?h4l~Qkywz zdQy*C>mEY4j4|%>tr0{IbOe75qxT_)mp{(^1Wsdh^GPJ|r-I5(s5gm3I6t;ODEZ{| zdPt#VoVkUv-?5hXSi%}L{D+5S$2#G-p9<(+cILgHbvAvukcwHsgdbE~>3FxGu9o-e zFbr1vgVE>UaPB^zk(Je7!)D@Rl69cXK245@eVcXdMFi(l>CeZE4@4ASYu$2x&I8~j zD@Ip#IjOy6>XK*GB6o|;RZIls_U?>Bbf$6u!^EcS63@N5#3EF{2eRM+wi%ceTx+-CK;d7aS6arG5O<Kd?;U&2pZ0k{N~f6ub~${gnH~y%Jgn^Vwl6dFQuuV`G_4Ex z(40FqVwoTq7W{gFmUJo;5g#Soyfxl2 z#%KN{h+2*?f&e(zykZ>*E=c7`U9NyXwO(jKL(!h-O!Noz+}ksNv&;yv%IpGn_s3Fs z!q}852bL7-*km4g&qkpGuGI%9eT~uvWThjmfMb;oO=vO{G{ECCycE;Lvi1}gT=L`Q z|3c#sgDHMF1Iiw7bWSq zBkJAJ6`L5b=gPc(XA4jA>z5kPv$G6}uGRQG^alDxk59bv+tp(!nRk~$yYK+;Sxcq+ zExFco;;ShD4foC&kKNRexXN8&&rroEwRJ0P7RxC#H!zN9iN#hBuU4=BKsOK~BHQ}- zAJ_T^vMdt)TpTSI8+e)9wKD61I6fB74eJFA{Y@wc9b5jZc2z+!$*>7^k8tjx1$+8h zDaP`4ZxkLtb6?Nr1JpJE7E4h&tT>2FR&g7MCHiDgNkE{3kK;6|W&BRXq3mg>!QX9} zpC(=(b``C{cj{#-Dq37yvjup&=}JMxH`ETL{ydYE1E*XsX^&DcKVK!}jCpB@)ZE-X zZcy4iCf)Lt_YGjoaoA^fn!Vf;j3Y-Rh^Q9`3ab}X6L0_7j-H&uY!c4xxweg&D~!Vr zrT-+k(CjMi-SHLmwVMx0sRcx{dt48mS!dL_vc*%M1AI`k`R)?cSVD;!Q9|^|npV(;$@{^3;BFl)_1G1Cnho#}I#L1Hwyk z-LfZA;J}J?$#4qw_DCR^T-?@Wmzx8Y-Bqh1W#4`n`_1|5Y=hf+>K{j@t1KyDaIK83 zu<#c-KlImrB(nFxF4bIih|y<+pCXJ6u4L#l^F!ztU-cHC+#r{uHyHnH#}?Z|mW zs(m>;Ip{4c;ZB~hXvvA$?xwFz{@{{?$7b~FrAwStLe#7Vd{sI_!@LYQVL(`W9o{qRIBu4DNJ*`evVNrATH@XU4K;Ks%1NWio ze#RHXyzupQUrWlR7pe)D+TF(q`kFf=3utGbKl?{R!nfWfq2cdu=LI$!@F~=yAVpjT zf7pIHY+~SKVk^W4uPvyq+FaX_ z%3gY{DHBlVH~%)Hx9RG`{yI^Ct}0w4bIYnc|GiybKpfw$ZaH9~TyiQ?pt=1)#{PJw zgUVF|Uwk9%($fa5HghTmx}lQeRf7m_7Cxr9H2|q8a8xsZd)34`Kb@qB8YVN>-{Wk) z^0eiRzlY(A(TCstH$V#%gVQUaG*4^$ce!W?chkq5sx~$!mW94f>acUU66_h?c^WLb{FgL$&nm|-PIpVpS1bbTw^`WWp(gMg| zJIwa;spYhxuxz*QvKCxCj0t>pj`M2l6#f~PNK0cre?I&l@$c-TIyj)}`tRQp?dr*} zcNZgqh8bMn8h~A7IDuVk_1?eV?D3jCjaUuB=inbZ%*@pPg>&523=^agmhd1l_k6_* zEp6(c75`}`-({5k=ozAGp2FVcV~BRU)QHkY*S96{>cVC za2n=>4nK6xSpON40Qoaq8-yPvtS_o<+jQFe?x4rUchmcN#8~gXn56HUZ)3imOx2a< z3qkj5R-~H|b4_(994+)4n};XQ zD??}Yt(A~jW6n71orc-^LR{T8I%zAD|Hnfc4j=%4QgQRAyZ5;~ZZ$mQW1v*oLME(z z!Cy{D;$@O!=IP%39g>3muMeIIyU7v~Idg5m5`|I1kpkrZ5_ z^`2AGq8Y}WDa+XC6xQ8ehH`D@^?$V7ljXGF}& z!Q@3CnYUHd8(eztjE!*q@>+ZgrJ)B~VHb;cb!F$uH(zgvp>;*!?>{*JygmxGXod0- zrLzcOZ91a8E96JhWMKB|zEjpD`rI`oSxH~5l!%VbD2M~YxfW2g(enZ=32C%h{Jb9P zB{YaNp84dBt3_)JdUexGd+X87yu!ep2EE9yr@54KZq&X$O}%7$XDSguqo969LCUXE zKd|{t)qbu5-2yo*XC)K4APOJ8-E9nIim~Z@<7Dw%;l6a#_j>>+AOSYb!!;~7R8*+W<;32VDt&2GiDf;V!x}-K9Ez(ryg$vtdO%XCxvm0Tqe9pDuXV7) zmopxy)xlX>NqiSSD2ZAh0RWn+L#ra7?{rvnhARn2r-PmCe89@CkVf(9eW<^{;$nz> za>JG%*_05G)F!w@pfpp`d;X}{FO4*0Q4%e`CMMKDw*9hgrnuP3Eu2c%I8+(9u$m=M z2j70nbI>qHHB?$-ICJ(qNMm3&@fF!_7N)zHKiKhh&{foo{Hs5__>x;++lO;U5N`{V zZBZNp!IF{PUNk%J%ckgj$rmjY!I`r@!Z|d^;g9ChkboEa&(ZWi`BO5CJ*m;w5UXB| zaYej;X6x96JejW$*BLz@EWqp1xu)S2H2ZPheFe|a>ySqKZM~q#R584HG72FvSn*_J z;M0dGiWY+EM(mynk#mv|Ru43KS?E#sEv%RFVuRFD@ov!IUDC0dN#^5SUZ=|X%U&DF zC2!$X5l#?)TAaz8C$k~VpLK6-xkm7VJI}h|=FAL9{MvN;hC@=qXIY)RWYEW(84cDy z_cwj>D-2a2BKN9V+Bi^!tqnXTa$-9SLhmiRzE!PWBwadPn-J>xnVXVBhA3o;sdYh^ zwh!GExc~mZKE3|qRel9f=VwqnmG`FckRf8$D{3peqaOX)QWW#1!4sc;A$?!>D4${T zeW#$6h5W9eW}${SjZt*j_S=QG&wOZHttM9@#&m$fe?pMOB;-Sz23zkgEiGzq+S;Rt+N(rsccF?JMa1YZs%Mzk&+Go(e>~40&-ss5)6dH1IM3rej`!gFxu-!Dt>@qn z^PHZI%fOxg_QPm0ZOCY?eOKEP; znRe#$Bfy59$wB|v4kIoYF)EFTu4Wf8=b%s)G}K!T_;rC!2CyH&wI6iPDC^LUFD3Gf zR@Oz&X*-lRUkK;Ui4!;4wRLe`^K2icckqJ@w30{{QXcaKJuBK$dYs%lvnwkYd%U`# z)}crJ5v@&~#jlk)y9x%^a0dn<#PEc(fuT~zx(+j=JhMLIt7^cq^P-g9LyCBH2-XqZ zrky!(XwC8IvC@8F3(HFPRzhs-N-SsMl<(~Y+KDFp!gauymJi5><1~mofbZ%blLo!G zc`7L2nd&5Ds4PoeVdP%oC{*h#l2O*WL|{kzy5ZqnkFC`ABCjvpkea>IH6K1l*A?*H zCpJD|J<*E&%foV(t)5Baft7oKW9>tcsq6+JFRt9PP z{B>V{k$|#_sQHUMWi1iv-t$X%osXk!`UBq}l09Sj5{J`Sb+~&XXmbOm3k$tcS^EjD6`RgE9E-6sU=vl2iQLC(AYln!OHgsw#a`YbbLjAA*v-Yywc2VwC`ceCW#-aIf*V120RE6dQrxaoz2g>LbG?RN$Y4s@sr2hg!RI8BhMaxs ze1>b0j}i3GjQ5WZ#VQOz&xFsQk+xr=R`fR_{Nl-6x(>ev`2z}p!=H5B`5p=PVT>g% zr6xu@POV1+nVU#Eg`V&!gp?H@=V8CmNg1Rq(z&JTlBj&V(8tz$Sz2DM%%3Njj@Xt*dd$8Szw~r6q<% zkN#8(eH2IBjduCo)#1agf<9%86x}(LzmNh2$prvMy6sM96cz0>zux&>C+5ECpFN0+ zOB>71du(wc$*2BwCqy;;u1+UsMf0E}!|TUWMMJWJ zzkfav6w(=sX1i^?2eRVWm}=9hG4#`%tJ)5oR@rHT7ldG<8Cc_P9!1SlHA>W+1?!AF zTtkGdijt)Dk1u0Fgf(K=NW&)7gkQv%(!r@s4EFS+QKZ?j09Eu}R3ztg?e`8VmM=bK zQ@@q$UOTtA4cg)Z+@<8zpf>*V+zCqBQ7?OYkE%Mcn8@u;hVpEAjMpZMXQ z#H-!L`m^p!UhwSm9O=93AiE;-^--tsQc~peN1q9&2W}5IEGmoZ?4AiK{3)sv!mSdZ z_okP>B!+q-Nx#p}`t}KZ|8+Gne$CP~DdFO_$HG(3%BfF{)RU*KHI3deG=dVRni%B@AzvY{j4MM{c;M%fEe$!D; zk%eE2z$YYMyP6Zdm}Bu;e+fLeGVBqt6L32`eyv-qM&d27fr`n`Dcy{xxS=ps)ZN(n zngh~!BP~v1E1S?!sxrhoJ~e3rJ-f1x!3y+C756Jey-+ptPNr*w5=)jIAXYl%(*||> z<+JE7Vg*B^0oM=A%9P9%+aeyo1p`N>2}oxZX(Ah;VnWJ26%Z$(@11}OmqPGjtO<_% z>Yh516qD%7Znk_SH@zDM6)?oDI45Pth3*w<0>;-gk@<%X2NbuE{J#i1Eu@{YJiDU~*R63$y|i({wche85I;Js%ZAW1$FvK$D5@PkjWZ2nlcK(WJ~TuLA=|0&UjHu^0FFt7gjK=-d(c!E zMjLX#3`X{&O+^ivmflAB?KrxuZ_ti3Gabxl9@kL>P#~Kl02(f4u+Ca-(fXEw>`epr z=1L`Izys@M{j>Y>r$03-HV}*EFKKmwOm;C zsVrrmE$)sd8N@(+m+3#$->vjY2zGv47An?4Qo3`wzoV0&PklolS|Fy-R&Z}Q^Z^+D z6+Nkhcs96U2}Ta0uP7pV1~;6*$S>$2d4y1`En7U795K0W-f~v>%JnYXUSZ1xkGOQ_ zj~&8OSteA{R>f9;wGij*L!$iJnX;AJ?M^U4B0(YeF^S{c>(xcV6uWKTZrEmhfZ7Vk zr{TP{4Bk`c<)U3V+tT;T3J6)&r48>Woy-BiYNg`$hdf3JNaM8{1OI(7>xgH;oNGRO zo%-uDPstLYJHnbEYaP*;2=@JUpS$^d5|_}>pmJM9vi{=7gv_5wsPjObFr#3{I%~^3 zW^v={hl}5Cy_dh|9*7*mRr`e$?!|pf7-frs&}0=yYiV1kP^pIkj>ABQ|3z%ib^UHw z>(L(?{GX>JGm7XD(`SU!EEqZWn1^e9?AOdQnDlM;d((0BimMG`3GdSMkgB?C?YxxO zxi1m0+@Q1XjS<#Yp=Z75AptVdyVkdCzZQ1WAk(y&rWu2?g1Nt}Gl@It3x1D0tNvGa z3rxDi4jR$TI3;9X_gta7A{}&cq&N`FaU-)+`IvQkEbKDyK+zk#3+knhmbY{y-i(TZ zAzNF-1V+)M%uwtV`{#eC3$|bwsJg1tDY4rc<(ataS*U<0X4}-*Amlp;sZ%z;Y33)& zuQJ|lueo7S^=VS*)_eR!UPfXcqJeljO9hcuW*kV9Ove;PWWSD$0qWVUuzam3m+jQS zjeaRED4Q*QG?hJGJ%Yj!d688jnM%+7lfj*J=LN#>y&r%`ut8D%fI4t*t2(57*x;RU zx~hGqooDxSUM5JXTYA$z^uVN{eS_9KlT|_P%8?LB!B8*h;`bNR!iqbdolfHBD?Z4B z;T-yoI(#hLTlg`C+M`j$;S!n2#NOCY52X6P+OO%ymV$eukMX@zhT=gv*OufW8Z+PL z!=Y!Za+^n&#_3XC`_>1AAvVL;tj>s|l#aB?w~pKM;*J$h@-KTqtdOL55x#0hJl@D@l7IG|1dmytWD_phkfun}d;gT#E#67J-rZ#bGR}z9kg; z=&=2d_eIswWJg1$%eJ(KLN&p*J-h;!qq3y2E>U8Zg<}lB=KSKr6+(6{2Bh<@TiceS zF%DSyTP3d_4gMx9qBp!gWPOy3n@k4er?|n7dF6dn<-=H+o@Dz=#Nvy zH>Z_5m+mkx>}@#pI-XZw|b4)azhQ4&r~|A%-8zW)Gj8t`CcfXOhTU&;h=K9DEF z84ykBeY@x>)zN z>Rb0MRm7{IL{m?tOKFCeo_t1j-tYc`u=a}aSQt#;&%v~yTDqi)Tf^#jN+sFKu0e%H zsaD={ObZGQ!Z4y;8#PPp%mKN@+)*seSX|G_0Uzataa68t>;<%M4F1Et&=t75-u1H2 z8~PAs=GE(;d$O_JojIyQeC`l27KD6Zzh=`0-OorJ+^ApNW+M0FhB=|pq*uKZ0PKZ* z^_P`ba<&Shkjjj0sdjF7)2E9#70OPMafQTF+;1TX(X^%?YlOc?cAC4)H^P7pVQiPS zSZl;*MPo5P<2w#H1O_{e5wxk0q4|wG9IAf{%qef>6Y}6>c=#j?U!ia;CP;?dz0RQE zBwbrI#W_DO;<)f$YLoh*)U2y+l2&y&EGqtyXvK}v4u>9>5ih)nI;|b*0tO1(%E8p4 zrV{28d@P(BtI~d=${+_-Namu^{`NQ5x61Pz_GRRZUi1tXR+sVA=4*6XcGw$2(Y~f1 zc$a*ol_#3#Y(6{KuQgm|!SEPm9`gVR{bDiIJy(6~j2Qmhc(UV!Ln1bVU zgqs(lK0Be;>$ub!PkWOPZTCCL~&h+pqUYXA@z#nqlsR94Fb(@EQRtKZ!zKt#}hBNz^2^?#}h$#*Cp-yB)df_64_X}r?k4^u&8%)o-YAd-~M{gnl`RuH%W zg;(?fRTIRUwTzqCKe?Btv-OLe^f@S^Y!=>&1DjW77`uEB z)U4YD*8*+iFhbhiJWfV-8n^Xm4m>sBq~;Kb-jX(LOErS5?=l`H!^nfU z`L$9RW}UM1^GOtE6`$HIx=7aRQT+`4y>x$o=JW0p=vBX`ZPuJ>;-BY(&_p;shW5&D zWR@!gOa-)A=hIQCpUxP0^(|L6sIL!4W+^DwR{gM%Q!|K)w=WzLd%FzwQ}+H?iY5Dj z)mc?Vi6d$Mc&*3eIYP-XiMZij4WBzYqDDY^_<2)jJ*g`yOVB@dvgFfTJ1SbYXwjcb zRqL2`sT14Y|7g05`$lI7@xGDoV$_1xy^ozsFhudEv$P=!0l|qwU7lHjHzZ;rGtDmd zfl`I1D3x~Z1hq->{nEE@`6OhK`nAx7_jS*XaveAF(&;(8JRwte>w_qrb)KxPUp%Ws zh}JM}%SGna3SA1!Q+dtR!$X_Gg2~&p-8)?%oJmhNHM+wrE!~a-uu;Z|CJn8yal+xT zT#eZZ4!~@|&NZXa%Av%pI97-WmFjvU_@JqHUAs@>B6Xg}d!AX=WH^jPV4OhK!5sd#jEmhqV7To~YZs zSW2-6>Vy)%k*K0JXW988Qb|n1JQ>Q~^7f9b5%B3CzJ3_mBEAI9OXmKtVpg zCiB+_Wfh?w;cvx1I!3ey-UP#EaaJn7;h6Vr{sv-oed%SypVwWc#zTK^fJu%E4xN4I zj;|;t-p5Z5$rdEi*SH28x%T|5NivOnE`}Mg6B-=IybpWWsDfBL51Bdv;u#dF-1LMu zFb3*aftF27WNs*a#Br4X&NskL*@-p{(`DvJ*a8-i{mxw%CG3&6*^YAC?s%#yVypOp zMs;J$`8S>7@&)Mus;wj+)cec~(Jc)Z@;wo0og(Y9>%WrQEZ7yPZCwVzERwV<8k19P>Ij64@uO#%MCWfFR=TJORXT3619QoXHzLELa z%{%G$f&MdoPsiY^9l!bRAh1EmT&7IiJXEhqLuRYFnd=-6ub~`mAJ@FBGd7226ih_x zUgGl^dz%j74uK&QwdR$G5N*{}FahB?9-q(Zv+S|)^IlMungJE=D{cOGZvDq1X4zZ`YXSh6H~ zGY!wdh1gT`XKu&lC-lDiXAYkUfGIxH7vcx?h(MePde`|JD*<1E3+sD+YY6`v=Nv5F zIp-S={%N}r^rOTF!%Q9?jcoJ*%d&u9P6iWKjMj4!E(URJ8^>+V32;&XaAL}M_8+7{ z2ZNJvA7Q1W)wXnm&_4hZ7Pp8w*?y+}UKttR+nMm+isU04it`^g?XyqyLy7tmrqri* zU68dXW_GtZ=tDkKB>$i%&!$;a)bE&P(k9LEMA>*wbPBRF&1U`*RPzm&o4U#D>BSnJ znjMN>MFByaEl2Y(x=J3=j`ocL+fSgC6%ku-%zWZH)t=Y^8G&@3tZp|!(j1!kFU7E0I!rNS{kFv%+uXkTeCsScJ;c|$K#d?2Fm4|Ohv zZGCqbnF6bPL-Wb{ZP0r~jHlBYaX0|I@utg!R8oh}OCH>ygB((~}FV?m@Ks#jO4!hIb||Go4JJ8v{z) zD+Z71b`}J{vsGUX_efo>`Eky&<=kx;wAGZwE{rA}?aK#7CWw0;WWn~Y zhwN1(hNd>OMgtQ$3`0!x`DJu(6cL=SMgCQ$6bm>($AvLv(vT}2LXhr-V}o-f2KMz+ zSyHss;a=C*(=Mfw_PCE;0lU_xdHKo%yc3^s5`(@zVC1>*d9)MyG}@rJtz^$@HRn7iIkjSoSdiI|Ck6?Sopt)@hSLrV2!*2F3A;ZlP3b%v@ zKGzG#-6y;!#;(Bce1&5^-->p)%q(^H&O7_bRFQqXnpNJkZ5pjV}jkW_=nv z>C{Ca5U_}05ct=XnHiiF!Vn38_^@ya`b1Gl{rqPy1~HjUqI9OXC(!Y1$mn%77!viH z$Jtq+d&>Uvsgso8k^({~0nY33T%LYB>K0BhWq*6wrwxJ>*|%{g|&-^Tkw z&)(U3qJ6D`mu`4tX-`sm)Bm&~Q-=>=qU*ySx+=Dm>O{_PC(|=*f=Yfvw$e*{G(U-!PIkLncVwkOr;7lo_pS%TFAY17A3$s_R zOBs=qXVmK!iz4;0_#243Z;-G-J%?9q(=&cV%? zd}PdfTM}6Wi8%^fc)z3b?kj(61kg?_kR2y=41)96XD z(>bf#oGL6T8C$dk=cdoqnedrC9MGKbBEBMc>K$ z=p(n2H<+5%gCt!|TnPuQ63^rpM9CK#I^z*O%sycS2&ZTHGH z%l0gUo2M>he-gXaQ8DESpo^7eWhg4l-23KI{%~Pdf?X7Y27kBJiH_!AI>tSOLEDvdGi-GIXboJc@!BN_gqKi*R&T}qa4*N!0?R2Jmch>%2vENf$0Odi9~BZ`Jzdo&n3W8qmhq_`R)kQH+hC&`q3fR5KMe5-ceT`Ums3FSQ}+Ce9C80HstV?4 z=ulrC`W{&-Q}rsLzW6CBEh26(mpXZaChy*Z2o+?++DrWoTp?AcicQEd)xt{)eu3%U z0fI)rhxR&WR4Y=VJ@sKf)RnnYwwE~^rQZ`-q71@<=m$O2{Q zAP7XAh1=3@kUl1_J$U7Z1`WyX#UBijrsE#?UMW;QYE)X892-72W^tm% zze0#tdIoQMY4K3UU@(x*A1uD%?GGiZ%;D(zS$tc2FNaXcqp94sV7bdigGMis0^+`{JK=M5%njyd1K{*$WSM&8Rdh6}R zL%Pt!FfxM;!WJS4?aUST!1Pu;x}r!GlrQgW=O)=N?J3VsrVncsoIT&6OD`%|HZ^ULv;T&Tc4M!~8FMq?9u zNb(A;N)>XPyNuWW@CypIvK&3k@c1zOs`)2Yf4*hRYlL1gJRqVEwBN9iSy8L_v&~}m`OBw1~%t+-sD*vPcib+7qqGV>zK0R`8%o52MggxJGJm^r3T%g3{o#H~)-B}wgnKJ+{yS|tQ`3_i`Ak$*{Fp`X>V z9%rFr`=&0X!7uDtJ*;ki_+Ag6_6`NCZiZT;{c0ztMgQs|+f2l{!#3pn`r9 zZjWTm>NiXe2|jx=A0m4`iZoza>PRCGLFnMQSO%Ce{x3~m(tbvW2^p3{; zyRRZMk=Z*cBTCSi6mg}oPbYW9P7RaJnGm9BYuJ}w>qQlcbY5%K=D^k{Ne{i?&=9D5 zK$v`&td}s=Xls5~l9RfLhR5ZeVR8)tYm2yt-SFrx5Ss_R15$u!Z1v&a5=P@7%^`9=d{%8O6m z93luCc0mzfRu$Ug8B#*C+U(wx)&SlR74de9F+ zza6>&8HRPa=eX5{GCVqrW*SbBZ^Z9C<~dZ7|Jd|Hc@Yo|^a}Aaxr22e7dl!gZG+r?Y&;$8%s-Tc;WBLV{4;5A-}fXOBW9>rqvQ&Y-7X+zR5# zwhd1VT@xF(<>`-w6Wph+smOU99%N`BuMm|4fq)O?D{N2gVZF_?S+7pF&9e|viwkoW zd+M)#3KtL+cbL_M7rL)35>Qfp^$4{<`!C9(EMG*|n}SX@bN=MIs|;K~VMX%b^ZtP+ zXZOD!e*CAg3JR5ndI~%=7uxF~L|dN`bwxRC4F|pfOb6L-ntQJ-pHJ1dP^38pU%@jI zqAlKT_TfDtXCnTertIc$N#oZ{`Ll9E25_442v$^EqIQ&V689=Z`8AG(7t=@Fs9PIAe@#J2}%TG*P99HilVW1rv#wWCEzn{h^FISmSKd&&(#OA=7 zk8b30&7V>_tP(yB`Wgz7g6F~)LS-aF42$bVxd9L0s~w%gT9Je{)7L-FM5rzj!iZY7 z(RDk){>Mjql03{Ex7_XRk`Ed4vkqrtU|dsMA{stU!w1|3t@Zsp1SfAV8Srd&uk}42 zC{(jF$xTKt53$Zx`~V_UFl_U{AtqjYZ2Ud~q1Du2Z)78n&rNtvU6C!Djj`Pzid;@()e^PYxBp32iM! z0kouGfR<$W6deapU zZbNlq%?76VGQ)p!0!a55B0y)I`XAiRTt2Gjoh^OqS zZy8>|P}Rt=>PrhM2m=Y--+LUNd9{37C>eeG2_X0mQ5LGPjYn=-L!1GAO>4bUK3%c{ z*&fG7>kU- z;54O>+Jfgkwqnm7Ly4A8fPba`&&hi!JRsi^A7MhqV%TG^(8^GUQI|+H567cqvE<4A zCrZ670M&DrM)d+0rn?GCsrk9&{-^|)=bRLy@tB!O3Qa)}(Vq&ba}2;tL0J^b9}YbY znw&rdVMKZI_?&r|at%`rc(Aijbz6Ab6Ay6ewKV^j6b+xEj!^vqy@s}bu>gD}qCf!yNF;YrS~nk_hPv?JuiDLTleoA5X3+h)^4mb)nyN|x(IRn=>GuWb zmZDLjH40rqF+|hB^apzULs~zubJ;rk-vvRT<%kC0E~k7||O@OpUFE zVVWLZ!CvpjY5wjkNHqRMNTn*z!JttUejCRH$WAAVCLJAGssCtDFdFNP!QZjQ0&SI6 zQt(ahn7U;GuEaEZ4nsX%tmns5qrOdd3lrrop#EwOV9BCs^BBbgi}>9|JPX!CHFa`> zh)r=#5jbYx^{VQ$%mcWU{~G!ckShzDNsKz+{Z4_ zN4KBDZUWXKZgAi_1T$X5T~Sp?g;mb10}|=hzPZs4-MGQnkF(Z! z8Jh+~uW>()pX$mQaL?|7&=`KzrYSNDcQ1h8#s737YX5UPueg852}MAX5)vM*EI0v% z*zO87EP(;l>R10c*?$eo5{V}`%Kyid^shgzUii-!@;8G1cTtd+z~hu|qiVzc*A;WS zr{=N`;t>>VS5&I;{q7e@f5YKle^q9dtDDOIJO8Dff=57lLyw3bpWx21rd@+! zV#-b-D=CC3c)p+l27nU8>Ph-9t3jPB7nF5$p7f)c=u%dJ+x_oRboQSJ`+JSO|Gz4s zr2R|w#mFK*hWG5oJ9--y9`B2_@8!FzYB@KQ4xBP<+>(?;bGbg&isgQi@BTXMztJw~ zEj5jdr7?f?R9xR`W{H+uX3)`uy2_J8Zc=+pg=)dV8T_g_t1qX#58 z)A5eZZd1%IwD>j01VPLr2lL>T=2t3@mUn4Karv=7Q3Glh%yd4xKpf*7<9?tJ9tZ1; z8`{0yJNge;Y3mn{LMXKfh%}^Y29wc-1KUt_OCZk10|qjYLeu0Kf|2^ck|y zsQ0o9>IdpYD0fl9IDoD(v7skj4*!6T*6odyr)kzK{Lx@fV(ou_*X|vb?>7_n1C3$Y zO9aFFjfqWy1_B*zPn86MB1z*8x}wsxnsN-yA77CLr~zc;-=R-8GKmPx14(6`+_(@f z+CUC+N&dCMj@~#A<lC^AGD|T!ED^>AHk!_j#0O^Y7yaoLu5tHp@%s8;Z3;CD zFbxLvum_uZHt*UhUyg;VXDnU-%|VH_(tJ6vN`3KwgBoP#$mpVQ&l>d! z@9&~r-tZgV+t&|M%(dLjeVpj-ULze>gmnrdM*30d&tvCGf+fJ-^1Y41kNIsA z--uN6$kB2w5S%@+0nRRbcy1`a1*B6XRytM%NB+h}lG5Wk6MVk6;|S%)_3vzgD$bo? zaAHlnHtlhRl-fA*{v_`|tHytgqK^*|aY6>PagGeR9SM;?91dSVJ-_B}WLsjFi~cK|7AVhsljQZG`T7ADLO8txBR^qPxYV{u;MYkB zVya=?zA?f+of0U`?+O7x$-=LO0TTdc)<4Q+t=q(wyzViPOz7_d?1{l>}3{;F&6ZO%h!e#br<@d$Bj3BMgfd? z0Bl+7az;Q$Ux}aqgbYZ1o_LdUw_gjzW|z}9emaN>#tC)3FSZ(qI$bQZ7#R^ejln2z^mJUP93i#$DRTD}i zXB-UDQ;H^0ak4sTcb1Rn!95|RAWq@VD}cC?*wkDpeO$sU1w0pX9l#&MHfgc?^b0<_ zMX-DZEIMy$T{fNg#nt0wjpp{vw;*h8KL%}4(v42JO5IO+GJ~r&q*qxoTjvq=mHqT)~5#Ay-An{Zviq2)TO0ne|Y+3iOw_4*(>kJ$aAP%`d-OHAT}1 zlK$wEPrXWn*wS(h2+gM#Z6v&&Fi*diIQ)ig6ewDY-xhfyQOu!KJ~NE-6j?kB8nw1b zDcMenOmL2$p@u@i5nQVE?~WcqW3Hgk9TcbV`e8Hr>BrZjc)|AY?L0t6rPkQogj7jU zp(pMM7Hl|Ld|Ifd^tM*NTQ!k|eq1rdp@n3g{^F=e_-k%o36q7lcJSRpn1@sdtx1&g z_TG)>vCm4rf1^8>lxPCvlIj2^u;JGsRD8Ao>!pzGqmP3#Tmp4FP zea7LiIlyXBwk<47h`OkOI5i@MN=+|h00$Qt8J%oJb>RRwmn3R9qa+|!Ivw@B#Ak%H zuhFi9x%=r04N>>$r)u+bW0;QM82(4Z_}F+fItLtE(q3paO4&>b1V?0>&z#q?&5btk zxSWBixtZ<<*Jb*5!fp9hnMZYjFgCbx`zv2PMHITiek^-6@e)IH!8t5^mf}tk$9c{u z4O0)sqt_VcH$=g&=qHr+Q~|)kTzuW&E#k3S-9XrLDXv16(OnhW_xP1Qlw0xfLjb(z z`+|~p_50HAysZ;YA$Rl2gw&dJK22LfzXZd|P_GSo77; zHnFRL#Sb;1F&K2Ckb7VFJAr9ef324vMVT)_9((s3T%}%4elR3uq+WCoA-W&&bMNTR zT8RJ8(3WwM%SzGK4oc`!w@p>vplmEB{TIm>!^B-0P4x-KY$D}xaqg7aX~#Ia!^}_p zvsSlU+CkO!b-8+phr$*+*Z-ZdStbL|F#QnplYw~U`HQ4z)Hj-0sMO(6Awfg9r#4Z) z5pu6~c<%YSLou=fD&k(-tkkHwL?{Gtaspt*T{KfV7c=h$;bS?MmHk_%*QVK2K0p@3 zdB_z0>Fdsc7}ks5{+;i&9*b~aA?jGgeGd*#%VNIqBml^|Z*9;jIgWF$yUrW*y`8d>I})LI-Lo$vqt z=om<>78e)F%v?DFUHt^oLUk8n$yL4IjLJkm>S}7e`wd}!@&S(NFm!Tj&U%6h!Sr(bM!4Ei99OiUW| zy`~YPNcD!dq=s(e9g?tkO&r<^-to} zE~-NN-UcJN!t>21m+V|Zpz|nPe0Izu1j&xZ1{8k%T$p%+v#T1su9#xb_c0hbi=G3W z8XiJ7jKz_=9-F%J$~l-4R5@x3pyG|i7URfI8C%N_$dwY$oQrQ6p*MuV5ma1Me`sW= z?gbMi%X!DP#5)d@J+kfO+bhc)V1Lufgar4}N>AP|xHBI})H$dS!MK?;8JqZAwzE2v zNV(7RK4Dxto%NR|isD~6oD=FE%6_RIa}me{>mV{(?MDo;H_;n{&{W9q?_0SX;*!!g z?(uyB8Pj#M5FlH(=}m1Jnlb315TeD>l??(oPVK1`+ShV-aoiA47}|$GNFYxNC>5X} z62T3%-CS>;307tyB%I~G@pwq-!l$L2p+2~Q6=rG10T}C8qjO8bfF)_U0!39ZITRE| zr_1(~V7NJzDg8M&SeYV&z8FW*C_vq@@c?p0QB`&E_fLXVtVEgo$(s{ZrzwxWJ44pA zlZGP{D+Tl{<;^MSt0C!?etfOw)r5-C4-hLgxHpGp5HTb-S{Y$=4r*6ThxLGTh7rr{ zl(CJ2=vFokaXrv*7N*)OE|}`=00s{Tlj(`3I&yK*OPPtM);xn%7T?kJN<~*N0L=@o zZSk6GHQt|Qtbg5wb{5e_UA`*dem>Co1&;W!HCVZsBnRnCSrKv&nxtf+3=88JOYP+^CuQ@Tg49nb9vS%=Y4WR6F;J-#6~xyMo?G(u4qVYrE;tlqIb?fx^q~&`7$7~&2$&*G4W8EKDiT8-6a>DY!{iab zK02jH$LRf!viXoGzNs%*bW5{+8m#gmgh*-6p;+w)G0VZ6K*DGP9oAeygBvS-ixF&8 zDH_4Vrv5);)h`jt7D+`g7}-P?PUkb`%yPzA(ZtTsbqD4mlELVB0C^zTGc&s+ircK<(eFduGh z^_FlGP60Ta=?%A0bLd$&T87e;7N|rc`~QS+1TqCS10tsn3K`U;GjE!nT#&|;{ zL3_H5F*L@4nU4+OON0Oz#sMX+Wxm5k&}i>!W;}duK2wy9G`m(iKmBya`8T0cdip>T zKTm^n2551aWX?ucgTyMOYXsF=CJczg zxZvQZYqzF)8)err_x~-ZIU@%MtfsxpkWzi?@FdLo^`fFC6>xk+Uou^VKq!6w@DG^9->sdzYr; zTK4?wR4uEau@X14gX6NQpHpzVPhaRqtQ+n9@Qu^w0;%^rQ(cFMw>^heLWEpiJl&jH z;#Q=>11Uw}3Z_*5N_xfPr795rFC~t@ap`pia7lG$;DhIieSi+kmeFSBHBWE{dPm0d zfOD@}EOqk%ghZ7sx(SZeq|Cmg*{1!FK;_X#PG8wjxn|8uW3PLko@p{51d;WxBTGfl6(zlShPd{0BYQQ8e8%IVckd)pc zjz)0vG9kU{abg*T^K>05h#|-%GoY}!lzI#js}BYm_kV{a9Y9+V+%!>wn%!?NM8^0* z)+y;gK(MboXS=}il+&O36le!D2DbU-!lcjnQnsxD2{k6*wu_#Nd2u==AsT2*P^Zuk z3^`OdX}c-SSUBVZ9ud7fn1lrHDgZ?c$2eD1lE(p@VVdsna(Ek79!H)lqMocpE%hn6 z$ZIKoqC~Yn-?*4z#DB-`p{7<~crm`nb((jV*!`q_g-OC4cpOaZeD?*)dL! z6O^zRD>H^&E6e9bDWPJnZA?jEww4X)j0>7(Gc=#@|J^sSvU+(|oE-nVtM>1yQ?V{r_88_0dJhB)1K%4Ydu$Z>mUUV~UPIqI@W?lVc9X;TAv8%!~e- zXDov5TOpi=l6CL%LzGo&q-t2?_+3p)SeD(Jkug?1KPO(mLMgLAPpJ0TmfaOxOWm#!DM6#y$*r6d4A0)a?vuu{D+JWx!U9`nQRySS#C5Vj@b_2+>Bc7AW^D|#8!NYhB6OnXl>^2njT2^E1tpvF*1o100i?q9ss zzvlT;{}GA&Vb`yU=X0Uh7M81NkCOL;Fi_62FS~2I(6|s3n7IMrGX*ii>61w}w0)h+ z*rNhpeH%tu&R++YwrO2W3j&GIx+@=l4WN(ZNO_0cu4!Y28w3<5%IEp|svF_d>)<0g z8&-+hP_xah>9y^b0t$8lh!ywnny12+wIz zK$t!sUbSa)64Ml+rpnv}$8i57}kh zwvJ%9XH118iNq*_qP6I^WjqwNuWs`KCtXeW^6GDNjWzYEYaqH+SR2#$CcK=?qF>eFr5$0NqDw;DY}RZv9G$ zz#?4$wT$Qe>iDohF}a5Rw6rlyr5@%7I!T? z{Px&id)<3&yLaFBv##fLx!aC1um(~Cu5_rSfU^TQ2seyvvBO;Zes~xbT}z8>@j}Bd zz0J?zQ+x%gpSjhr>2OV(OKgcjxr9=*QZX|^8~tR2!dSPc%t&rzrR+W9% zn}vPi%P5{US4n^#e)8ww@6nm(l;3nW%PG_pf3XA)#j7J>%>X5K+-v>sG zsYMUfl_+)HB8?sLv2W2Bk(u*WsFt`{-n|{t*VP9rtlUV=+~E&dcu6qjR;ogydlb8X z6E7W56FUIV=l=U}k-$%GOoN`_2(vHN&E?3h3P4rP4bOZb1*vw5Rhd7oJIUra*Q&qc zcJVH*&rc)H|aA@ znqR)FBg8ybk^?{sK;qb(f313nAosO(%+YTs1^w7Jpy78)!9h z0PU1brxo^rhzc0pn9Yn|(e8_&&wvU;>`kKub;h~6lC6CeZX(GjTYF00F6KUH=YNh& zu`3A^@Hq<%t;D}ytSfi&WLQF<4suOCFrzVJ`TcTT?3Q$A_RG2(a%VHz_>T0&#Rsuy zX{bhax@WYsdBmTtzEEnZ3Dm%2uf_|}h8rTA>~WiQWwrbDSUMa?O^B^nPk^}o$AMN> zsY-*c!Y2(j)Hp8RfSXW7AL^zP`-zLJ__r`kXI-{EkkYc5c#`j!dPISM8bh4-bb8zz zX;xt2z{I6Jk1fiPE#-YAmhwL=Kr4|{169;Pn(EjQF=^g39`_hlELAmYI`Pq59;HSt z+4T5!0nt2+LHMBv|MbxlCAAz^9p3h?V9o6GHX^@znu**$8~!r;mT0f{50jk1N7$xR z^}NOoGW+xmo5iGddYPq{WQxnU0B427C}Y*HZX_+CXVGOLvK&zsjhHHr_OlOFvYoMi z!Q#N={SL0U*#6fB=a9IsZ0)m;6i}VFc}d)`J!>LtsS(4F*w23iY&8C8jINU2q*YHo!Dy{7F7hi)1@p`T>W2k^K zDN0+QdkStI^}n?{1k?m927xwJ!9O1@lNbpXiRG}{(HNU7sZ1&yZb`y7IL&3()}A)F zKyExMNAb{|>n_%hi2muY(D#sjz}k70BM3m-c*lUbc|*`#bZx)SXIrNB4J3E*#7c{= zYY0{@vMA(?jo%r=|j87p( zN+RXc)^4Ij>EO7GMz`0DD%t5pqfFl%x%g{*p49rsSSw=PD)Zs*=fLltufDYoZz8Tq z?+Bk3U^kU^LLlhDX_%2(S0*LNLj zZO9OeRV1r?VK*BQeY9nc&NJkWPlFH7aw3P85D5>GgXp|Jz{8edKF<8+WMLVH<7M}? z5*D@J?zY=Jdj5!5$-F?6_lh#Ic6f_|>95RTg(e8NnS7sZUE5+5i-nMdqg^jHgCs$# zT9xs>Le#D<(T-3*@%;TUm+lFW12`A!%s`>lj$xl@g#m;>Df0WZt48Poi^?=+ZH&v2 z<-*c$AdefgkhvfF?I{%1$qUm?0UQ=H=$a6@JBhpYpro1b$mnaOnNM!H(mOiPc}#RJ z_AvgTK07dwYU5X;_`nraQSjCqgeM}{DfvpCea;ek@LvI;xEg4v*U=2`nOMo(#VlA9emvV8LKufFP6IDCFzzAA{nDl+cr?sV{KBO)IaBU(lg0#rx+akd|-{jGXMo0uj}GPDI`0(ViZF(2s3RwFnD3v|8V&;IbTL zv{x6fIa((`2^Pt$RxzwiwJEy9e)YH|QRDeQ1xDwUCTo);z?esSm-VP_^Jd7opEe2epVZT>_#dW=!(ZZEH&fNqLgzaXcLp++3t0%8 zh%oroIW1Ec<4VKcRtEW?A6gKiSkpEz9tmaEI}g(-A1AV*yxp7aGWLeVtc?>g>6{Br zmi2iygfF*VE$0RW|7UtqEey~aK>t@vbbm>9U9^bS)$Hns9mo>v9K>$Maq6@|5gpS} zX1QSMmOZb?Z!Kd>VJ zaX;o1yN^z805^9U^qgae%=bx2l$ruTvftT&+mB^Kgj2Mvjl*{=M)=i0({KG8k+#lz zg~vF*o}zzSI8liMZS$to7@Cv|%DFyNSGj7KkR3P?ON3IKv|moW)#$31;iQGU2s!kP z`%M7zZ`1KZJ#aG@yMtN*HC#@u_03~hsZT0ljQ7XVr?qwt`k3kw6ry zn?jmmp&rVE*;J$-vY!EC1}vnv(Li0(0jMx|YQQsM4=LQenle>uLzG54E zkULYiE9(FLBNt-)Zanc82?UwvubFFIwD;t^faXKo{W9v@ zJ>1Y#RpNJ!?Azc%Q^zAI+o(W%{Ooyd#1OaSvevm_>jr5ZxS4c?{K)L*u>s<8ebUu= z2zh(&`fJ~YTf`bF#FgL%Lu#dbO}TNx`JybTRQkF7YI^jc-sl+3w1p=9 z=dAD-oVi_udPw^SA!)^NUPszQA)Y1N9rKhOY;d#aK1or#^-nIP+UZgHE&txhfsuSC zS2f(nuznq^<-B##1h;vy9a@o!*Dp`}`n@{~&dwUf%Xz}ftLM&6GRgf78ZB6DJ}ug? zOVq9Erz+W{o}EtXRQGjaFCkh-stz#^MX)rH2>(x>!-X6_*?0&k$+gJLYU$pd^x*|( zL*t^RW?bAmkw>9qm3e2@D?=z)3ibH44|&l{2$a#Lc`B@2)V45*e$%8+@BoJJ{ync%-4!R&oYn=@lU^(Xc12 z0sub}=BFZl;tiPS9i2M#*B0VEug{>JWk6?RSygfzui59!xgK!3vIDWaa7KVxBiq@* ztcmY*fmZJD2h=3>IUSn=B`R*UlUxjXE@~Yek3a}dA^z^kq}xp%yaN`7c{zNuag^fV z*E?zl3x>CeZ)39wiMv&yAJJNo2c9^mN$&YrK<7t+({$JWLWCq7^{*cmPp%aXVmAF2 zZ&IY+J1C&-Y^Me{fKKcWa^F*voT$*awsnIWfCF3uTm?#lfSj)6T`wwS+<|GxER$og zsMXdr9Txx+Hv!kh(B`%u2RHmsHhI>9iU9OPPX75^LiWkx$XS1Y19D8i>r*#XAH#Y?&a`U4Hv5m{2s%9^8@NsxE7gA2Hcw%f$loQ7px!DBe}?#jQ4op~6C@)m zJ=fchk1kAYsb^7kl?%ddF@jld-$OeG2<~vxH!FN<-IJUPEn^2q7#g0q>%S|CASij}N z*5Xd9yBH!^OK4Zr^qiM1OT=o_U3>KzCuE)Z?+hwucn2}*Vazv5%sCPU-Bpu&_3N@d zN)4bxx{t^FJ7+ILqv=^H%}L#r-3425j#$ z^$0~3$tp6~hbhrpo%Ub^RRH1N5;}QLD+Nb?B5HgYzN^v@zL)a$UILofFrmyvj=DcL z5DFK`rE8kvJXX&RHqZH^$*9{1-g5zxz{!nw!^oN8Z%2;Uw|l9&_lf+{+$J`(0UC3%p^MU(mnj?U~Ok*mv08ic-Xzs?cFBs{Z^(47o^4>T? z^hFAP?JA+yG=fU~aNG$isazC=_hhX&Z;6sLn&`a)nZHk_GU@ry#=9WI?meGbl}Z57 z3O$wk@_FWFEXy^QjH$q<%ZZy5F@Uv&9_%NABsy<#+2$KBv>^ED&n!)-`2rvJlLn1Y z(mrc=&G2E2=aie>QWw>Fb@sH&TY$u{QvL+8h76>z(D?$sv7CMGS}WfHD{JUHK{Ug<%XaG(BmGGL zeLtitgrL&6hD8_408Z7;Y3h`D8d8zNSl@AG*V6=S(P++pQa!<>V{@%rKg8m$N zDruG|iNJ$Op?Y7P!0NcBB{^M$nXkfa3*HO?W&BBV>)Tz7`VR*=7w&40UZUBnoUY{b zB(h$yT06k%Rw1DXZtKIlxs~HAAIS1&X$IXxDt$;U;!>_>fCr?CH z{L}+i_VFMoAJ+%-pz`?cP}k52)C0M+2rjI(-oa82)J=T*KzgPlnx0HB_vdTiFvFpD z{mZ$0yq9tM^Dk42v~m|d_Velqa}3J}nQ5Xz3qFgsglw$^b*q6XcqVF}1u&yV;8{t~ z0%I%Zv>fyAZ9we(cY`!Q6xVZC{;fok89>^Z#<6R@y!8}V6*S(nd#4d0Ot$jsu6e2V zUsE%bi=(ckq2&>*bwIaV2ES7Ee`uHi?Z9$}xppq197^0R7G@Wtb?$A6bf);8g-w6! z%GAhjX^EIN%)Q;g)J^hN?bgWJ8F=0PD7``5G+Ug_QV&^|DTl8c=IZ&M{#3m!`x(Vz@`;Ul7O2fS3J*^8i=r+3x3#XsHRc#EAtEMHkZQ( zhMOCkyF!8N%ElH#e8~C=fI$n~r~NWjGGwAZ90hz6>xdwu(e#KC6ysW=CCHNQt;F;W z;t-?oIO>+1-SMHo<7CQn)4d&CK=#K3k^c8>3<1JXa%I<9uwxn&j=f=ap!qMYNO4o-)&!v2Yn?(mYeBoucsmeUS$Y3W@H@(9lpme_YR+5Mid z-)GZ75C6?b2N;#-Ool4?Obn8tzsn?*z!*6^wnS9joa7Ut)H=(~F&*DD#c_X0^X3{b zJr>7)l`TS_rkXPsz$hSXOuFzKc#Y((+~?bzQB)X~=X>{IS3Q|;d7K{OPqhBFEh&X> z4;brZdF}lUH9r5Nz{ZANA1tk`E~}GnD>I1v+b&6^D_*{Pg8fIM*LsEvd}}KHu8cbZvwFRFrsPO+EvKj8=M8$!dvP>{8DEc;)A7 zUF^XOvaFwB=5}(m@1}b%$y+yj2nIJl#c=)?Kc$kZM{t((V+@&qywrw5+98GquaU-L z33=%_Vd}2O;)$4Kfly+wL>j+AOGa3KD7oulT%rxt_*>L}N@|Fg{;+V@yG`%6{$53s zn?9OOCS+(?=f@#=cS6f(^bJqFs&QR&*1v2<42#W4=dwMj z27dckIw06ii)*4OG5+abEZ!@^Y#t`~l2-t$!)R3TIjrML^{Gn8{VbGXKM6H*mg>W?0+d#89>@7qxo5} zkhMu6W-$b(-{lBf5?Kgu7U%sOL18$W7k&czU0sD7vPCBvAIUwXE#F<}g8_7X9qHXyqz(4;P3^1i|~H9;^AYp{8)H0u*-CN~7{%CeNN((nJ9M6KM4 zCEQoG0_s0s4u{Z82gI+6s5E?mvgD?l(MPH6Q`fqF_25PqF`P}!Y&aGeFK8i1oBps7 zQ=5s^m#=K}p`3H(pW4EIlzt3{Y>O73ex>M#TqJuGi%S5`TL$zVXHuJ{vn_tYNP451 zO#BWLk5+(Q7er*JC(2`nFMQ=5Cd9 zUy5Tovmf?O6wI+GN~v(e@Lm1TiKSI|r~Xm!p@h|yxZJry@bL?u@NSNyo@T{Z+}ViT z(8YGKU`cY%(0j3=xam`>W7J_wR3?x3vabf?y!%P_kI~Y&*d`eKvg4@UP=dTkyG$oJ zX{n@@a&~rat}68>b8psx)N2hnYC|}z-JJZa9an_>UIgzJBIo5%ui|NR`P9l-%S!1i zdcCMWg2T^0@?KUI zLZ3YLnau;Q0%zel_n3VvOb3skjLpfwzc7O>BEX5Xa6bt0#(e&w=WbPZ(31UkF&EgW zII(}P`e6sXz`qUc8KTzW)JQps!$TtPzOBpZGaQL;gq0fv224_uEiAY$13xF5XnLgo z;+>w!`bm)7*d$G_bATEPDfd8YPullg zg1esCbk}%BSuTeMh0)`%4+q>DNnLb)c>f7TY_y{26{D!VNKxj6ukJ-=twkesGup9# zCN6!HY58y_;6dn>Es-F<)Avfz@0*G-qy>?nmYB6C_!Z)O8<(c&@s!?LudfSt@V%84 zik^a|%t%Ix$O6HaC zS2UN$+oF3EAIP% zn{yF+U*{jQ*6$(5Y>NRluV8Tfav#N}v;mkPjSJ&^p-W9stMR>GAl|xAM%5j;_#4LJ zb^9apk?<&sku}%^%orj!swrlzFWW5Xr`LL=k}%RedQb5ATJ=vP4C{0F>j|9{E=xKDGuj%)BL%kiv_fFP1k#Yb;);HpjQ6d%YBMRU}=G zd7G2|=hB0wPW)k0etNRrri%Z-6pRLVx_zfs#=MpveHkRh>zYT?If{&j%YM*X@_LPy z#xBJI8F`>UM5Qj;erW@n{#2R2Q&DwgdT~*nkrlU=p(M&y3Y zJ>w2E&LUoqEb9^L^7y{1Zj%&ex%AEp{@X?B7dYEYRaBRs^LrdRUCdlk=cq0y;l1$ zmS?+LVmBYtBQ~HLOLbq#ByF!#WGQU!N$v7ow}X1jfMlDmzSOVG=L#hj^6=gb0Ypk5 z;@Di&ovHm+^`Y|5kzU)oyQrX0a~U&-s*>J1HNETx6q$(`o{0q+Z5{TvDOM?}g?UFt0U(A-f=J_}rXD_;9E1znet(9m|gV{)DC_L2n1 zmzw*>?|bcbJbyR^*v`{;Z%gyar|izQ(I-YdZ9@OJj*|vk+Fx1+!^Yi0pV$hm`;;|r z6a$l(y`eJs^QSZwT)lP%!tJJ);G&nXo?2Cud6@mr(h?ihtdj%*-UP)2_w;oX+|;1` zlezt#*!ypfzl@ir<#z4hIeW3v9_@uZ$xz~VN_?<#h*~MrruDkIUZ@Q(ARo+@CJpsx zB)gSAekXW6K27W?M|s<%PoxB0XKxtPt$D$673voEG{z}iNzd7XlyF%No-vxr_#owE zsC$1eD=|d^;w_&5z$d7tqSWCi_0Utv#{s zr62W0;3vCRdI|mCOJl}X0S)fPp-m#8^Y?BR^m$qL$ zTmLiaWoJsRzrY*O=dJd8gjEk@!6LS=03X!kr5^`Qv~A3iyj`96iqn8Qndy$X3|w_D z-+O`GZsqRD*_b)UdPThVQo8qF!X+uo9-hA&o}ag=&mOK>zgB2{qd4a@Exm9hh=-d@ z%I)}Vl)3&Bjc*F!_v7CL6wFkSbXrhnVW}NwcE90M(>OALn6XeAax=znrHAT5l40lG zKgv}iek+j1pp%!jAR{VSW8QtSwL^!sQ(b_I=;x`p_H54W-tYWSXHCH_jkil*r#rNX z&>Dov!M7eK1KDtpV2AB_z+WWniAPBM&0V~%c_x_$Fw3KH+$TE}Wv8Uv9-%Y5c=Qi0 zQOctH50a*a&^aNSWUHFnjn-7!ShZfyss3~D z{;Z(Bi`kt^WBAp}k$brzm*HAP>&*)AWH&Zg4mb;_!q%?;Wb3h)M5g2=08bb z%1nSgvqDpLi}3(I=)|ea{mAL@_W>>3%s2IA)^KnWFj_QjZhq8^mT`IMTpDc6zb4-$ z)_a({E0`hyr`0@v73<_L42$OJ@*|H8f2C_bGuj#|VE@^rHYY3aa=e%iEIE9}8KXBx z7TV$CbM?OFyY~F!Sz3Xfpe`YD2A9bprF|`#S9nUb193LAEjBGS7lo-xiDQIkH(y)v z#@-!xk1!q#WK5QT;D~ih!+4} zmJ^s2w?h3gv^_IqTEkrraN%$v1+L%%kn>XmUfX-BIF4UFztckL+|O2r!4|T<26*R zWqOy-z6if69*TiSr%Bk*(Y(8Kd5dY zxoMuHe;{c8=1&wYmfoBko|>^+RS7y<7hR9-*Svftypwt>`S(_1Pya5z*CWfR?5)Co zG;Va0ZQ;7`Q8_jzA0@EIc+^+;+@x$G_y}Q0+Pu>i?A8bKpy;3gogrifSpPjsCaVDu zR^yY1JVTG0aJOyIOn@&`*tTJ?R1n8%2~Se9=>!*pEUcjgHDI3ZxaA6N{JNKbtA6>H zZ3HK{naIwr=0iFPb_@*r+>N7-bT_x1H$HmJcG&c((U@B2m});gazihA+XSlh$G?ua zKXJL&l^MN?aX`A`J%5|BAcu%WOf~IP5Hw>=j^MMl2E}Xb3Q5wOXG&{Z9k6$9JePMgTS+IJ^x18ms6T9u<5&-9gIK&T=jBvB_>QI}^}m{3ZZv zR2^YiaNN&cZDw~s56W6V7I{P@I7OcGMj2V`?@7CUUPYv`Z^3HuOUI-A3c^YAqg`Ia z+UZxG@Lp%&&JsfvWLh}Re0JJk$GA*ErXwlluYTdZ-4CHfHA%+(3$}DIGX7fHXcjrl z4D_yHIQhM_Z6k~q-lD6>E`fMh7<>-``koxEI;X@5vUtTv>9T;Tlsw>M3&*<<|{ z#AWAIaqnH@Zss$^s-~dgT?t4*_LauZuDPbqRO9h;+4)0XYMSMEB`7>K>l5T`obFCC zX8{;1i}$SEX7Oh~fKkMte8Usb?Eg?tQ}g*za;mO5?0yaI7E+QFHy1o%n#lcZbU{VZ zNsRXne^NWFW3;lUJ~`W5e|6l57Qj9Bm(T#1`Fk&_zav9c81X>}SqpqBbQ zK3k?nx!))Vg}DPHQ@5;3twYmUYv#e&X70BeXLXbI6Pza=KeD(t{1+YP&V8je>;6Qk z9oFjY&EUU(x)tUpbdO#+Whdz0pX{yu7^1cBz zyq>T`aXpI&Qk7i4keQ!F`f5;G0~sXBIq7=-acSw=)X4jz?!>kggQaqtztCzVdXF0( z#5-RkytlU4E7w?H2fXO^-SA`iKSmLBuBcS_I9unJrB{CSj2~X|TxXJrKWe+8Vau%S z`fJ0{h9hDLLD+=h8)7S+&TWg2qQ z*%1|~y6+$JT4KAYNTTx3%w*mTgx45(9O0->+Xlg`}>j!MNOC8!#PI^+@lWz*(Wi7cuRcj_mR$@9RJ#GnFj<;gJ>Rr( zaP;4~f8&m-x4DyjQ@r~SR#{bqupfgwbVpn{1C)OQek#PEZw< zIP$eN-LCsnY5A%vc@q|aH-vgS{mutF&|SE+rg*pSmQsqJA;aC0nt0ON^VDByqjBWM}bP?@#FY9=!M z<^$9kjbU}3^HSAB&(5dnihzyBzVrf+T<{wK?_k%dA@G|f_~r-i*2dy!giER}ga_g| z3xu?i4RHQW-0K$b8wbPEV$U>p&N|hO+;rJ%w=vWJbiS7+OI~^ei5d2eXnF*mA(i5( zFAp;z&BG`xA_=f=40FdM8wM6T@Xsn{lA$Nq$(i38Mo7wgap%( z1^V&E&4LU2s+e!!y#b-w)Tg1fQ|{>Td)bLZSbI8=_wUG^n>jcCeAMrb3JBXh@r>2Y ztorlgr$<}NPtOHc?m4^ZKkvoZ?$V(gp}wT<%}j!i3y(Xo?(zr};Z3`zLMGW~SEjr@ z5A>T*UVlY+AC7rX5cEeWE8LL~l-#`d5ibb0UGNYP!dccG2{SEvK)Z4C=Y;3qpvWJ9 zcN0Ov`j9MylA~@i14sxA_1Wb?8{5vqJaiHL;AS6Ay3@4&ggm31v9AXcrc~YZ%6`o!1j1O&H3xGg zOl0nv^&K;t#bvT_X0OLXQ1m1)GNy$O^rQY9wT?@Yyc<^9&eWtGZQY(DIJ{U;h7{}J z@~5|XTcC&|S2+x{@g5Vs9v<^e^>>G?L(89;Ip;X%4$R+2DVXNNiQGFHftP(*JhXLn zaD?3uR)Khs0uQh=Zr8Y*%mh-{?<|f%R0s=MN$34WYfAe^?dw!{m=idxaM%Cn%P&~* z;?+x5P5Q>`^7??z{li6`07zA~Kd zs(2b|n@KQeBf(=oaEOq8XtKembViZa^_KbHN-qA+f+0fm- z;Q5yPuor5|LUMNZrM*%lkE6+xSe86xz}_=9tA)2)ucF%`(bVOGbg)%P{=ZJ=H<}U~ z!QG?`4*{CFmvlp@1Jl2n2~HjD?K%Nz(XZn=E*4R(3r*~4@U@!4U1w%igSD6@E;AmX z-?q36X8HZd*tFZXZ`1k0o`WpERwC0phCKrIAdM!WHU?D7H@JuzN}dyVWcuw+Byxw+wk4zVXz}JKaJ($8EqhQjYb0Dd zP4z-en%+#n>FT!(-e38IzZ5Y!5d!Jx_i7hHA5P`u+w>eZ3ER%Ft3?~sXgOOcH7tM1 zHjz6NkpFTX{i81jVK{{Nq!I5?vvr~Sm?m-ERsIP3(YfCczWsf2>?rgG$Q;?F=QGbs`H0HKj>%?QVc8mHWuLhu@y)Fyj&j<5D)hRDhpMbA&? zo-Uq74@K98=4ZS$n_aS4YP-(?%HFH=l`o7VIf2STGL+3`K1&`T=U$eqp4r#;71^Nz>zu0TL!boH7<`t7g0tE&DEDoGnyM8SeY)UYs-Ng z<_Xi`Mg#SM)X?4FO&8)TMxmVC@0`cLS4dInklr@Y;-m2+@d7Wb6frM)vUk*vdQl;P z@|%D#;r!XV{*B-#xL32X3sC4T$)egq;qgHH>~+pvw;foKJCgg*8iR2$zjT!^NL2~Q z2nO2Y_55>`Gx^rC_pBf_LCqqdD{LyMv^_P*a+jW;RNS$lJ1;y4)er)mCoU#@2Vh~< zXMz+au{P%){sdcAz_$xxQk`ffEM8$%N|Vv%E8;M|ylwU1NwVVIBjeqD-g=W*v+@~M z+m#7N=ag-tHcNYLQ6(DFW%RU+cpbw;H{Z}y=Le%6Ej6v8*Ka81Jl?OGzgS#Ryo)JY ziYQx-pyN?_I>3R8PNS+_PuLo`C(S9r>1ZfUJ*Guc!5FEqL5V%85?O*WvV+nLa-`*S`j#1Bj#mrm9E#z`8jFW64f~gj%6_IM*^I zTLJbf5_iJiEJhvV(fmJ!cXV-^-<*{#u)Jt!gXss}yHbzvns%t;fDX{A*ecx)=oWAv zeQ`OBgathG<~B&3i&}{w=~Kt4Mb-j;Hn)85_NTY-Avk6FLHL8S(2vu!3d|PQuJrVZ}~l za|@_}9}Xv9vl<0VVd5LYAre<)zd5GYOs0_ zbAUn%YTE<{i6qCXF4|otFQANw{|4#g{cRt?xqFZOURUi@f(JY@zrI&DIjMR)N@a&b#Mb`XC(`fk-G3MHiK#Lz|=x%dIb; zV1D{mA9He-1diV!Xspq+hUfei?7g&9qRq)1fNNz~_pTD1!nC_WN4z_=3n@Y$S9VtB=p9hpq?Ml4a|xxK z8$-#S8+gMvrs&>MpQ~vdDFX=FF}XB6jm3MFX_LXS2QsEDPYg6Qo1HZ&?AxLS#SNyA zoPZR#6t8yKW1AIB+^laf6lnZ=D#t?rynw}Iw2kD{EU&X9MXs1)LIZ^c^r7PnDz<)i z85^K9>FMJDH}C=#HZ`tMPS|fuSmpXT|Iz#hA`&EAgqgSAz43<~c@XvbpUcw6tUH+5 zf6m@{lGc=F?0xdxu*cR9Q+2srn$2)Et30(EgCM8E(`kYe|++gYZGcwFC zYgTN?<2>-PHW%Oc%xc&AeN-0olx8~Bx)NX;K+cO{ZfZ;*?&0^F?S8nNUH$%ZCiw5s z(Ix@x9n)W!O{%S~qkCAMpoRjjZsr$Ri}VoAEdV69Tmu0hbjJ4BQp=R+e6II_x&Zf- z3QuBVAr&tMujX%j-E3dp+yyNjMt3rER;7MFh!uEWLpbg>^W!wB%)xi*UJv&_KzCnO zM2xz-*gsPcdi6`dRuTXG57YX00isk=8D96$b&jbOJZa*54w@7U7tus`m6m@aJI3+I zxp0t&9#3KEO|BB1P!BD^#!~$E!B|OOoF|B$rWrVLeRxChD-*#9&ZM0%cmQs`N7b4p z`JJ&($=RCI3l$<>t4Jl7<7K)U_|3iQyS`ScoL1n(V3+3jHMc~D!7k1!|(H%iF$xnj! zSg?{psx3jiIoi-p-34F1e)kS>xvL`pUP;NV`_&~mYj*P!?n(q}@1@HgSjKkY#blJ) zq+r)`yfCtH5i;{W`zW;jRkyM_9q>WFXBn$yi?7>^ITVcs)OQEX47VP-1U~7_-c#fG z=xX6g#8gk^Ka}+7-~W0Zh#Nx4dF1&3jzV&8I8?}J6gwSGTY!7YU#mTXs(=|c%MIb| zhay@tHaZd9;5X^el9GlRv$Q{fOtpRy)}eneVO1O1>sqwo5W<&wF8QpjGf04ER`siN zHFQ@WmM%0^iqEu;IWzS``nFwVA%qbN{G-b6VyYBh`1uH!w@YV8@w&Xo*u)}iyGoyC zFdE*(uu_jNH>Tiau}9*lanIX-?Ll5<&l4loipzEi^c?X6TPz%!ib-Kgg#+pRRFT8NUE%D25`J zP3VI&t}P52(vlNV36|_b-af^1D@6@lv7|mIE1lQl%7ZVf z;HeZ}NFHQ=agCP&SxzFqW6@1E(~u%SDhPAkhCG^8Ocqu5s0%q(zOL4g@q4I{KG1ck zx?m`XMS&al!Vh`sebx{!7~;}NDwl^xWuj~N=ZN(wDUfqoB4)B46;Bxd6ygqsL-VCy|*JAS^ z)>!P?8y#G`{bzkIXv)sJyR&xinwb&u+(yd&{@ZIQGyl{dT|@sDzsP_5>Qd(DbEQx? zK^Sj?wTTU=Z1<5TH3)lC*Q%$Y#{AD51eiZtAl(hn*tShLa2@!BetxEbHz@bg57U(J08o=p~> z(+hDmP-@8vm+dqTJOq1Z9pgfu@n;zMhC_>q=JNc6+xD@yQHlA}G<5nN7H`_iQ@qu0 z9f@#7i=l_pj^Hb-*~w<4QiYGoeB;8SQJge9er7et_m5t`zoeP#Mil@wb*=yh$2)4m zm`HFw$JmfHvlQ=5ygO|dhx|JN4!?K<{_>Z&i{nkrMv`75LblzgxZH(Hbz60m4n6C}CL7;H8A{q5#)ZOCVwF$?aJRbfJM z{@TR*>yDvQshf>ZABs=1{M8eXeD4RWm)CiTi z4LPrTEfL2Dun6St>!eR{;-9_}*Rkt2j!Z*O{0V|(mfJOw`!2j6$RfH8{C~gdG;FBYtz~4mUHW<4XUm&|)_Ln}DB2GwTrUk+^fp zZE$oWgYNcKc&IDxw?o2&=l;^sln`h*R)QGK0Fv1nf>?BLOCOm7_8WghuO?4|;f=7W zxmt+yoibBp}EUtejZXAA}-aFw$o)mB)YxsgD zB{K1TEZ!p%sb6qL-0LH}NXfb{Y6}*iVr^w1qk$`K32;me>N@FLxISvjJ=3#&m5N&8 z#Ub>>`Tp@+qP)52H9>0m7Tct@TVsxjBSXL^`J-xX&=pTA%1#3qNl?vRU4HlI0n-P; zht=AS@iLTrnZ$oMIi@uF6U(Q;;D!5&ZgoM#Hk@Na4 z*B9LMG{<{c?X=gaL>U5(lHMW*FMnZthnaYuFuLns-)rEE!BxYmCg=o`DV48%tVO+x z;g>t45AO`{`h#WUag+$o-s{i+2i`~8h>HaIbW*-2-o+-1E7S1@4Iydm3_=r%S{s!L zm22jvGM`0&lmIWH&*cCPlV&71OGJOdzZ%%9e`kFha=oT~aeJiHPDC@s=k|dW{0Km(F>@IFQWiXu#W#O+BmU+tLBr`*ybeny zhEH8ovF>@3c-=nSkUk^?2Vk~?G=kCbgiEF@ijhQnd@0t z{PvarVnAj8BUJ=%B5nnxlg@A(WKV20z|i+u+Y-|K9>?1+&o{EISgAIDz2M0G%kh(O zkL_w;ojBEF@z$hH3}NTgmo)kQ?0Mk+#s3VXOt~kjat1e!;`}9?Wgkb91d<3-c_= zW*my|d#%EP33NCk*k0`SBBg^6Eal`DNDJbi{!Q5A=?5 zc5dtY0lK=f92je6tv+nhnmi*EtWlHKnpEOj-QFvP$mi{0@ zBD{$Y_TcNZ4oAh*p1>$MHwx%H|JI>Kn5=>SAE~!Iz`QLphHr zTR5YD7rKLS_yc^~G^Aq|&hpn@?>!uK|L@@&R+^$qPAUkqst$6d6%56+U1b8d`GhcM zDS!R!E$1tyf+=gAp&-zR^ec08b=F9@i2&l!8%amTo0 z+)sCXfh6yGSD$OHx#oPH8TSU#RtL`+;}5@g?G&Dk;U#ka6+I*mJtH?aDYja8mMfZo zYHBVRLjI`)>_syl$m^WCb>Y25rebxgAmR?2{BR=>#Z73VFpn*>+Te00b7xI{Zt`D2 zPF2id5npn?E-BHyvGspzU?nB}N-aZqPH}Htxvl*X`j) zAxX4vms#l88i61xs3t-*#qVm$WD{pT<-QO&ZDaSpe)#J4IZ)YOWV7bcV> zA@9z^4DuRC*9mQ`{xrLn-afsn+s|*{>9t6h5=waB7~9;AC)ktYC<#~K`AJ8d4VP_j zGteO78344^=FrBwiSa zz8?Qo4*kS~yEOMpw7S1y-o zA%`A?a41JBi?jz3?Whn5H3@1F%51_V$83trbRjz<2gFmitRH2!J=6+3oI4wHQlYbu zf&6eM+_C)rzi@??F93Jl)CdHf8jF~FJ#~iI&Ucj#8WNF0se!api@bM&1UxV9kMROR z^)2SMJH;Rwk$I`ej&EBj7r93fECS-L1F>J<`Hn=}{K|E}{a}wskO2Z9ozOlVd7l%~ zLVsIpk@dHBsFaPx9S^%e>X@oEtL4n^ySO0ZOelD-e#)Hwjzw+O#33clbCa!6dNEuO z-T7XI){It*=9+1>BZtwuLk!6T;>Tx=qu~#Dk1qS05XgLjiI1pu6>^ah6fXsP)4f0# z>%}*K#Yv=|!gacplN>gDPOw;Chir;`71R%80&kLSTH{TiRXR7EOI;wVEaM~I&tFNml27{ zPWXD4=8Ni{Xq0$y*}v#ocFppH0Z{re^0A&N^?{iWL2+(h1rwCMJ~v|h8g&>A?)b7O z|BIoOD90qX@Sr+Ws&Qk1Qxpa z*7(j@j}D&XDwd_9DXDoOzR@Zzb*Po#6Fq8XI!DX+z-?-O=voN0>TcP)Y(R~PrrK6a zF53+=TR&B$lUvmmpD4fOCdY|5mg6ULIlluIZaSH1fsE1E;KM0>1Y+oYx+-H{tj?pgs%7NEX#MbHY z5Ad~>O${E9nCR9WMbk?8+qSi&RU<@!H~lY}j~BMGvI@TG?5b?a_)EZwODSEmu^^0w zh#~8eD;aa{k&5}if@VkU?u<@9Dj$LGR{*wNiw>1#Q@4jl+y}3eMWh-@G>O^n8c3|! zFaJ-n)q?&eN94}HRu2lq#f`qFH0wuVm7?~x6*V^qwb!1H1uBU8%`ZdmeZ}1kUSkvZ z`91NyS^>fB1gEp97w%niK|M5LZ%Kg7><{q~kbn^b49Cy(JaVtE5+wEt665twvaksq zx1f~@*CopCL*_ifb=Np Wi(^#D%BbXs`N-uvb%yL*veqcgV^+kXK#^~qC%JchQlpiZL8(KAW=`|mUI~Qnb-4fBzUF=>}ef5eR_BZ{m0WyYo*+}P~`P+Sk5PS-VsiQFHu9Qz@GOzzlNrf^!WxaP9p7j!V&& zJwKwk1tV~q=1WQ6XY$ptFvvgGI{cmgP0dj>aJorr$PEje>pELJzqu)s$knd)$mdCn zVtnMko_2h0@Z+DJjE^71I!b-C5$KMbr_WL?Op*IngmcOOz()G7$N7Wmj&G%zV~T=f z>7PXJCxW_6zdfO)RWtxcaaBb^T`x^nwax8&#O`H0$zh~H9VPwa5zEh?XIZ6V zdgGrHw^mW8yD zhX392zc2lxF#W6M|Nay9AE-)p{}Y$Wn=pkikPx>kghnBZg`Z!c&W)$5VG7e$`4y;i z;&3yAYq<-=ck~{BE2v}ldWiLJka^0{%38Dl-D}1QEv@%&ct4Br^UpZ?x*Y15{&On) z=V+My2mA4VrlPq2z{&#X|9_kK_iqaCdtg4AeF;T3Cgwkl*GrtCFTXP<6HMv!VGHmU zPpkp?`6hOUP)xLSxyHxKPbNpracP|Uo4c%FfLD1N+1!r(tq;s21QO;LcW-ul?~YcJ zoZPBq+&##O-8KH1JS1%Fx60p<(u4gKyMMm{s7U@})&2uv%>TPl_^a{#9aDb(;oJW` zG5+@8|MR~4|D=L@k9H{oUQ$#VVTJiE*dW1IzB4vV&n#WU0^A_vcmwEJ6N4q``sU-= zxwtO;OCvoP2CDGSTY24>1I(QB*aNG!xd8E>2K{ z^v!I17(d=NDp?QjVf-+Sd&;mo?g!wWR8#sDHk(R*!UdxP&=mymN2eXu6q7lsNvE@X zZ?yWq`^p3}!=(}T9tZ)Ca*Sq+B1dcC{qd3j0_L~I=X!HUfRD-(tvLhxz)zy50+TsC zCI+o2v=hEzD#;kA>ZIykc>yjH-5^pEsXr(?KR)d<22OE&Ub$nY3Mg7CdL-Hv04)jY zH|=$Yz@n3*J8K)jbx@qQ{lf~r^J`IR=Bgtc+XH98}zQ>Oy#b3GM+ zMUEosGTK`xnC~10y`PZ)0sQLJtoVNDr8W5#H#$O(^$`r7;U%oAiG6EirePX)_i z7Q^4>&n)QD9ssO&C;jEnICnOfgd1ck8#{b=#kU$MVV2mY^#V{FgLpqpFKt14v4fbS zY-|Ki{#_*(NWVU$BrLK1N;Spii)Kx0Hy{9*SF0>LGUIok8QW5QSb!s8GiVkQhQxyC zL;eXx`mcrl*w#_QZ$4bbITCQ}TBy~`+t~x5XKM=(@uS(uJw;R}LiYTED0B;jpP7Kv zy+`KxLaIP~Aj|%>>vG|v7zR^=%E$IS{L{w^1iCfKgcDz**yCfG~B1 zoXMlc0rr}zey=A;M*yk#?(tW27#fBK^ZYIh%BKOP#%o)pyf=74r<08pfb6qNi!~Bj zQq>t1ecU5sFkXI0(!Ox7o-v3Z`Kf^Ua$XymPrt7msui=AKOd@oF6$nIwSNsTr#EZt zY^(royK*GQ07+po=WMJyWWNZ>V@=o)Uj85!%Ht)u^9mLXPJ{CtD@rppe4bgFPi$;J zCMu$~Ap5-1oft?CR#}duO+#@HuJ4f92d42rs7&jrkHK*%ZCa|$^9uJr5>9#l@vsAy z+Dc&`{F@sjfn|Z-uJ{4-H(VXO7tBB0lZ8#uN28VH!oOm`3Me^@mx^3-qRneB`dMX^ z4VnI@cfn-RYz?g)-vy$oqH=bQl*vNK4`lpaH%n2M(-{dEs5nqhk0#y7FcCU8+%Smg z%EtCV_S=xgil{e`U?9fa*uk_k8{3ZYYGrXG&|`F$sd^owZ+3td)^ZPbX?V#h{~%7M z0~#`2)IBsbdZ~XchwZq52O-|#>jR0)!dCQf=frmx>HQEyst~UBKD*#YT!^PSNr)~i(B45KSe=Dq0~qk*WRDHXv2Px z(;S&EmkHs2HP7y1Gq9sfA%4rjw>uk~|DYgE5`(Fua-vN7V*}NWR)`-@NsPY|=PoWA z;{pK|Qw(I^Xj@fT=I})FAtaN00e3LCkk6+DM+xGPzNKGNU763Nn{NkYX zjK+pbnIfdTZW4F23s{GL!{i%RQk*HE`Y>KuNXNlx;@1(( znyFPW#_R9qGAtJaQ2SibJlV|@BhC9>U-b!P_GoBlN=WnssKV%2$T^A3g_DrH1mJ=( zNr0WrTIk6(HAN;Uj*O9+Zh*S{oP#d}W}!yMBVifHR2{m#ccc<{OeK9CJIklss&!Ud ztqRkhuXXMYyGVX%vH%>6n|tmFbUkV5d& zxiBj?AH~+VC{3pE+=b^ZId`Mto|C;D6F1&r#(sj|edq!(r8Ja)mkGYWMg{;o;}_Bb z6y0T&xb#+6@oE?n$-bZgK0fucV=h9lI$&TQ2{NkVnKcFo+wqD8b`H2KQ|~ zIcwrc=;Rem?Z1+7S+Xer|4qv!cz{S@x!>c=gfwejj@PSgAYFuNGKQ$R?>s?62(z4b zHVsUs$hDh19YE^xE+b(ayaEye)}D!@8FA)?v}rUHuG9etR!S~el(KwdlW)rT7(@liwy&b z9t1d>w50pa7xX>>jG&c_P%o{R2>Pq1YZbjaX54GO04;z(Or?pHyZ0btY1YZGt(LEZ z4})u3MFs&V>X{7!A4LO+TP{4dk?u}k5sw!H_T1Gf@4Sh(cL|Z;MHp;VwXOyf4whf$NGpO>7mgmTN?O%&q4J;b`B22PH@7PzZ z_H1~(;SYQyaLx>?=(}UrbB2>f!FDDAO4546z3Ub<<19$ASPG8XjA$8@b{6rZKTb=y z#OiQxj%#`*wW^zC{QcMkp+5FY;ZT0HF1~;y;}N@oMN_c_uTvej5gV?C-kHEY8p-UP zy&b~o-4vOogu-}P4c06r`{Bt@QY(T8^Jz>6uw?=Uh_2Byh^kBp*?m2J`;kPUMVs>6 z{voZpEPeFv-1UUTb?bqkP;Y)R8SB;6gZlopotC;BLv;qaviVCX1ER#H(-6dZ>7K@l z&0Dui7eh4vab*BRhDDMjz%iqo-Y_>6MHSD83$*lHn$;(Xri@u@HN{Oo>skbn)(qTu zo75ZoT3=PfKbyEd+=~Se3gflE1HO<>TAQ~&;F?K03q~yoz0MO7j|X&FprL2QT9H&7 zqBAg6Ket|)a2aCd3FGr^u3~Z1pPj=osgaEg4`z*&`00NVoDoEIX%rm&00TQUrb!Zo zn$OI%E25^+Dfb6mMKvt}F3AvfKd;kZxvAX7XsOg_SA&;B5)Gu7c`E5_tP&8PYsCoS zs4bjARK-B*sF~xT00)5hsJX?!Vh);>a0-DpH)|SGo?n=fT3VeF!I+UO64aGO z8&Iy*SmnErf%**WOk;w)B5EK;*71?PzKANA(5B679Ulb_)J8Y&&OJne{uN%#DV(dr z(SIz=I*7MQx5ZsrfO8>h@wpNvsAT>M!IF*$oB#pz`>P-Zry!#Hi?wmP0SHLD7dx-a zZ9`!LI^euSTkZ*QO`tN|Wm7i#YZC;=9 z$>ks-4xcpRM)yTi1_XL2{UuY1=j>bnc7>{+uSm>Dn1Mzz@k%c?*IB0;Esf_uGx5`n z%DafU+?ETM>M$inKmTr^+tq%@DM5Z*BF#~@@%DC#pNV1m-A+p@81teTUQ~ z3M-My@v3xH<(>fr(^yG4Pl?7ICFohv%YUB>fm0w`5Wp*wm2;|QE9%6MRbcZlqGI$d zptswHvnJRUKVc1i(*p3+G>`@WLgh;hxwNWSkn#W~_%TsLRen|Sn(b@ua}ngnwH*Y3 zXxlVN_n79hm~Wd;klr-YM&e!@G{-+M`$IuaP1pb64W)^a2ZVkKm6Mwi_Z_bX2m-Fr zt08Eq8`@r;ynY;Yj&Lfq2u@a_^bvT%xKG9_gIb@~Xy>-7zYmFt53J?&tzhg{Lc+|%44YNq%(J!i(sy8z3=^S(*fIBfs!XRBT zYH|+d2ZBxSkH?=*?@T%r(Yv_5z0m0lR1JeZn@>YEmbTIr65s_$M~Y)nY>f!>jey(X zM`drLRh$`RQHz{u$XC3*n4~L}VimRmIv24gRX05GdY_j*A{B&AYFF}<76Z`20L9y> zPfBBL2I?@gx!vYi1ew{Ktbo!aD;3OXJ9qp_^y`R(5IK7?%Yx$-19A2p-`9IbStPaI zcEX}lWrG%d#Rp9`(&BwxCi3{B}z?>Sf^jWkl@kOn43lH zFItx*_TV=EK2M4pC*}kEGJwf$5(B?U$zL-$f?+V2B$Czjdf3lmOk_|(h<$(xGa68k z!&3ZXnJ(-8SqOA%BYl`8c@(QX$#;Rk8r7qcGbWEBe{l9XFCMv$nFhp@dlvQuq)vWw zq@`{SfxG=7fP>di(k&b7C)byaJssP(I?mi_!!>k-+Ko-^x0>s_5w}6u$wpI%32(B0BcatpM0)#gbu8_-e zYNMEKib2jJb)TqH3_Yh<0#d>Sf)fGgzr5Wb-ALPld1$LU;i}g6uFd&_SuK|t5e)q` z1Flj9`#VDc03D8LfTNQDw7EHwe!gvhfjl;IOTa1-S>X=pX1QDzz~cjnlt#s@s_>;e z0ye$=wM+c?x*x%ieF{1?>5cG|tB~L%q{P1%eu2fM7WCnz0ROPJxSsfv6V(T5=wb9T zv2J=0n2g~HXwE3jpF7D1|{H z^R*J^nH_G6o59adJ3fq>k4OY0M3JPKFie9?JOiatzjhP8i0rAsp|t9rp55{Da&}>M z0ZCsgZsjwVG%hOaBJ)ja^j&og6~lj=cdmREzcgbs2~Lh%jL={?;A|-C2w4E8a$e(S zmfKN*0h8~y)CmS{MeCtbi#)$nWuMkz^^ce~aG8R<08WT z${^yhhgudk2?H|%3~q+=4=yg}K=p*$2j!wZM2oR;H%o729qDdM;qHq~X*E5$+Ch4e z(Xt+cILic8>Z_Pwga17jXa3lW@{*c08P%POPFh$TUs6J3e9<@GoEW5|Y_Be@-V624 zVTB^;0w@@RN05!kG3o-0rt5Ul=s<`g}upTtUq^GA}i?28KxHrslfil&Fr-a5XxG+ zl~1e9CTJw*?LHgy`8wlnh7Bs5kcpJ^_XZB)ZErTYnq!73+~(d*Bfpu6D+f!H|2@BXJivHhrz;|8qmmM zm?c{ZsFz+|v_99EuqQ(dZQ=0K+peI{3?%AK$(V{=6+_mkZL)8Mq@0AYB5O|Rrmc(m z?4!oUN0d?lrQb{1?{c7=FK~4`@uuhBnn1kXZP7V^I9O>x5}t8)pzBW<7vu)JO*h6z z(Bi4@`3saz0=JH6PnCON z2>KwmPEAvG=4Cs~nup6dmvsIGGieklx;Fa9H0(YSCIu?jN^{J*&_W<=1V@ZHW+K;b zx;;?A&By_V-WLs?Cy$_-b(prC=)1t7`9}A1bOJ!D8z&;kf949|qJV;wnn>o_`&=e& z)=k|rx@p*Y^6RXFb8xf>x;??Mpx70&rpm82vPd|!NI5yB1;X*x2&NfWBGa!RzjJRL z`t3VB-<-bwYis?dz5ee#>!1IIFZ~D3@HhV{_y4Vh)xPj)=wDxef7^rq ze{=93eQ^0d_U0dR@V_P05n!rJkJ^h8in@yCc2V7w*#ra2tE4%+a8Yal*eavwqZXn* z-|%(0CXH-t?;D?nLB0x$X=EV1>AZ4=A4danW#dq92?EI>=b}hR19$gxt zP^^+Auv8)CqNJjdqo7er(@>PD%@J?PTP6E38+UhB~i$AvT}Oy zSHs7#&CV7}V)`NvAPMB%CGr}Jg!=JtFu&uoM@s&g%k#PpL z#X0ECv`L2#*Fx)w@li06mo}2<6mqkRq*19+seRKYu2AQ4ot;Y*YsleIfGc+NJ|=$v zBY8Ey^qD%IFg5B49&QB4M{i6j9B~7n<+D4?%hwirlo9<9>p)^4-MYH_K(3_;_;5bb zO4dv!xI;~0mhO<`2eQZvgCmXQqExF#K0a;LX3fn7`TdYH zbA24vYu^vxeS8NSF4Aw#ht7sN-yMb;8uqcF5gy)<{X4>`DjyMWQ`P07h3uRhDM{e% z?R{>tQ0rkRP~OB0}XeGKy|fznQb5Tv02Lc^jUZ2vJWlUmT`w<_a^XkIme-cma{8Tq6Uz9w0V|#y60^m&-<;L#Z24{mBl*ou zw%%$ODx3-gb|;As6XT79cl3o|^))rQbmdmBahbsLGU>ZH{Sb~|;woKjGpQ43PRV)F z?AMvdJHQ(ldfjCC{%OxfRPFg)lAUns;XR>~e7`T}Vo3Yz;el_0q3!B9{p{+_oo*zaKLwGO+rK_s2bv>rZCNR&e;-PmI$xWERX`dot^frM%|zP0gYBpK*gNacZfC=cCCK;q=?`3Ofa>l<<6gu zTpWkgJ&1;D_Pfc_+!>a-0X#T0lQS|oeKO{y7=H8FiO&jwO0zmw&V69;&jfh^rp36) zwi^+eyuM;efcXKtw#;!Qr+@Y*VTbKEX#;p;uE7zH!lO}YV`JlXu~9HEwOIe;bicl= zmfCYXiU}6~eyk0&i;%uc_kDPH+uG%{Mp3R=1o*KI0oP6BtTnN;96|o+z#^an-GpH7 zC@f$ttOxZw17o&kq2Ep>At9l}4Ardz9G`&x9=Wy*9ero+si>fE(q`6-d;kn+Vft=f zj~mIW5MAqGU&S29A2Wkez-%mq$Rgu_X_VT-eGfun zTMjQZ61E(2d;bR1Ox{k+h6~n~nQ_m|Vz7^>fv29gY)R6erOyW<{RIkA7$5 z)QGkMNIs;gZR&X9Bn%ufp|YsE)2(j@@kRG_fepzb5{SZn_dBqAKoBTIqna^Af}WnPc_F)?Bwe&C%@3 zFTMnDRSiyXvH_n9JXZO>#uw`R#s=3Kw)Y75-^J2p0p!g_RS3Y89$!StnGL=5y?=Ng zrlh0L#(#)lu(d<&hOLH4r>w979KcYsQVhjKXyA67?7wH1&1%z|0J*x z)jY?_;X(ttoy+an+1VLl!C=ThF0v`aia~A)J!82s89*G>eiwkrRS9f!Y7Ju}@yuF! z;LyVtL)$Rf3Ll({|L|}V+O&c3lCtq=KRMc4b^IlB;!+G_jSlD>=#DZ_Q$z*5z>qo( zNJ37L2^!A4t2b~79II-L8P<>1`C(_J6eo&o%b6u5C8gI14P_oSKoz&`oXcuOrF{X$ znqd=rJG=6Zo#J1bqDvSg>fsv~>UiRl^!=I9(Hh8`P=F-ZfMlG6iv1D)kmS%Xsv zTokuhi;!1RDlG;}7y=V@MSDP3`*V^?x39K1WpQZXFudRE-ivlV`**`_K&DuZPzTeB z8eB)lMcOp7YbFW#$Nmm$c8=2cwu?~bVm%yBtJ=Cf`g=_*P|IYs?;A`n3X-4Pq$tk9 z_C7nxmvcAK-AKdbf^O~som_Bt(xa?BFZuMLZiT2%;hpk+0gyZQK!bH(G!`Bkgh%gC2eP z^^vhZ-VLe+VmFLG-Kf#s&R=ypPkA}>9#b{gY$!3t;-grHyYnzA!uYttVQZ-+uY(r< zdw%sEI$bMNJY`^I9CH%xCW4o5y2+GzaV!N-`FH#8n zq`&%p&gB&gfC|8~e3#tg$?N#|E83J=l+5r~Wc;*x=4t%bUOe`=RYc_HDb^EtOv|K7 zZqY_0c87yeNDY))vl^<|S(X?Cv#wkl$Pe6K!yx6B)HilA{3!bqbizFB(rgjeaYYJ4 zv}x@R^!4?j0av8kMiR+PN<)WeZ!m5?Ht`ng8Myhl`_OV2;1@f;>H6B^V|eDP3oecy zJFm1O5O4MKQRc*T#B!XK1GmBdN|VN#>J`YPX}Wn9B4Trs_-5>RbDrH%5}`ZOVquUm+Km}Vc`SB1B^7l>4NIH{;It;Y*L}WSK-+gL z2~{Y=2IB?0M<-MqWO>Zb>;A8{HN7ZIQhfwb-+ zd1xYH&3I;r=c<#C8bi^->1m^#7$1<&%40QyBWlS0IkWWGyA~rJV_a@uzj-QwoPnDf zAx9&QT+J-ok+MsM+1(g0882e=arUfa(ej-lq2(-W4*E@=Pxs>++sERAb!PA7&BlC~fdbhJSnVS@FXnQ(37Wp25(xws}NB)Cgu6vRnf@ceFw^EQF;pI#rg7l!U z$(eeOi~fm^=@cw~=yAr`e+7LX85!BV{k~{?lRMfx!!a0ZCCrd)$jJxXerDH|%c9O9 zET*PMMp!H4T(MQe)p4oXwVqTFB^WAmmvYm*cWO|<-skM}M6T1Qi*0bwkVdV9QxM2+VLm<3m#tW;Vq2W8Ks%dwbyMIkzj3E2~^0DP5@{+U*dTA7_ zIs2&23*T=C)9}NiOMK%Y`;QxzJ7&Y*V*_sIJlgBd>Cd2Vx%)x$i|>&5pdM$&w$sQ7 zxlv_cYs-+==Ra>AZ*&5C)_KW=TUp50`&=hxCTpIC^j6>ddVZM^pJxZ->Dq>bR_b7- z>dkyiD-=g=nXKjK=WhiKB$p^DVAc+6f^%%Iw_go9n)|`I@uSGO9ll{6?)6Bu^gS}M zVu;Te$4bE%7gu#HEG*2)qs~k&ynXCY=0R^-&EXw(r}8bex|%Cu%gn<9n>lUlq`g%l zdF@~_V~#{~Y3WjNO&f#a*NRPFeN@qpDX%w>{d;*kb1{&qc4$4}BLU{3`k_+C|?r`a^VTfBACwZF8-*rIwVh>>qaQ;neCL z5JtE1z`HIU!%G(ditDdz-HhaIF=w+M$O83_E;Udn17fAe!dKMO$E9|&{(Lo4z7&i% z(!PMa3J8F6jq3Y^nG%wqFQ*hN@aP8BlanY&_$BS+qDXS!y1Dk}4vJipvW_N$f?EHG zw&8k2_XiZb`-P&!1ur@;ka)6lRm^Q%Ym%mWGI^y8s-z%XHM&T93+G#>8V7fSa46cw zMO86XE_6&0v#@h-l8KaxpbYX@{cKPR{d59xBNXPpBU7#$L1)9*?zDKd$S-wQY*QZW z-f)i3)BS7roNjiID58eHnhic68S9*LBGj5NSjOAcJ+gYMHy#{a)ES+Q%pUgc{B|$L zkkhs>3mO$pnyj5P`LcK5?IMBiLpz{fZ_d&jWI?6;B;d1>)aB==0ZCRBR1@RpElz`R zSo+Sjb$Z3@2Q8dhj2HLAH*SBMLa%!UyMZBPRg0v!RSC!KV5rY z7_~;ip5WT^sVhHkI9Dxfo=}7ApSB12iL}}Z8h3T)lfFQk-z6t%+>WB(+!$fB5e%3x zL9DD-z|O0it0;XiU&6c@ka`7CnaB?}qc+CS3KPI+_7-CbJj(!rmpV7`4u}*tHOcib0HxEhUo4f9Gzs8I8!{{`9 zH@ZlhV^;g3EPDe_&P=%G2A}LO4>!cZzGp^@MA=7Q?_U$r|GITJ8o8=Lyvwa-8X|@~ zA7qy2j&K45vn(o>Sacg#88i5%{mI})Y%CEB;-w%^p87x-746`nrL*e$3UXYj4@WZ4 zCI=g*&!oW`7mnEuEZ3Bh)KS*kl78*T;>v~C!K&y2dwuX1QUlig&~|I}v6F)u@Doxe zk@K$S(DM9Hp)TL3(KDg{mHPcQKhrSss6Kx9{Zd&K6=6YdDhI8J8(t~A zcqTbg5XGJp19yyXwQgtLxmij${jsp8n-x>k^yg9SKKVrjRJ~v#SrT=ykcQccB_3d} zAc=cFq@j-(EO#J_5Rv$#CLZQQyJl0GYYqv1Y?FrLxkQC=o;!#HHD}qr_cIwLpm`5| zxeL7Pj;s&#Ow-_0Mug1n0ocU%Tho#YXpiZXa}M76NG*ZF-Si(YE4n0gD4lpx6R1ka zI4HVnSUQF!1=neEZ`G|vmLoeAdslwQkl`F=l^0$W{LHzp`4-OCPPz5~GOje4WWuEQ zEQtz!V?WJ%W3Ihh94+Y5r=wUURPoGtDF<@=jS_E!f2yNYv|2M4RAMZ!6N~J$_^4p9 zXd3OD(iS~QTL4m16cEhGPubN5Vjs9Z5qDM(T7Ajjr|?GYH);r{2b1AHl;v`&(q(Tq zx`V~1P1XHSy}rUr&K-->bTv_t0h9Z5XkVh(>hDqbL^)(&N2v^&P>HDH)j$^? zoP4 zD$-2ii%*6a+7z>|kc@ zJK`FMKa%H=)^7$aeQ$V38R36Ch%jN`GV1vhM;6D*kr<_BUi2I&RaLUI?ja>U{2^)}g9fBo<$rt?==J4_vv(868qbSVgd7uz z-d01j4PSp4BL*y+QsD@J9_i$C`42^!cm~^AEdjhql9J*32!_6G3|O>AL?>gA2<}so z-5+@BjMBfiV29SX<#YPqD(2*ucjkq{b3+M2{*#ElHbgZZ%FauU6E`G>>P?E>t_dDX ztL2yr{+zgZT|csHA!$g*PQQQZg6&|Q#8&6r8~U`?^ZBxZi%zNCzfPkMV#l0WtBBw4 zKSEJYI1lsPuUq`dc_?vNap}=_J`OGM*yg3j*VmPOSAPtB6TWspy=RNy^&J~)d3dX1 zzrSPTu0rpZ%PU+@O0y@bpM^olCDY^!ka?50RJ8BSM-F<8hm7p$+0k>%D&z>iMLY`> zEu@`<6;R(8R`NI3b_x$k_))fT(c$H|cq@NJr_g%tVuY1B&MZQ%?($8sJEeoabyWQc zZWTL&^596Y-Ee{BaA3XaU=CG-!|*V~O3It@@uKZ-x{#HxLUy8y321Lboh)iqcC1;Q zP|0GfKTTQN^@M)_+e9=9y;f=eu)?>ULR`v&RWAI)z(?DG0_`oA=g&AFew{}9qsM-p z*)Gv}bh9bMiW?p|1b+3guFCwM3p6dwsL#LQE|k~k^Ezx*;`Pas$+szto1%x|{;Qik zV%_~09K=2PRrgL9O#LmB_dHvO)J%On;8$xYHQK3r;gE$EJFBgmn||=v04re0Y^$`r z;xtr~5A1b+L#pl}kLZM%tEb-IMUjy^$C94J^OWHY}?ROu!dmOjaKNrd`3v6A1*iBB7zBgMLz z#wj!)R+OM4jCY-B_k+fV-S2P79SsmFy04n9cs^FJg5mhtq<94B zrp7l`wKc1J49_moTz=bcx6@*}xwGixKz4k7OJ!;reN&;gY`);&5XUaqpGcSY=o}ZI zOmvgpj!na__}%o||6sk*g8l(i^a7vzM&}aEp!pd*QPvBtQ=f+xK&vg zjBAUHY>z9OsqdpWyYOYgj$8Z0Jvs%*7#4N*t34FadwN2(SAf-|(J5_#M#K(s`903K zlg&2T#Ml18yin&y)6YEhu-)I~9FO$rpWIzyIlwDj2fqQ>l0Q0;J4vJ7@-;M_*GK(? z$#PVRazB;25JA@Ra_L3|bj!qAFPmZ`RmI=%-OU5^CV77HUMDq86gau4aTAQBaIWEe zYN`lEbAX(BtI@WyJj|o52|pDx1as(wg8OB}K1AW5ORX#HvKmcZ7| zeZ`0rAO6#*1BT6)NZ3RpdtE_wNEvUKHIt(122cgD-TE z<|*(JEvDO(4Fpy_&X?aQylNu!heonN=&4G1)+HicQUp@c};u|`m} zAh@lHb}jX%>&BeoRJ#JoR8S%*!OW^6BvME&T*ksVa@r}J%SAn(=9oFc{{(mQ*gW)@ z%*7qgcqbvkN)AQCA0+;~R8d7^cwg*5XUw|7mnr4e`FGo*Ls zyKqbVkzOc%{F@ls@wsOapU*iMSv71vFf~dFi5(GJ2)?LKM|R!ZI}j#2@*)ZF0p^z& zMNt#lK0$l0ruxauAk)pi-;>zC5fON$=NMvT8Zs0t*lG}D7b6q$qNq7_wdkG%p6eiX z6mGUTW`*-frlILxexA$+q zX#?f(6vnAi8P1d+>~pa;PvVc7_oxLI{AHt&0q(@mHS#4w#ed@GfvS{; zrvg{r-q8Qt?=Ba?aPfNN6J+tebx5o|YXOcP-HAx| zP|y_0Q5(uRfZ;psX!KR{8a9sv@bY-m9{0QYhI3-I3|Ax}N43Zwtkn{fR|SrTZyHe~N__+>Sf zu^*P_qBJ@s(n_aUn-#xg(@fvM+~5($I{x%?S-t7l+oOAG`o4Dty+aD7Wn0hbKN$`% zEZOhbROLIo;Ox~z-#}jQE#nqqnO6Tg$ocEw^B-BNw#IV;>PL%Kcssf9cN6S&eEJ_i z&YdJTwvt5|lQ|0YYj`VsK~HBvn@Nb74XZPHk|~nx0};K+%exnFUAhE$&Ps`u_t$tA z$&KQe{A2#)5l@S$&z}Rm&EuQ;d_v+iUKhNc=w^r;p6IQPZ#pd1gRDy@Qub}X-0y`P zfJUi9j-G}oqNOOH(zT`~AGGJHZ#JudGb)p%d`5dyA2qxpGL$6o%)Hvas`RB~ggvC7 zJWlHAj{w5jXO3QazE8AP9~^$}tfsU2!p^VxKtA>p zdj|)fmNp#v%&&jNh&TSU_)zY-73#-X@QW+ER9EqX&aWJF+aLnHzD%Qgb+3$Wa z7dyxQO|THN_WQ-$;@dE}rC>Sx_Fq}#2h=@?=W}4HUs>At^li-O5hJ#dJwSfkZM_5j+2$wBdoE{M8i4H04h|ACP-ZDx z3OR{oIPME~r~bYq z=n*{4cCWI-UJB%_gaX*Pe=tH54V;==ALscAJ})vXV^5HL1d)krX<2Exz`<6$r6TF1 zBGg#LGO)$7<&wR#GhIOUS*=QD{d7>vrNoYbO=7fON%GN$qDntO+TRWRi#)!PkD@Z^N_Yh_~G&(n6qM_vqprBPTEyv+oV zf26#gpwjLrZ`Zel9ZQ)2$rfS<(}PCXR$bQ*{PktuPMs=3q;6c$gEa!TA}rS|wS9U8 zfB8L+5&S+xhS_m@C;ZpoNO}tm!1dfJHhnz_t$p%!YpE0n!12wpJOqnJ zXOqY(c9PqbN~V^Cvc~5?{>#btk8YK=TOW=OtH@liz7HPzb?DOIJ#iIXo_FdbvEQ?H z5HpmjvfC5sr%m|&Pj;V(Cp!)dux&El2P&38-UOH|UnOOvSRA;KE7^{Bba~_MY3*YZ z@g!YX%gC=o>C4U$lkC68|Eogc37USbHIA6LUl2j_V@dm7Hm+-xmvII2DWwP||Od^7?V(^7-*QpK|@tXzJd^ zx~{!_(Q2d8$hcg1pO64<;~2C$RWdMf`t|KbP{{n@mjmv|k{9E|_!^G_&INH6mDAX7 z{wSTKaOFddE%%Xm|9X+6=Z*rgbPk-$G3x>kcFFpJj&i*=!(W<4#Bl_O29E3DT2dCW zyOn_0?3t>OleFP6sDSqg1>yXi9nOxzN*ry$Wy3^;2M)W;&12RNt6RHudP5nr8lQnf zG|XpLsrL4^QtEt%mjho*o&4l-=>Z>eL!Vtj{@hWM3#x=ENn0~`hbau@E71{Rs03hs z+6fQZn8bEoQuo;?S#&)}nHw`~j9i~|f!UX)p)BSn3UrdV7{!Nfz}k(=HWq_(NfM`R znL3lLdY^-wGKQUFtK^jA_m#}gu1tTte>0+Oyy{o$lz=y|kg9Jtvx2 zC9`@znkF|D#vcb5?UcC3R^c08rTc<(=D(%R09IEPL$$48I%BmI2w5ZK8|g?;_I2thG6eGileixG)^l_+cv~MeK{T> z{zU%rWt_w`cc?dGME@a$K2iTDwJ1i&`|>~@oD32y)s~{kf_qxN@cyz|DV#B46t6eb>(w2Pi`U*UzpYX4;6XonEVy@zX&}e%anL7Veic$r&|W zKhP@~GX3|gzJ!g6>W>?S@o}UjejTsFQ3>mZX%DX-G{MGMa{2Est0#dV9(c~n&zlz_ z(nOc~5bQ~#YGgReiXxhcDb8#)2?b$|Ew5uyeuvW3S#hnU9A>=Vi6%Wp~ZxA7u3&P*bgF2FvW&+lva4G!giGXEY) zLCbyNpyDc?+mW;d z+#-cXFAl;l4T|8lHXKiXsEbhlWOgd3`(qA<>miz4ykGP; zj~W7P*22cEqbOEx$!X1mF&^^OlRdn-LLl@=Ei-Zi$JeQn^fEEGn@--=O1yV>f*Q;h z$gP4{UJFeo_;H8QUP=knXytmbQLmhFTCKw))16`{(A~+c>yyZx+37#u_GE+S?)u82 z=H!xcz>TiKu3QI()yJB}H9^{((2sDAf*4*hOwJpIpQ{+{d{k-8cbeikJ-(x_;}1DC zG@U3;W9h~NgVBvmOX+}*x;?!E8**6r0lW>)p; z?y5S~b*j$ZXCK?12(U*UqjD~+?%=u6bWM)2b!;4RrCuw;TGdvN%$~u8&TP?wuolI= z*P5|9Qc~dlMO^}}b*kM*hbeMUq$1*~Q?%3ZGLiNVq2kz9Q0*v27*?lU%Iaa#T9eo6 z<=qd$Ft_Ah3aDpYKUt;|lZgET!63pI?jkK5@`tA~^{KX>t*HbP_j z?M{^(f-uBlgVINmbzS8#BRHw&*{(m$PW7Erd&8^R@aFo|y4tL^3CET=S!i&WYlLlj z^wan$^#Vtu;YUDV;Aq@6unlKV^rSR~iv=kmJ+#=%LaT_dl-Sai^k6~h!RmpYcSg@y zZig^*v5~GN5Uyo4toDrWQT$+Vjeynuhy75(6%c~oP-{+7s#-xs~1mohLf zzOy>TcS1J~bw)*uyue${v3N-V0MTD{l&A(%EDzjos<&$1ZEWmPo=TNbzHfi&S`M>V z_wjVt{&I+M_7JzTo``MaO-Zlmhx99!3Gr3~w2!5Jk+?R%o_oIESHyrb$dse48*vN; zhfmK(U&Ikct@T(A%4CYXmb#5H%e@tGhUOu1H?-YZ!yMPUZrv8N{*8!HG(~ZH z`Km@|B-E3)mO9(f^P93UdWR>)Z`Z~(_e*3IvZAktOuY*@<(&-Pqqd;tDBsm*EzS84 ztE$AZ$%k^~df(#m`xOlVs(W^x((3j~y}+V8?BV4jOgEq2BV=#3uU`11%_)o+xZVc* zHmisEj2x^$JqPmWYpWKAg? z-#PjB;AJl7?#I=-GjoE*7H_C{Hv8bn6C=b~qOF^aJ?hu}lOWI3c#@Y;6Txfuk8g*@ zjKI?=23FAai0TE;^6v(5BXU;h9?%5xWL*>Bg82zrFRirj6Q1I zT>Y-u)vnp@e%Gkz(s7Qvc*SXYj$wL5eGR3TOH0o7q>xK0~=qrcS914@1IkSZNwl|@9e?=Q(D=0MA13r(M0u6l zEOYBhRp&jkp5^O`d;Q+~O%oW;Kf+F?+G!l>d^5Jz><`IxQcor&#THpUM2;;I{sE%1 zmb{rIirUZ^oz-^AwaAjSP;4`0Xg6o)EmR+q;J5qQH$7%20#jM1_vcF97o&omPWnB_ z5Pg0Xu4AMH8-)GaHVSvf1s=cn&#ZdN2pFxa>Z!5pkL%MnrM%vC1NVZzb6#r--sqV! zKInAP~|HoJ9acnqRy& zt3D&OF2?Yy8VBkNUPjp^%Uo!?dgl0wqELvr1q|8mNB=Qtz{o*0G`~~l$XOXwmW8#Z z(Mzo|&&=X&KTVTulysJx7lI_kuq;Km_zJ!y$VC^yq+B`Hb*Q$)Vx$ldtD5T$eZ{6O zfOgms)R&)-OrJsGAODx;@C1x|tr9#xrh zHbA8{KUf;go85-AdtR%yL%U>ajd!(4agKKEG>|VfKRyWw7Iw~5`yCVlLd$xGVZ|%JS)y|`EI-y#( z8?-|OMub0-_6?|al3;U{a|JauCtp#9z!+~76*!+?d&AbHLpG?1Tf#pUbf5;|C2!xO zNV5jIXAvf)uk?>NLtu{z3=j&5=8s{ia=zX7NV){N(Xe1`Mo0%rP_h7F{;U6$8gjk>mai5j z*Y!0Lx?WeWg!@6vLS>G>om_JOIX2Md4x^=SoX|~qcK5;Qf_DDE_sa8Fl;0HlP?*4B zVM4+56_~X)@mw^2>8FkUJ|{j=Ek3-Y@giM>9{F5@ESFc0rcV2$fa2&utY$AyNa#+V zcBm#YCoRDRYZM&~Dq=4wE1qcRh0C*1Xc!?<64*`yn1ax&?tw^ws4qJw_^}-k$Hp0d zyoAw&Rm3nTg`a6OftfIRI!tjMS65+qGL<`aSF~MfKBV~9J4(BRP&~qunRVH$g7vE# zX|^L@eiRWogak6c{{4`L9YXp;p-f*014NDWOGrTFPK2?vIKO=IymPqml|QRLqYvX< zV(CApSSN?3K=Cl*&`zOr0g?jusswSFMCKfm!Pw3ss8H;P9Fih$l3&aJ3hYVlng&Zd z2~PD0wmtpPnlHzAGv5<)1N*!`hiqE05-e0cW!Qas0SetOhxqqtlIn=I=nMLWf zUdh3)yH|Q^ImlE*V;piu|FBtO>&Cm>{HyRdjup1lnA|8y zf1`N&z7mN9xq4Nj`Y&aLbOpowHNCjdKbWjI4y0;e;3sqBN=nc)ypx4f_lU@pd91S| z26qfUA~$yZ9;O&MJCjiRmG7`z-ySg}Z(@7DO`#VwF`!ev;|}B+z#ySV z|L*p~XtJF?#Rw>$cV;agDq=75L9T1(Yhy zuV&GuudO$(KR~7UYe};!%VDrLBL}3wYW~V-FH9hw9XTJ{tk~Int-4?pth$EUEo$gg zjhs_)=ls26qx2+qp(j)I+O9OmE#<9(T!#DM>+n@qVG{4I5h>x?A2bD*TN^9D#uJT3nuYY`3_#`6FeyP;MoJ<4yl`EqEFPPFme> zWsAJVUehRR+u0|%5&OHIyndDPIX!E1a7UBVI?!kFS2>P3Xns^R_}-P*j*hv22b4I@ zA!y{u$D#nVyTL#0>5@e`Q7J#C+sAKMHvXdLNtOHn7s_&3tA&Z}?7<*bi%w zN~CNeG<3taz#AFApaXt)+XzuXJ_(-HMxN_P7~}t7U=+4u{EaM8AQ;9J+-7}zSOa*l zV$0-G8l6!?x4M|IY$0FG35qOPfs5Iov=cR1VJo1rp{!{59GjX7Kus>R-t5~!xqF0x z6dE*quNq|$^R#4`4|L-kR2yD5tag*s(X_kYF$B4|;rHDlsoKhb`Uzy^P z$Fvh#ejV=qv_+j?pkLs`+|egdit3L7!NTs45aw#I;yDt>7P_@>4PX+~5Nc?#7KEDSCwZK-FRd zvk52OJ{Q)#DO!-o3P9A^GZ=_=yd;%9Ph+nN~iGX!KuW)K2&FV}W z3t)Bj+^NrW4mmJq%~=n56;o#-IDMh#^DDful+UzM*bnxbH9fKgxyc4?2@DXw)wfQ7nUe42VMnRw)Hx=WE*kWEkmJ@FLkW0g6}9#;IK=s_9SYAZr|{BbLhZn3|frIE~UGi)y4xw9_g1 zxIv(|QY~n%bd$|z$V{I7n2Y>xd6UMERm9pDD2H?0GL}@9+4r81?xKn>9pU=EtthZu zm{{vBj3!4PfdTdHivuQN1b%SH`!m9dKCYsop0I$QIj0KE2N{Q)psb?txBTpc7xd8H zam-jXqzZVo-MZ7Sqx!Fawqs|e2^>a(Txu$o$~=7n2)!#JuE>XMJHM-OU{OU%bI^y| z=_(&V+7nx zw;zC;T<8P>wM(({!Gg8-Zg>ZomSBoi=-1!&98gadDM(hR%crL|n#db;b=iX`cfO|e&b;|WcT@p56Z`H17sELyCC=oSoSU=Cp_ zS+qpL3d>DcpMbwDvG+;IEiwTATIki-0;a~|{E{-@K@BkzS0+h|L=r}vW(Hg*+;~9C zKf);a{pfG7j?gGKD529lg-6E9cYyrucS0*oFAO>UtMa!W@fH(U`pdeeN-GB^yC0Kp zYEN!EeSKmT-|}jf)qg1MfI~Wspitr<7M7*X6+pEU$8KWhlvy;bFR&)AW;?i5&RGK<}Pe}Ey8uF#K8dk!x#9l^z4gHpIPzKD_ zLu_{`+y{lXZ45Kf-`2u@QN0NQtu`b#aKE)*`Jop|^sc_6CJ9z_QYQ#ZbjjH8Vy79B ziT0IBQVz=d$wl)z?%QydkzHue5oXRn1~Fx?-j)-S{EI#SCH`#B2!bNoovBg9FjZla=@VbKR=gkSaI(2S+1=?%H8GCfiGxw@&q znd^kpyIS!RU2Wde!9ZyJn`V66D@^O0F&C(gzNyJXJUDbzDYpAs;0m%hd@ zWiG?FD<`EeZYrjO!~ZYV`=(AY_(Y+}o_SZK|7#SGyCt@`p=%|@U%oH4etI|Ce6|F@B2D+OBdyQ{B@424e1`7*6R{lG(!iD7sTGsNz^r zz=AVe818P%4>UmX2lDen9CTwP{rtsRp2 zZdM_V>d!0YM`@kAYU*xY9_Z^yo1)^u-r3hSZK}$5`1#FlFHbUI=p<3JQO5=Pd@mb~ z5KmTtDf&;AK6UHL3p)0)*+fuj0J9-+0`(Kve;G$QaPPjQ-RZuv^$;cdk=^-ZHMo|P!h*Rnxpb3w495*=q z7`njNwgFAfbPT#Ho-Jkdw{i)1G{G8&Vj$m-CCQ(c~(VK}6ojb_rC7 zbhNx3od*Z~0J#J|Qw=zvHlZ* z>SHH%3n%ymbv)^gN3oGm3Wk{&b)Y`biU>vjMeHrS7|&|M(V;ZL);_e>-(SoM=dsL( zfP<9FIsSUlPbH>ooL*oxZ0xLR`EOWym~ys>osv1UsdWY|WVoJA6nz>fwpi1s0P;@C zFBTp9KeU6+2Db@G2Gj`#%uwU`kkHN)?k3A+zs0o9LE8Tc*SA6OUH&M z#$twp*y~=LFa3(OB7`Q+V&}YnK0-;?z`1)$EU15tnlq}XaxGkRM)Rajk}31fQ967t zp*X2*`Ondo&O@#!a(4wNTFm`T(%=Iv=ef{{?LHIxuPx#U&Ei%`x(@oKxN5e5@B+4; zEU{@9)kRyY@w)ri#kK=Oi4*qf^Btjw2Vk@R#dH(%iRy!C5+>@!_d7o|0QY=$=t?;c zMqpaKCy7va0YjklzqW!0JZbusu6wUHQZAWz9U1%LXPbmA^{Ze#wO90s(HEKl&_Hti;vpMU= zd6=-A==V5+^p9k~sMYf{6)B{teWa1vCg|$8{&XYS!Frn7I-EI|^6enYl`^b)!~NT` z1EWyT6H14awSV~dBq8CE3}l7HiN^ble*xRcrqq%-Sl>%d3Wp4VAiN&KrAIUGzs>>o547B#1Po7|-YOpmFA5=hBbt@&o3Y`G zz^aw-74(AcfppE2eOrBhQ)A)|DPg9-YI6ZqhC&!#sd>YzBGBktd-p-iYR}_S-$Z>biT=lPk=KXKZw;H0clR_-+m5Tfekh-bRtt>I;m_1%8 zB+Vu0%Ab_7H{RZJly{`>=fUny`vB)|RHKgSmZUpc4TPPhOil|drQGdglftsr9Prhm za$q*pdn>?j7vq&ho9T%}&F6_6!?QR0nZpCN;QD}Mj}pf5k}HNO(p;sGwAkfdXdR1N z&M)^-#|FT@h8!aHhMDh{=yO9>5!d3D(eqfZUUEs+TvA+>bmU_9p%7d+_!$B|^wiB< z`||lEaa0C>JFcJCpy_cd_G-&1OJU0B!PmllLioZXz$IW|gqySZ=@m3kesS3;HOsdC zx6`*Lq{USqXh$$qm~_eY}Ks^`)NPN{2wf7u|(=6sWP1oJ(21+>qm4O?p^-0fhPfAzJx zHnQB>1nY@^X{lV)yp;XJ*16uL?i#tZ8b(Mb_W9o*-4X`iKs=S;-k@iFnaP(He@U4@ z?w?tqNXkeU57G7yk~5*BDoS!pRk7s8FrC^~3{Hd-+6;q=ch;Wzp}xrAh=xX)3%frP z!+2_C4t7KP8>lDhQ#L37=zHQf+}EpP#yZzC@{|dnDceNyUiQh>lv%KI=71FP6ZoV*khOpm6pkGaL|qvy+i?VU?t!mlm=d_q zfb^u4bb`Ye{@l6Q={5be&gF|X;2inAd%Tk+v^`a`ptK9x#RwiA*46X6;(T6aClna2 z<~%E(OIi&@ftTetUgrylM)D`Gh~q6&Hl&0fXA{Ac$&OB=)>{|kHkR&8JOfetmxp6E zh-_lr*E0RvKO=Aq*-r-9Ic~i2$utgNvn?UIaItYBN|12RS=&AyG!gC0U&%oS5_!e5@*vvr-Q|bmS8VZ&6(OJnBM$S{%hPO!Jsr5 z>o{9+Bq3zhR_|t8WuoCKJ(WT-4eyic@ob~d`x4|M!JcBL66ezT=(mbFj3N#PG(P(k z8ZnA!18cvxnWNr(k8r6S)Cuf6039JQl{H6;YUP zHX1Cwe?h9E$CJ;tTXd214@#7IZYorq3DTC;35~Q1HYugwIg&9-YlqXgNW!%D;8ND! zzsvWX?FTV%9OVxvlL9N_cvFsW-Jq&MprJ)Md|VZK6yN_k+eglrAAJ z?MEX@{dAJAS z8L5DkwUqoVWGi3CCRxsGwXfbH69_%r3c8f6{WBygJa6dFYmS92(-a*d)fP50WH+i~ zzNWTTC-Zzc`uArzzwuoC1E;~oyaUAu?JLiorPQA~-+i3~f{TVt1zEQytm6L54Ft17JA8EJKp%g&l6vEvYG)P8ng3hJd5g3=h<6ul28Ht`ne z96>nREc8LV1wKG!?=k5AWhAz?yP$@dey54zm`$kz^;j!{=}B?~<*>S4S~?xCH=V4~ zk&@b3G2$VcLrG=r?}LJOSdIkl*Xn)KM&2_!nT*lMU&>yY$e3uqf)N9BuD^-xtW%bH zDh=hxl_u^5WX(@MMv$`JO2d3Z+;}act{Of@7VZW98Y`@EnCAKtn(6R{ra-{Nvz^NH z&kM#8KTzRcUa&dfRY-a|7l{N%0( zB8mWz{-9mXT!6w<4ko`}YUAY0@_TO+ZITTsIHpqNA30NI69uBA3ORSYN6AK~%ipha zGme}KAB-jB+H-#ToYLI7v@hvxv|*R!?=*x?l~sFvdo<~^RiFOg!f(yT?Vl8r4f^Zo zf@w6~1xdt%XihUrM#wR#bxq0y@I?JQ0~|ZlW{C5*rz>jfKEIkmTYi-iQ_bQT3fE0l zKcioq*gfOK&MA4hnC;&`i0ke9t!`(^GhbR^>XvbCAm`ODqy%T`g8s+>x&)UTooQT2 zRk?7($NgK<>B~40p}%dEn5ToRbw>x*H{fe{y7F#X=`lcJ@9~skAHCq*EHdr`V$YA- z5U+q*h{@cKzlS$rQE7gNScb#7O%r{k#5eHEwcr^^zVKsz|WEy#Dd(J)H7y+4YL|U73kH(m}Mu=Xa>9$QzW3N^~Qv$;!$j@#Bwg zFxj|g`9*^|>5c70zF0PP%SRCO^gAWJ|22P0$VGbqsz^iHna(TK=69#;v_6~3WpVf5 ztC0yM`n>EOdtW8QDKo_mWhQODI0=@=d(K0q>h9lcO_JWB)nvA~+`BI=k9&tT(S|+3 zwj7Audg$Al8bMee+#HqpwPK^gkXBb+%0R`@&f z2HLK4x#>vqu2APgm9h~o;hnrOmN4gW%ued(3c74 z9UZVeaNvoXZdX9Z*K%%Sb$5?tXC-qERpBo?L{b9Z44O82um;$33};yPCNO+=s&BOs zP&4$bw7$jetumbbq2!PuTErX!N+%kXX=I*kFeC<|*eckVuAp@*Dw9Vb(Gbi5etdJ= z8r7*~_V%QleL>;k+$=jUjF6b_mr_WEwtdahQzPDVyLaNQaR`teC1V|;%I~CzBy*JX zExh#7viBt4mh?|KdNur|r-d|JVj*gc0CzN!UAHFc*WsN$yhcVf-%X7<*EzeNKf>)pG2)|ndvVp}`kiyJwp z_>*pRgXPE4_UKKHMA_o%j}2YIhHkpg)7l0U+8u%{{Z?fpNO&DimdhJ))_wiW^BQVO zS-Sauuiaqvx7MXM!?BMCazZRH*{bTAw^i9eH0`)&c>L4_HxGL^B0n*v*e!j>3nnWS z;YFS#h&|e<7L}#M>V`miN`UQa^kcNA;Fe4wPVSdGcSkpEwlVSa`&~QD!W$4O7o?}q zl?XEmEBe_1Q@rmb3l3e=J?6mb@bC~DXG`5!b-|wo0j|cs3Qxc_mfv{-LX%G>(1F0O%gkg zx!iwg(W|vlnuP`(t69f<>0i5VoCGtKnJxD+-HgAhg?ofkU0$Ns_s|j zGmuLvBc{qD{4%Owb^r7E#KXgFz$)&;@|{{bVffc5i&g7%W5~=)${{n2t!q#wjZz4No5;n~VYJ*S<+lBMP=njn(pYT@`8d*S05q@XY-OfRDWS)+OFffYz0Js?usK4MzBcB(v3czqq|@c z$W;Eba8eLD z4fy7Lv_fbuLP9aJklF-XzQdt-g-Ke@0*J_MfVM0C>O+cmqAdZ;p#mfpjmlCCRrcUA z3ST7noXPdW`W7K_+g;WHKM8Ygmlr4U2ce{><@r0mbUG%IRO=BbN>0V}|fL(?M zs|qNk+l1_L{iWJe&>%M(@>f&(I7I)%bFUcCn`W7-7%7lh-R`Tb!?I=1! zX5e}aw)si?)b8YVId3deLPIPI7+tVSO|^3p*2CM)a4D%?4q%q_oSd$LR?QyF@0jTxrVwqq2%4Km zhu8j`nR$?73zxNf0!{EzU8T+nktxl&twd2T%?QEjxVj4CoIvte`B$Q8FeNOQ|E{h856vXhg2=TKXPm zfll`~n_BZ@LL0A!z2WV|H;bRHw06R^j%#n)?tVzjB=?vG{?dBxR`bZBkE%A2Mw7@@ z%43YU-@AFV@q94nqoa&pbO3AqGFB31*tIcrG#{$% z2q{kU^8tZbcdKQznSc6h{*suDx1ShP-2@e-;emyKh?P=}xmr=J7fNR(_G`XtmP&l6loPILKtV8h0SDT|2~)3r5T@7*TC8F_i;90*E> zrmxZZOSIA?sz3U2=ynM}h5D|xg;}zvju`S7QjHe0r&eC+NzjgyqCQkI`4N+#af1nT{OiHcFrRR$0X_rj4Q(#16-oBZ13OHkv_*BjJb4?XW z!B?AcR`EuDyo%nZeBo}>6du9kppxAqRzOk*gRvqdMvmTSBLgfoF_%2eY?8`rnR6|T zaX`5-DHawKEB>qIlbI?p3(TkKsyncJ=%l(q|8p1nXN3ITLD8+tFBt0@(65EL>3JqwOk`43zR*4qLAkdyhaL^Rd^w73e z{jZs>h_J}bA@WPT=oRCVjs%5OyTi&FCwTbBw%-%Fa5eVoz$8>x89V{J6~t@-lMYM$CJinx)kp8^p7{^ z&Ic)(!F8qQHjymy6zZVYOC9u%9kNkWsok7mYvrm+D{t4ujHr?-N;h@ZuM%mQ97~t! ze9EKuZEWF_G_7$a-#qc((;yQgR9VP)IOsS5pZJ^%H$P&WxAp;vt%D?tk+_BbbNE=_ z>sQyck+ZN&p7FycqonXn|Af?|OjV(<=om@fb}Jgyiq|Jwg;^y;tR?Q0FB_9;@_u2{ z3y|eazTvV0yQun-jpNS8xyH4&rX>axX_|i#pm=3;dM2~TcTk^OzE2tuy0Fq7w$OXU z_j24RUGx|Pbdue4PczUFPAsA3LM35_V)+9RTKxCS)xON5u}WD62)U;jYq~`;A16WJ z;~7~PFuP!HF}ff{+3hb&?;Q+hJc^}ktoBCgL<`m9*KhSstC*SQ5o}QNSGcCZ9bhE+ zLUJEX$Q+IPS#xSwA(!+A-|?bT zIDj|Yt4eG~4n$2Y^@h~Exz8kG%)FAa=fZRMhr)t360y=oxXSJhAPI&A60=1)9oHWY zFMid)y7E4bd{&!63KOb-x4kNAf;iI6y|pdSot(Lc<>sXSA4iM)@B0qOA#95cFtR_AQ5@t5VEs#tx9bzW>Q zvAKVs#g_^>`ql8ZRWlf~tR9V?^3#F`)8W6A*LiOz6nZ^xnN$yBG5%{H~-DW3<`)cV(C}YG>Iok}u4t-+Ut=trS z@$l~K2WChKiy3Pf)Bbx~cs9;^X5NI;>s8cu{)SOhx=~2PdwZ^z#IvI=UcxeO?f9oE zHXiHsgw}gzb}CeF$?*$((aAC?AHf~5GZ<|E-*+7k4S!xKum>i)Q(WFm<)4|V6vKtz zQXyNU)oB6Rhw>?21tb4-FLxelJqll6A~B~UqWtj%LPG{YmintpH07~*pz1BdKxbrj zGbrm?8Jb^~9s-3wCl{m_P^=l91W_32vG%%t-KP0}^G4h;L)!^*=VXbc`D^!>hgn!| zKLKi#!U|qTViR#;alWj`bZwE(bL^aqV)0%Y47ZjZR1PE$Cm;8Z+xJD~Zk| zv(YP=bKJwSe`>;qS~S6*@20)8|I&$Xd1$LI2au$I`}YnheoK`V(`d$(?B)L3JN@jM z=8Exq+Pu%m4LDQ~YYLX2s^d4bTj1PVVc$D_rq2<=|xLou4P=xa) zHn?z>NTF{zp{<|9@3^1hdw&%#+6U5mySVIEAWA<2N6YG21$H!max!aM&)t*Z&hIw6 zt5osp+^nX>OQ~^*-^)7(zmD@Kw+~jHxFET%##MKpSMV=~tBVphME>WjP34MPbU}5^ zhcBnu*wL`z_0g5+U63U!771~DM3fs;3#-1a$^VkvUGf2w?Glm-!6c}M^H01B`KjVD zje^tW$laIuv6~m#JHVWx9S@Xa+jv2)Y@x*~Kwz<-BE>4p^ykkwX~%%gI%vIj$X}{* zbH7ZwAo9dPiS_ipz%ob<+MRqS!1^sv?}X>WfE&&Kob00X<%hi|xQNvT6+A{rF|I747GMHgWYyA-`kuT=Pml$J&_j@9t`_05bj*yJ9rA|p^?}c+bIqv;; zjcpX<)3K${o72kh459}q;KoB`pYyO1qYt8RF@LXza{vs=_R_YZm%eCCVTWK3FA)w5 zMZ(80xY@m=?c)}f!8-%qU4$vfwtjetU!6vsY!j~xlwj0fB>R_blZ#=*qm}+I)6%~0 zM(&ddahdL^GJC%bS=RJ=d5m&-_y)5Adq3=33NJfIP0OPp34}i%Js+^2pW?V z^WU~#TTvsv6_9Smvlq6Z%V(#xd@*E~##_H3qLJsirtBHnkB61Wtt5PwQe^S^Q8QR8 z$E28u4OjM2<+GZuUI_h*cr)BoYc&cK>AqOZShw+224e}J2d2nzNhFAe4npRq#@-Xo%h!GgdURA-- z!LivRrhzefn?&s#`pz9dL{7k6@a6{fg%m5fmpi3o~x*@#JD@hdq4w6)C5%jsmgK zRfKu7&?fILjw2C=Ihud6Y~vjt>rycHkM-Nm2HSsQe5!TL{#v7a-hSR$VyR(Xik*cc zGj@v{5yqzCXFRnsShiUQfhfWc2zO=eUB^8d0xY{X@~pL4(2ebY`Dsv>_Y z#<8CXL$&y4KJRPlJfzWhO3B6Pv{<-=EbHS{qK9mfH;Trr&( z9zBkThIP!x_9&zCZyx8LKC_KDgjH(t7Ne}dh>o~cyA9`10SJKzU~jW4iV&_zUV*M4 z{_sOX@T9dZcHCeDJX;jgOp2gCR~mku z{l5LU!AQ8+n^x6b;^eB#FrC~6)p_Fo{>yP^9bWpI4|+k|A0!6Y zsq|<$0sB^EX=3T`E_7G1^(tkmosiAi{h#3=)yI9op-(U@T36P-|EAl~Jf1v8v4MCHFj6aDGH#KYb#sCmu zu3|r9v+6ARy=g^$`F5cf=%>4G_7z$iHvc~?!25T$o{FcOt6?xu*`77A4=HJ>{tm|5 z{Z|sH+2i9VDau{9>Uuj0vaM~_dTiS7yNOpn@~+)l0B~0^KcXu`=7~Gmm4mIRr+VMC{ZV{&?_b^TGUD$Y0OQhx`Ia0&YYUP!_`5g z>8U+9+?~-wC#K$o=W7D%WqX|6XvwOGXoHskM9N9*SY5vj$y!WIBfsh#QSxDMCAwZx zF|nqe5^+8aJd+po3Vk{$js3*%^&NufA7Om8`8yZJ#V6-1U((g8Yb$WE-(EfFFAn zXnFy9f`Nf-3Ln)HDU&tW`^w1y2(p%eUf`CGq|!o3jB*PZh$23^4rA>chjCRbl9tkB%R+OohnF?0ESZ9uha|7O;x-E!z z>Fu)E^-DCeOHACB!ok0RBr)}FnLYBk915_5D!1grorw@Qw#rflR&PsZ^d2VsPmAosPxB`N zsh4)d8t0k+u+^f|1Tmk|tw-iv$%uTPfbMGfn8gFX)4zY}U1kH-&rcblNu*kj4AhWG zO+i!*|LPEoVTtwrrhr2KEc~xe^q(K=(e@FucDio>XF|Ltcie}o_ru>jUYGn>rbZQC za!$eI<<_0bLBvv5W8faK{eZg(nwb+6cyv}EJjh)|LrLqR46IIM)H**J8lF#^i1Q}P6> za!Z`?u4#<&rS#mE`CT1~qxu0~Ti}UQsC)R-<0H!dJt5)c62rA z@Wk;&rXP(tR@a##ZMv7okUr#F`A~_7=u4m*%IR$!@dEpoFkcasifQV(sB<_o%CpOQ z#*sgV(z1~!e}Wh>F9aM+@mp0N+5Xe&ZH=gz`1Q{Jlj4+&-qPFAS->Y;SEmZY5L(^K z9s$sfEYe+x{BkSt*FtXtQVLBBz<2ej@FuQlf=5c?smi2cIpO?~(!Y#{_fMv=(B2cQ zYW`UF>5POdAD1#tT}c}Xb@fKS4%zR217H0k7? zZfkck*s5m7tVZPA(c4f@M>81!2ncEX4CbvVho`e-B}LOjSL!FV2tTUojdQ<$$Os7l z^_~B~U9HpZ-jtV5M5lYH`zWXPY#e%XjJ_}sYlrmTyC4IEi~u|I25dO@hpBzlrGm)t zXwE3yQ~PM*=DIK*p2(}p-;>qS(wJVYlofYj@W{n*5;^&K6lpY$et0CRN$K8*z=oZi zJRNhcvje4GfcVxd2M9zW4QR)zkIAi8QDK$|qTplgCA-gWR~CJuLOOPuJy(o1?y2xP zm^|9WOo$msFD{r4E>`h30M5+a{I5{I()ic!y9Isus=cZ5nY5yV6r=|SUwnJd*lKv> z{2%AY7R0>sZ+`129N9mUuE>wCkJgkh!@1HzKtX34eMY;qDw&3_z#A&mMoy;*<=#n| z?K^)F19<|D_YYL^J_vdi9RW1g56CQc(bzcE&<y?n7z@qE8KF|j;sq}e*zS@hpn#8nBu12#y@Wf^$Wnm3Sk*R?)#R81 z=-HtdV8EMEuN5JCDk_^VNl6kUQvR!e4b=b>?rS~%ack9@4;kpcF5#j=)tV{@{({WC zCewuBVvvliXXE#8af(P=|KBqgvEH5BEkbEBXA;Iw7nRY@;ii-yY1gxjphtb?P@pdq zU^46j-L zk=a#ch?po3TdNxgEqkf+{rMfKAa-VwBz9_S5IQ>gRupl2F)T8gMrJnIj)E0~gZw$F zNG8VpXLPE-V@5O$`UAg~AZ8;h+^mV}75N)!O?mD&R?HoiNk&Ubp zd2zHPKGS=riqwsGVN@$DBEQLu)*TN{SPKEI4UQrbI=pdnAITOU-`DuEf{#rRdJw=T zx+__+F`89Zy#Udzo6(xI({Hf%DZ%PUuOvGF=VDjotL9gG3fE{j7$&JA66n5awfaZq za(p&3_3y%ad1tgSLS1#tD}yOQox~L??-#QiT9P!g62@f*slhZtmlacV=HhtMM(j&c z@i_$dPt@%M7PQM)o=LryEQ}1t=P$Pgftomc?d$8=z~SATpDWie!<2l06z(~QD#`R0@zrc zRmAmH7-wq@mH&8z!qYDr(xM%s&BTo{;4a@WG(H190bB|HFzXj}cERsVvro|<=U#Xx zjid^q%#`gFvHa_4%KXaS{sn|vlL2qf@}%EG&xiNBysU`R5I%LXLux3V{z( zrez8$Ki8Y3zQV5+b_F%y=IxfeH0KO2Z!w?hDFrmTdTiI#YUzSNA8UMZU9Gb!;$1L3 zQ3ztv8hZ6*EC?1e`)xo1MhM|pdOKoQwK4@JVgvLAbie-pkE^c$i>llHrn?&{8A_zP z1dKsIO6ii8?v@@2hg3w7Mp}k&=!PK&q@;%)T44y0&hNbU-uK@B`|ao9IfumxnHV7f)`JFUs#P1@yM69pdU56`MvW4Jc za*qw3@ONz|Kv|<=Rp#+wgxKMlT<*g9e)Uk+B)NoMJ@;L_kb9uFWzP3496{cKn?W?* zmgjgHk5)cX8C=UX`?o@i8ZQ#xDo5s)bQbVEEO{T5pCtIA@kFcf@RL=uNCZ;jUt7-# z=uj0cCa&k6jbF|WLYDk~KquFJ+{wzlT7`Y1=R8If0i&QvR<@-G^%}u8q)*lxr^-Ev{16y%*BFqmRE3(q+U=?y=wi^w*(DbGz<;%cQqA-ru#8#!nsQhM6 z7Dd11wt4D~kHnRsF}9AcFn{pGU|9`h;TFpqL5aJBv#KOH3{e2viMaOe=&1K3ERbm9 zR^rbQNSolWey*=kmCh$d?;v4jIH$4wN~pLQFW@;|kDG*zM9S+{QzF)4m?a^o`*)DRaLY6U{DjaBP{E{_p^qwR{b6}5`%wmYP7Q1P~DD~PJk(Z(SC?ER?zn| z_hoO@+I-!5=b#?CfMs3*^Z5Yf2f0=Gtm{PS&1m&3XoqP5q`sI+232z$@cLJL9hpNA zg1pH++h8L)pkin(VI{6M?}Oh81Pa22zWgT;q^$VMX zG0KXiJJzWhn}DSW_CR{vX1xL*rQLMM^^ceC9Z%Ckwx2`{ zgV$XRLU~HlK-E0F(u13WXLu)%O9j7f509l?$vqEcg@1x(mBKuD=p{^}JQ=0=nO8dy z${*skWRvt_q5T!8x`y|BDO+MjJ(8Cb1hTlnH=|7#UL0J8cYniYAG4V3Scu=w(a;VzcK4i=06(38kj#Bf0R}2#jLS-7J$_p-_NezR^ZE8 zB3X!jSB0T@iKgfkT#*P2=XMQJ%=(RYC0pcPML-sr?eUgtZyL&nQIm|l&RI%;B{ zCK*kI_4avfC&4IsM^X~XHfW;FmSXm15Ec7&T9D^de;DTC3xQvcuWRC-7UN!)I@kNu zbAR17LLGDZPIi8FYE=j3#EQdb(V(7PyP&dsM6P4r$f9po*Sgv!NvuzGgHsD3O@oD zmOvjsu49IH@4P0X?f5*oQa0|i*nDiG`7mQwqB^9_hqa_0V3LM*VW5z^`N2yvX`ZcA zql2H|^tBDvr{YKx-o~5ZE)_0q@sLB>qgD#xFwPD~Jge`nfuT?#;l-`Jqsy&H9njY9 zj!o}k6~2?7+ok6aAvr!caF8J74ey%^9kyuMZ~V z0YR|naG#){qSgS$J$nJH>}-K=I9fkUBD4b6!qi-w{s6suV08mJJ5UYW76`o3xP)X! zdZj&F&73<`VguXhVIBq~7tnORJrgdU_CX25@uWX~uwV?)K;0`Li zq)7)%q!QRIsY;}h!C zhc;)usv-=ZR_$YNGG7!lE1p>0Cw8L)pB=}-aB82_v4)E`M)3|R(KzD^la5WL2mM+< z7s$vezr}p{h7pz4s@vc85NwEl(wCzZuDbWTol;h4`S`0oxI?CrC%n1h9po(}43=S3 zPSv!8iA)^uUuWN+7u{;}CUpYXT3w#zw@Kf6g-6S_Idr!N84y_B${Jg?8$A;<|B~6# z95V-H2^VPnSxr18B>j-dZ)AUV9GDBGjwM_x_)-YH^dVV|&$a$MZ~c_5@PxKf*tN2= z^mD>G>&uojHkCqUxJ!8;4m{{3gE+LG8Wo~l75@WSx((~?3)rRvd6TA>I&a~y6ef!g zcxCwlduKA3_Utom+rV`fQ^c`6f`H9>_~eJboa0iDE}v#LZZ~gN!BDIYu&_9mMm(Kwu3n0j&+~8}1 zo!%Yf*kN6LcAO_5TOL=TwQlKdi>E@FY!*m>@Yb?k8}ZlLU6?*>Cpp+L*YJ^8XFLlM z91_B8{g_9L!Avthf9B!zb`1Ks&{cDP(@J0?#_AMmS!M0?w#dkgvL%P%!`T)+I_lZb zvJl9{`c1VE2`Yr)K+1nN4O#?pj(RE33_oJmgLdvrAo$P0t{Zo0EEJPs1^M_%E9z+@aui~)^&qn-zkIgM?^W2*6>1iESLBVAG;G`; zWbUz?eign$H8DB|mUuqw#BSCD6Nd@*y1&SAWq(O)W!oQLxS~CyjgD2??&3y=7=+4h$ZE4uC4Ulw*z<>d zq(qLp9Em(C&yQsl+v^z@C>}mdD|p&VoB>ecVnZPn{PFI>X9S1A@gJI=`*)~KC>!5) zfiwL!xLh^Oe_&*_BcFhSY~E~a@A(T@Q9Peg*O2w5+SFxo5dFbHyEckoDA$iwok8|M zw5n8e9KJf&`#SWlbtRknEM^{@rwtvp^QE!W>M`lrcM~ex3p=;2wP_|tlB&jS^*-YD zkDCakN3~@(L2FG$7SsSno1>%)P5hUM&zNlZPIm`Iq~6X)^YKzoXCc;DbGi-$gOTBu zDxj5Q$i_1VSN1GT^_^2`7${)3h}8G)rSGWZR-$k!OaBr9wW~rsw;4g+Z0Sik8WB0L zJMV+!lk}Vy+J!-i5gPDV zv^0OzIZoqqgXL#=yFD+awzygvJFHWJoir1|qllj>{kyguG*0QWE0^ud+qc};SGbyG zl$8NK!N|n+(;6#{aNS$N5o6JSGe$9%ZNDRK$d*k`yfpwF-rF=kBK$B)*v_P}?_CN@ z(rVo8i5*uo7Y0#c%ivrVQ`vJKU>z74WT!p8jLnDnK>mQ&!(27}H8_VtG`Z&mET2i0 zTz%c$CJ03iH^UFAAl|{dff_Jcpv$>t^9_Z3GMc2>7IxCSMb-k0?W5iMN z(3fLD;=4g8xioXCWQUb|RDePC^BNKZw)-FjY^3$CkZCo}p;1)LerQexARfoW!NL;d zR<57cK9&+@RgZ7Oef16txz#MK+I5qJkTl5tWVn?z=QXgmvDaDs=+ZECM0_MZa(gpr zWpaJ0GyRpSI{_ufXS1KRfy4>9*luQs!{LdYTs;rDw1fSdj%gK1nku6&7;l4Xhsx~y zXiGN8Z{nlQQcH8vtY^co$Dg8F(^`^bwG|)U5484bdjd{H3d-mZPQz?{NLt^?i9fKk zv=of%Cqc|2II51Y{H%!&7*fmHEqT7hjjGhroS6$6Xq3oHH!uaVHI|MWU2Z-G%UgpA zXd@{6T-`ZS-PfKPgmzSh`h0& zGttNI9XH0p5U&vgoS$n=KBiK9pNdFKv#cawx0nfZ$*xaoy*l}r=|ga*-LY~sHDr; zbL2gsI3P&naDObXD5y9_lG6sct`g(-J>HQVdb#e0RyWAbdPbQ2feR!g_5#t45*uqm zyyU~ejWB{Z?CjWCK-CHZso<1w(&&=lJMks}49ucydT^kX3N%hZJBdluO8Tacu2i;0 z4rm|-lM4pKA%HkZfqT-^XBUkVzy?Vih)0wicgl70CpbRdBodSwprGS@s5f-(j+H9@ zyY1q>Jq0IlUfNbGz}cdSf23N8Y!X-r8*)q;iTOa)8ehvw^Ugm{!*UbbB#^}lG&LYB zp6MQ^mE&hmx{jHR>(VU}!XdD+J+!NBUIG+J1CL{)xjd{#kyB1(FbSN@&kT%7vh*>e zsvlmxQ&<_!lNv0(qBShv8Ncgbl3(AI{1U#*>IH18^2<74JMkawreiv=xw^_-`9Wz= zHLGaM&}q$S*|z$tpC8s_>%Q1TagCA8?%MQnOnH82V}w~SVPQ$BN7?O@S8A|--4PUGGs0OlF%bCXq>FYy_~Q-DN$8G&|pH)>VXE* zE~x3k#hoHRidTPbsCyFVb^i)sIijB-kW2jFtxekbXlk{E!oDBUR;*TAC0vh_&b$k74d3b zy7yQ5zm^`%S+R_Yb#(QZ_?!D0v&##6AV+Rky5^#VsY|JNRgpWGpTSM*_-CLK`^|K{ z>XKZi1tR%6h&vmJg3hK$bs%kZKRQFoB&bzi%wPV&O?1&1vlKnpD$)RbpasZRdFnzB z1WHDubyT}clsn&BT6Jme3=W;c&1n@FWVT(AdyiNc{WXPOFv*Y2nXsC59klpat!g&z zY5i0ls#E|d<#QuPder$2#b`wZE5`)HnB}Ir*a(Zfb+-w3aJ%V~&k)r38w+s1ot@N! zEc)~*qLEqjHAAhV^Gw5(QN+uZ&V3oUZDZ=Bq3j`%r*C;68QflM|GNsn0<%4uYWO30 zqSPK|8DZ-qS%t;)^5>hMj$RARlbSBN+5E7JSFXA2D4h4%@U2g;)4IRo*7@1JGyl;L zr{rSo6Tk*{h?zx6{4h!*4eNNkS(?8*w)3IshetcCoez|W!)Qe+WeN`Mcw%7rZMx4t z4R^C1r7&!QvPpcaf1EgXmG~k!5gxtQVk=z0R(1NuU_U&phrzNvXL418x`|j;+3*3& z&&lUY9J1P>MIWNL+8`EHt4grBmZLUc`{8E;i4*;#7Fbdy8YLvU}`qR)e zX-#ktsum!q^3*Q|=A4&g@%v<&u>Bo9<%_A~c&AZ7_1%6KWQ1kZ8~5$Sl)(2f4y2PB zRLupmh~lVXyg2{*%%_!Fx=NG`1(>O(L7NN+OEH}}e5-+vnR3Hm-C4q1! z!^Y$%5lEXJz3I%8m44JOY37?rBWb7XclL?a$hwt9YToatGKC|BZr-v#Ub~q;cIb_t zBlLfADz20^7Pey9i-=bBn3>Qtx1aL`x)Ckh2oDX4h2Fz?>w(k2raRdq_AZ>*sNIHk z=^XV{V!@j#>o!Zv{;hEy&q9DCE0d0W^4r>IF3T&jnO2I?P+J^Gh-;I)c@-mOxd3ik zoCV+xZUedWLiEWp%bmB>k%qvD^1aTU`MTROvv^;54mf>|0UpKdPKC#MEb?-jNk z@EySv!NNskK^;P}L3BQ*nr|2*y1j)yFeG=w^&6Wi?|p-9D~+8j!k6#8O4^N10t#)-V(;Az{5bM8DZHBr=$GRDk>c0bYs_tD3eSJ-&!R;3^sot_IIygxB z)eP3A7LIp{q<(`9iuInG)R+cKY}MBzsKu%P=imE(feA{4-4v2ig#Ctn_Wpy7{5Ou{ zFQH<%iq4Bs7w{3lyt+yPF0Q)hk2q^mw8=XWI`G=A=yt9Jl1!=~8JZA%Zcw!}FI4@p zB+llQxAkK;A=(GW^halW4-QSN^QlG0RBx>{_$Dk_Y;H%8A`&ZypGjyFC!;Tw1^|z0 z*=_8`ym0q1BGmf%nq`;ttx?)pLELN7mBzQ|{!7y3{dq6=d;v7sFH*$VVD7<6TYJQgX z-~_+?ma=hUcVTOYb@{T2tL2!tzBtTigzr^A9Ky!OIkem-O z5ZVop-;g5})Xld43h=3JD!_P^$Z>Dj%$dushMiq^kkTebu+4l6p`2eJQ&moD`{sv! z2$*2wBZA`{U6elne~o?dr!GJN|G_>~jh4EJ?u^%2;0C!=Xgg65( z?LNO6DbFmqYl*!St;f`+ZW^c<60x#k<-*r#5+jHm*avE~rhI-}qJBSz0;D=^+F3!N z=GkleL-8<*&V}vO9yV`QWT_}X*FQ$`70C2@E_Y7HCaPaS?k>m;(g2C)zga|6AQ+Qh zh475aM@y5Mr?g}Kj0bqG|LnWNgmvKkzB2GY}$Qz z*F@@ubGb6=%vcZ?0*YpSeN(S1=A(r1ek<2ccb_Tt&&s>M#O~onyUSF%8eLh7KcV5# zP?}AVH{E*@*pnY|QtoT#lJY%Zt-Q3RA8wOODN3C};(3&Y!FwZwk!k|w*T zS3O|4H!tjs0u-o^y;cCV=6{e(f8Bh1H{aj3Z3rnf7QP5s4`JqLD-UNzN$V*5i3pMV zX@rwQpX~p-UVtz%f|AD7L+PLcZ@#QS(44G9IVYGtJmo3eM#FJTGCcU4_bqmFCDCOM z?O`$EMW$JY%afYEAC)R67o8mlw*5`_C-mB3bW5Wvc5qz*V+Wo~53#JeCtjC416lZR zOY9`_L8<7CKbq7!>29SaVc99>w;yb-4bTDjJS_YPn_B3PZr&a~bN=TOr<$2A+K*-( zrJ$_EjtyD*{6n>i?|*R#f8CTYK>NQV_8>qUlorT4NGzZ!oeSd; zR2+VR=(ID<*)j_VQ>#LEmXfzaal6gS`0w$MJ=Td%5Ll|Xz5cWk5^F-t(qCo_@k z9`@@pEfVDe{#LfM>OrYI^Jt=0-wZ9$aOJ$>%dVp)&w$4?d6fcP?>pw!FRpV<@5EDS zv2-^nbjF%>6OZVE<>A`#0LRbsG_aHS}X#}2Q-%OZlIYFV3Fng?EeCqF8>#}BP( zcRg-qf@fdSgVC#tm=;t%DW+C?ucM95`~uXf0IF&A0)qNWu#Cdi2!~W`e~KI&&3Mib zrc=mp@ph-{AZ+=HmyS)x)V|!#9$Ynuu756azxyscng5||KnZ5+Van4VrR10n&hqxh zdud1yV&E7oP30Yer|&2%3k<~;s;V|_pTbWsHj{i#Qc|5u;|ion_h%~c7ks$0E)$A! zeobRbZ{wMjyCcm{4N}8CTZO)MXe`pY;FqS*3BhmayQ;3=C5aSy%ShPiwm)~u=Fmpr zc`F9jGNOI$>aVCt(HK-dH=UHiHeEewlxab^<{(&c>jaY{@%t)0RY`(;z{HSP>r7DM zLHX8$WKrSayxIfl*Y$E~4<1mlv!?yRJL-GxBhw^_JpMwJ|4~2y9~MWG^mTi({}G1j zi-3k6Ity3-bKfwchk7=RGJzmRcPguk$Nj z$!d+@gAQY0?F?TBkwz#n+Hn5`*1$UG3Ba^r3$y+MB$9Rb`)op%vPp*paz>1v5t5*|BujxLYSZEp$@yj zOQ_bDs~D)ORTto2I2^Da7F&uS3$VzQbU(^yYz9&|NcWHNYFfJw53Wk3IoV6h2piAu%KFl3MAJUYn@~f~UHjaW@@(u>c{*4<3qGbtm!?P{ zX@}Ts;-_|fMzlSTaMM={_}br)K7D&C!l!kolT*5m$TD<>qzjkP8^ltE-*|Ewy+2lb86*OA<8 z*}TTRUt}e8+SOzE+U1FOG#4N~aAXng7W}#baFzi(2noxX1@})IE7yNCnGBEj*{0v{ z+3pFXILy!`JvfwWWu%gKo1kY`z@9c1t8p(fg#VF0k#pdaU0)jVRhnsYq7d`DT9Wy$ zd-I}-9JZ~gX?(x}^1d$>&D8S4tqgWCAr?eMe{_soD9>OjOb!C`M>%^I*Q z45pCYJ*~Rni?7a0c2AR`CrZ@7-uJp}LUB+j7dtn$|J;s5)&lqBU5?K4DN67o&%*WmLtE?)gicM(?{JB46~rT#2EN&T6#Ruf*8{*#j-WC;05xs{v&$T))*S zuDn6i`+NxYDp;b+1aOiS_3BOi-xEz%`!!Kf?yLnki7Gj3;Ew(=3;Is#T>iVe4ab*q zcJd#uNHDB4iFm7txF>yv?+pQP*J;S5!XQCK#;a7tlP0a7A_KNR!q@FRpV$4$&z|Ae zBFQK;4#f|=9Bon|smOGY=Zk3Kj0&`c5^KOFT8UfFkZ{LB`S}Xs(c3ZI1bz92j?DD@ z0=M|9H4~K*=^y)Ks8)?(Zvy~5vVJl9{O1_8QXAIDGLPUHFsT~%9c4Wnd{6_boK7|{ zl=tzFt-m*CEXw9c(Yn;JFz(0kuN&LcxTi2jW|VMP$kL(zrZkIhl*|Z8{L@v}1?DBW zlHwf_9^l3`1EFuqNiME!ZCZ;sqg)pD4; zLk4;^(O>V6sY$J}%f&-*VPLK)DBmW`YdThq6n*q?VJo75l)4|bEr8e;YpC0HeIqwY z-#rScu2iW9wDH8A$p7H<|An->AEy+0A!*tg_`7&^n?=U`LA%j6i?wI7d`R_JjtKQh zbGbPRGd6h*a63K$b|hpU!U?;`q7sGUk+CdJ6?SJAuEwUYNtwYbGY#4+A2TB|4KQ{xRsKkZ8I2l<-Id(Ek# zf2FKUjGgQcx1>ot30>(>?VQ8cr1y`@Jk*nD)AqACSE|8a*sRN>NOmUbU#?)5JBK~! z>qF&?TYJXIdYg7ceLmj%xRKG-jqKc%F?IfOe~F1iZ;N|tp{m?RO@~&mc7$K6T#?ou zQL~Id7N7^KME#^Sa~)>l42SbVHGa;qPn(PB*`=!bFmSJE?QiEYwqD$m6+*!;B(fg zr(TUOx1tKd*%(P00FQI!r$Zag)7#JQ`HS>mp#d%$|E?Q@FY3ihHuB*^@%Y&Ra>&R3 z283F_u$$wM>%|M$nVhb%;rD`IrS)5z*h+hTfGMTh6@aSQJf+|gD;oZ!U&whq`%@Xd zjuS1)8RGNu9c4AVg{rxM?@@CyW(AUPAieHGg9?B7|6i6Y>s1m<1fz3RsEcOd7~$D- za614sJH*g_f1N9d&z)Dv=Z!V<=!%hx>6oVPaPl||AS z&GmnHcmSrutM5ncN#x1@eCTNF=|g4eCSIg8XG3yf>4tBEo~6?=iIObkO>IGu$5$Z_ zis%aWH>%#}&!nhU`CaD(jm#C?;upTrX_E8)c$WIUAT~urU;6 znjIBH1VcC<=D9Qi`M|*=!@8CYb;Hps0%|N&ZmE$96wzKror`a~lQTQF>(c78I*7DG zxFssIo)m-*X58k;(xy&|QzK}GeT7T80k@m*%;svV7Z+k#MMszH!XZk*B)p=KS<~G- z4F$)UeBV%wH}o24v_ywH(zTzq=&VPEJTe|a>pdwTip@lvfNP(cuScv)vRt&Of}xcc z)<8c2_OvR=UlS!dl^jU{C)-myaUYKR{_!Pum_Xy)H1CFR+2fUgJjB@ z)uRDxD>Y*9K=omN1|f(KLsz1?*ojlhWBZNY{A= zxb~azght2m)C7u;6?Z|2`hMzPDjxxcQmnQb{SKTf!ay?v7ot00yVscXodATE(}@^% zp98;!xR`nWFJ6wk8hg!`3ahdJ@Em}VnjQodU%KRWL@exL-Zuj# zjL1@r={a#B#!xO0_0VaBAUxVuh=^cGeKmsB5t79HxI1>Ov zHE&lPzAzmAvIRRS9r-wWfPUC`#go?H>2yvB8F=TEt>%DOzRn?Pn8l0S=DD zW9{N*;Dq0)`5O*fs*KF1`#?`BP)1fv-?Qw&UQ2o{2)payJnr1 z{xKbU$tI@;xF@jlYl8GwqhYwGVdkit+jFWMopG6a_;-i_9iV;seb!r1&77AxN^-q_ zULyq1A`0EnE%uCw`dg97@UL%FxU8HSq!VM8i%08h{U5zLx)u85Mn&Dps(L?LMpR7GF9>HQHBSWLktD=CT8LmQ+KXS4jb8iz4wC5#vr)$6iJH)e zR*@jK5_UKV@14>SKalrxPK9_iF01{AQ(X_4QO>hxA7?HQm!N7BPO-AyJrIa6uKyG0 zP=vaaGUfg&74V(derwPnC3Vv?aG2$u(p0KPpM5hP>-F6bhG!X6Rwp=t4q*G&69iW= zL7A1C?o?X|NC2%V$nzY^<2=IcpqTD_O0#zL@{2kqm|dRQzp?0Gtt1Wr?UF{KVKZnQ zoebMVJ$ZYAaZrWv!?{m7VQ;wh-m-Z}Ak&|1tu9IoUW8HtsQiL(p;k5ng=vmk8sT8- zkzfLXTiu5C(rrmA<1gQ%xvWzuKWYusMM5PRl0zW@kQTjP25-J@o1>E+PDO*fzd`Y6 zM5C#bc|p~&(5!o)TRzYXAl0k*x|n@Y|8g|}n)7BY0Y_<@{)+y=Kqm6C2=^Ff$1DK+(Qhn;Shb(~#zYCY z7qD%;ybg)DS-eYyQylqf-{0nrtxMF&t}gv0HUcf*jHU)=SAkiZf&C3vq4#?(4uw%7 zANZ?g@yLMSWf+dDp9E%!oMI8L-kc3!VLnPpY<3o!NDo}>5d}Kly%LwGNgQsmdnf0` zyKu%3a_l2}bET603O6i5XmubXqBya_1L;aZcsf+hAwhogTb?WvFS5^JI`_A%uL-ul zCFRKcz_^=!$*pMPCGJ_E{$@L42bNAaIpaRTPG9kIr2>|JYhAIL-55zfa{;DzVtTfYG3Uzc(Z&gl~}*>n0i*kxBwo96q(0LBD9>+Ct^ZcW`uz_xO< zyJERNCCDK^mXzBz^EOf@X1Fvo&dS*37 za~XC)9z!gV8um?D^ZSDTjDk46ltDRaTvzF3D@*x;d~mDDpOw^x-NvtBVOm)RJ}sp_ zU0W*jWrCD|Jg~Lvy5%9=9MCV=vMuHFPR`y3U=mY3+W-EW*qhRyV%+4KWtbYkazMCYV zSjKa=9Awpn{h6lxvFqHjv^&$SyYK66yVd*6XZMU%8Mpn4S&8`-5>wkGH6Q4Cm#k?) zS9S^_)31rE`w=QZL!U+Isxo{O;AWfY|J~c8(gr70e4}2i|Gkif0~%!Y#47r$tfW!X z%cQ{gf|ek|ky{mXFl`r$N7!kX-e_+yr#)JA!h((NxccW9IMBN}Iak%=3n$^m904avUB zk||~wY%$(R6Z|iukqA%&a8rZggV;hguV%h(({KDt_{+f5AMt_MJF^k$W(Gs}*+`@1 z&p8unbMs1@1hAu88$BFE^<4RRbQKYYT_&=0=HCul?_;>==jT__z&o$L_tW?_V6T^V+VvDC1a#)>L^zjCMR z9F}n$#|N6n;;`Yr7+-bN^!0Mbo?rR`V^j^jWT5>Kb@mN*BR03^qKRNx4Tl9it6i)| z`B+??n$!5|O>vOSihB35P?R&5ZI{7R`IaP*UNi;cY^tg8O2EizFWy5XjQ|YWz%$#_ z%9ww1ZlwY$3dVNU6rR4)fB9mJP$%O?%tQ?Fmof-hZLz1_b`}6L#=?!6 zqDyL3Y=s}O*p_5!3^J6>2zJ;7WM;LmwH<`1|JnTEI~VGJH)KXMC^cN}GDc@{#)a|w zOzif-uHyaY7wKw2u7pYTYZ94dPse{-uzYA~4;5%Urt&#di=T1QLN9r zf1BiZl>?B{VMTx@-lOe+n4%J9Gt887gc=BmVL=#+>n&v`HGP_j0TS&6Z+zX+tjw>;iD9!9K$O(CRYU*E0}Zt&pBi z9YL7#3+Pg(F`}9<@-Y45HSv?-f3U)-2D~^0G|oF_Rt^Si?>(LvWaGK2djI;ufYU&m zM)rJamittu?_5~*<$_#gWkR0&h!1mq2F}j?I4#4-O=6)Z3uP@uj3x^1o0f^vgXxXj zKt@1s+Pt%e0@`x-b%Zsi4U@dC39hXtk7+)!U^Z`|Un zcTWKO5&f>&>}l90#XyhC9Mi}gw)UKS`pJj8zp8c@7!3PY*r=3Kcb;+XX4gtBc;1z~ z-_?=lRe-Un+73mD=1>Wd?o^tifM`GDwE~F9Jlo8S)eqzfou3)Ib5vfZ+gFG9oOMKs z_uAI+JY%NGh*EwGcJ`9O^ZLcRKW8 ziJ|+)Z?xUe%c>9e!$iZWvzmp-?of(^5|ZBLwp}KDC}=#9y=nW-%#8=lqMlt%_DLcj zOLKjLHP4+=fNciGm1fq24=^Wdj_^WRwVvXD8SX_yc`sbvd?ly|a<(^WSyZfoA|hOG*U&0fS9v-XLI+MZt4lja` z+5>?W2aK1yPy@fukA6|imt&QzO#H{zahxGs1Q>PDsd&Aw_w{`G*)$E6~DJ|NfDw8|^VzxO zO4j>yUbmaQyoIr7g-4^pqOO7qe>%>BefLHV!sDavCOm5nAcI(t<2U>66P0~za$j;k zKy@CVR6Y0f4z3)?Z-aNtz7V?Bg#?bew>%D{bXYfiycu_qC-E`@_v?EIm(A_^g%reM zIBitzv3go{y5d4;M*1#Zmg>E+@wQl7xdX*K^HC>q3sxMri_F;?gvm#H=h*W!Dpzr$ zK+bVxuJrDgh@z-E7gATeA;zXK*#lNiXwiokh(QE(VrymLgQS*lNBirDW$GoaL74+n zrzb?Szt{)*M~DsWpw<9Fp^de>$W&Sx?G0-r>B{R ziS{Shvn5xw)K0MkEx{=x`02BUcHz1Jw!BDweo?35gd1#1DzT$>Z@*d^E(pi6=5i$9 zw6euJ;5~?IkvZ{od18BlC#}}bfVIhq_bq|TZtg7ky&RDFn+xp|dZrxwK}w0hIPrba zh5`Bmw*aZR&Ajyk<2GjLMbyB-LP=gvcL`TF?|z|Eo}~Kd2&YTGb37tQqic0|_JixI zK24HWY8K}pyt@gk9MXe~yZAf8*qI`viyt((u5XlF&}#G2qM`oviDiyQU?xdAf>46x zMDk9`wUVfCvv=B@m)$gfq7a$%7g49r=Vyt1QWg|%nI9-}w-Sb@QN4K(by_d zB_IFU{+H80%r)J2y|xRU1SZC3^P1@tK7JYJIWO%kjN5ermq(s%7Ur?8Xbg{-4`CP{ zdp->1f)qJOJOP(k+wT%@*hmvx(BLT9V-*l%)9z!Bwp?yaW!O#+m5XqGDdOP#`ilMW z!7yswg^u(2CKviifQPpslSE4@RjdATpw$5PPBwHBZc522Cvr|hou1N$S12Q0L&0kD z`QU&LBwk_~GcMn5LYNeSGx?mx4geRk5@uyN>!G1gwIx$eG*o|i+@Z^%!2V`G=Or5H z1C!yPcKT3^mn2BoI$Ca`iZ$YW+F>Uxk^EG&<`Yr+-1t7yXF>5=rWiW;B>|?pXslonW3_9<&zg*)}iezhxXJ`;cm~=tJCT z_BVMS>SUNKTUv(m&SR&T@Z^QGSptNAawX~FH8OvT3#3(mp#EP;X0Su)c%pYbcLj<< zTaE<6lt8*{drC}=+~ZTrLqT+k98Psf3ALdVS8YFIp=uLTL8W*8&)bI9UHXkSRINvH z2|8O;SiCxs)N3m#$!LqY4jYcM-EW7vCD6%_5|BlR%1)ghmOSmVXz03IrP@68UZw6= zkPs9{75u;AjOYyL67rDT4Q}y|$UKGAf0qy*hc#ZxO3V{8f$!tG-5J>9Feu${e0w#2{c(6+=xchO{V<1kEbK@l+ z0)Hy!EnwHZVK^}SYtd_!{I^R>`3c#4pI-td0#?1s(#ig>dvy!!>7IKRbjnRig(Rv;8C-4^ zRuY71s{sEgqhRq@NpAb==$mf@WK5!k@LAyP|CD}_8@TqpZZS;Bc0zdng=qNcc7o8j zafv&z>XKRZ-4b`U6_i#yhtXdIYwRvu50G3l;vgBMnEyw3y3<(B!9eN35Z}QfYE_)Y z*PT*8gy@>#zWsybO<7l5RM6|dYXK&Orbe%=9f}_7ej?wY{+$q#DFPVj)1|5FNE zh1t+E0*1j1pgOnGr4^08>g_Evopm1m{m9=}*c0ZzCwgIVR`|_1t&aS!LW_-y63uj7 zE1IDHP?s!cCWN2Vif7K`L`$7Mv4|!3-kD`;Xl5s6WhWL0&!en+At=|%{O$Bf-#0$c zxNhs?U)TH5`FtyT6bZsX=6|;`{nZ{vb>W&A9qT;$1YGF27Z%p{?ev|Xr4HcQpTI2VLfNp89#u2{AVfrNZpRc&QIO<@DA*u5Za9(8t zmn0R58h->0P^V&?LNR2XB0}UMUTNy{(I(wQ)r#4QzbiPO)M_Y1hm5C{kJrfU1HMF! zc2sUMPg)c22+#`M+2mr0{=ce)qoho-(p|(Ofge1k?LECRDP(@SiA-09Ai)v$f=&tb z^tx(SM#$dY1Wa0;|6}?wcPk%*^=7aQjwm&gdZ13ouRTA5OuNn@S9U8oD^4qS`^o`4 zD?DqhcJF|1bKIo%_J42&aY?a!#C){+aJz{+d^#$Z$l@LBLy7iNlpaz7kISNiyO#aB zVKUuvs`K@3^Np9z8yCZvWDsx?{kw?s>7muMhg?mp_^fEH6u+2pwL35!kR3>R=C-^W z@cZlg=pk&xF4Yv)T;KBc&q)Fq0%;nii zA31vm;g2XrWJEnoj%jxwvewGMVt3bigzMAH-S0|kmAQUTH?dgc|yDmAWlxBT=r zwLqOKe^i#4~Qi6vUy6Zgk8M+>tXwP zDo<`W<;q7NpAd2&j=<&~SLV5?fvtT!t}1p%0X55OawZaU%Bvb4hZgagR}lpeMIsaK z3|mo8AS~On#6!Nt?Zh3P$=rMB2B0#1+8>QZTk$ z9+4Fr(c9O{dCGD=uL+f~fF)6Zz41K*G{|)|#h<$4Ud2*b>L`PVHb_YHVML2y)EJ_@C!go*^E|(`oEojqBOX7${Gd@K~w~j|}naE8m>Zj$oymaT6mMWRZd{ z;R-n$(ORGuM&d?QIi!kz@aa8-6$e=9C9tG{(ZOv2kV=$ER5E5mUa>Wz1XEdnIAuGe zpG*gS!#i6%LOLgpl<*G@?@vJlf|2@EQ@3|1+2QwBRy?})+`04C5z@%!p^f<%w*%#; zTA(aaZ_+}*-1q@yfXLM#uo`_asoGyC(SB%kaA+4M>%U6<6cR+or>;^%_>P%sUbo13#8uB9Qi6J_4y-5V12=Q&BNTA%vuT2uhq~vhMVvF{MO}S z61ycZP=dAi_cRxK#a!PX&6=7MBV){2c|2J^$?Ser^>07kUemcM%~&$@{*ZAqZKpvR ziJd)-7C+;%l19qK(DV_>c!PZew;8sDUdxOKc74X5pzF}1rf1EY>V3O)fxD_lAJ>p0 z+KlMO!hCx#Wq;VjGL0{Zb>-gUtJLKI27thq(>l^Mw%U}Go;YhuWvg_u z)k~f~Vq}kk?j4@)nsq<3_ycMbDy^ie>bb|Ed-NucN{EErm?DgpnlIyeu6mPEfKZ02 zby6$|2P@H;GsFVaJN~eQ2AE(s(M-N#$R6Q!xpS2`%*ZnR02s+)YK1S$Y_sbVB_pQD zVxWd&xe9^(FA!B4*CrZlH79}|T^@su6}W(v2J}`9FjdP;oFTnKohI*_1qUYRh;{_TVA!7@8OJ z^aBk0;{I#rM8HzPN%*r3sG*9yZWKBk4Pm}fK~e}fCcv2Z5k9d&1SQ=dJ;?$Vr&wo{ zlZe>F*9_rjg?Rcu6P(d6m#f#lurvU9EgNPzJMup=wJNd?IhFTBXMYDZm6}ry981*J zA-wGLmlM)VrjnWPga-xF-3MY_H%=YXUAxHLgys{UozudV=3{$R%dciMN`4u^G(09a z862~^j9|WbQO3a)$IzQY@%LQ;{`43whFUwbNQB?Q%3 zSn=$*_lds7D!Jc5*tUxDk<{!T;np3qsli*s*|}5fF^;XdRM(`DZBgrG^j})lleeKQ zSiRceAAG@YIbp$DO2H=rP0KXFjVE?>mwml<{htI&WbWxoTiM-Sel=4Aw*jOVEW}Rb z4W7O|CbA(3B^k_a{z&1go%VWVq~3J67n4MGc6&K$Wdv?RIi+I{Bh(qO_RqhpV2~EX z8&IAkt&z?8Kq4O#ka^MsMY~3CETrk!g!x98F1rHRxDFAJ3;EiNl zse$K8TvnzMXJ6l%W1yV@B~2^j$NIc{z=RuJ#nr8f6*8XFP7Yw_J*Tft>DG}c8}@;W z-?*_VJk(SO>rSaJ6K&_Y%&KzPqLtq8YxWLV?Oh2M329_KB@yGYo^_(2%X!P2vfI?U z%~6Y#M6Z|SuoO(=Z+}_EW1vj9h*z^lpJC`DbjNIX7*nrh+V-()Ypq3lREG-N(<@>zw`XZK%q|nj>RMGGMR|C4Afu6yCed~nsDQ=+ zN^k0QHN_bYpXq;@1&tXdGA#G)*N>46`B>I@L-$3E$$MMjHDla+>r3zd_3g&*!Ud3g ziVt97wc3@D&s2Svy=$Gcm|NYxJxgpe;fejQLwZEJVKJmm1@<&jSU6p`JTJf8ZHAIn zc`ocVolv_F^2eD^K=%{1BP;CONKRn5UGUZ7gD&^YoNTz}>}dgYuM)!{c;TDRw5g*K zEK)jtCzroJV^^jYg3vGI%ji8+DBfuyjwDLr%Xp}hr!xtP5RKn)IS`uSwtsu?OJ89{ zCz(=gI?n2<6s=(}$XmR~b4AIq%>%+Mg?tBRrF>Ur9A0~F?K?;N1@(eRz}&a#u4Qt8XMX7u(d>Iz+2g% zmAWec_Yk*VLY{EvvrSg*@UryXHZtfB{IdT$-D(V!Rih*|EhPmA27L6w^(DTtV~Q3S zwLc@KWux2*vjGM<%1(jI_?s(z7+PM^(F{@@43=$pj+WM5l2qPp0bzs#)M7>Rq`k=w z${_cn5L~)VYM&DLNwIS|-#Le%HeIIy{nFXCYxp>bl(`9F$^kw$7!XOpxrm3kD9jDV zIF|TLA%_DAYHqTH7oD_k<36qrdJ}n>lH;ootv%h1Y0(V2zUePKjs%W@63QZS=#2WM zc_3IPi-y13^u>Jk9X>Eno3ifL?sjTiUpU=*gH5N(v#fDZ^K*qjVutdt{MbOW6`Cz1 zzxJMSL)BT3VQc;m|XK5S}AaOK=5m%#;(PEo$NgwD$Ke;)(McF35rI zp_(|F5$P%IIkNFU!fsEO-$Te{kPqmxR*N<$bd{Oj4MM?GmlN)FzTkH>(*6BZv9869|H}1`;lGkGwF@aGWSVytu=f62HMym+0d4i&|kJ zqG|uA4k1_VN6@U}X?B7%$ZqqdZisnGki+)ldIpyzJz{kMVa-rnZuX$PUm?n30GgU7 z*!0uz$CO2(fXkhJWu2R%4}xWv^{omw@T)E=-p+!ZcPFyeA~NXn9aKl0#(FpJ_B9z- zXDmD6BkqnbzP$KKGH(+RiPhU?zh%biyD$wTNjJRWF2gP%v;en}$kLBHc%d&vPw9<{ zZw0VU^*Hb)PG68Fe)J+apCcKbmns4RUdyX-7D;d~^XEY@V)EQ6<)Z}quM3}|wMrP} z_Q{v(Kdj(=n53UjzYKs;;0^VDUfI0zM8+@~bw;MEfYr3q1;&o(vgXveUO+~e5>U`;233vLEdPJHC3V3vxb&Fd5qpErT3|y1PVE%x3()2QY$S1bQjx)vDQ$Mf;))Iz%du_hrMP|b*9zu2p%T@_dRxgEt zPeXDXH#Eh6uYjndP3~J(a?>Ez`8H?sO(Zi_{lXZBf(aE7V=}8u@-SN9lm4>pB0lok zL+*atalvn~Mm}N&qzwa}>0s$(d<@gnhobSzv!_k`D}0WS4iax&z*!N1`rTljVI1k4 zaCVH=#|`onVD{HH-=~Pq_CR|#&67c80;<4CpM=3B7Sm{Ly zmVeXPvAb1npMTF1;mfYZ<*^h4bx3BGN_M%iN-yUyHYe;d^{4DrNafw8q7wb(ZwzNM zt(KpZNiiGtj->&|KIXBggAvJK`OWI{&O-Uv)o&S((Rz695k&R}LCP&r$FYFTAixHse3Pl|5jhMwq!=Ifi3* z{mEPxB(@EMseO1JBIwTs%4b>obH%a;8~7ZE`Tf^+$>d9Ksl?n-hBy1;xq}|EMD@ek zxgA`TwSB0sZTvc0J3J69BdrE~xzetTa$$eJuk?C8<0NUrHFuj8(#J42uL zhAIW^LdG#rF-KHD=y^RZd3{CYN!bl9N8FiHvFwjD!6-f-6#W4ETe}r}BkRpG@ic=^ zc9f;z>tNjIQ@zIisXsc`IwfFd5iy^H8otpRyOWkkdmY$aEz<9Yv!6cIv z?=fV*h1g`0k^>~}PDY7$J&PF)_-!CWx_oqkT&J>{d#uy$~;7VK3-;R^=KcXXf4)?eqcuuPCv6l;&`;Tvv}$9Mn)03)l4L{-dB*WYEr6 zh<$UjUv$5l(n|H$y1``aunBZ=n@IzbRsNW8tLymnjqml8e6_cCO?Hnx2A4JZb3A^{ zG2D1QKPC^%D0bQ9`^90lE#VUpPIl#GFpEPQ-6T~|!w9%rD-C}0KxyL%EK;=QF-JGH zgOB@8t9BuqT$-O}+?|#i1_Nnk28};57c62T=i<9nUn@3a#rNm%u!tejGSg&<3Ihp~Yx^2P9HAemIXH5x zlrh2_AO42B9R?p`v>p@OKMkfpy&&N)3ynNeT$ZQgu@OJXT?f6HJ?6Z9@0?UO`=qIe z_(R4EeY=8Lw0(?E(RV1V@@ZdYb1U)I7+dJBBw4=B-GATWXpIQj5TndhAJ!?M7%b8 zoTAf~Hq|3MrR8@F+9u07%Y6yjRP{4sdUNwuIs>6l_O)_^ED@eKwq$zxwaISC6s6^x z2+iXL-)~xGxdxw7Ic~2W2o3EK*(}K{HN-%fJAO_R(N?^Es@wZHa$bU{GJPyXi3Drx zr3RWBm61E6K#agF zM>mFzv%~76CxKJ{Y9{G|R|jLIrNHY~UNh(i9NMUZVJUsb{#+2O={Q?4QQl@YT$(Rf zP+|m~I+<&`iWl(bM1D=%y=|f0vTOTq?k&~fn-1k218bU`jtI@tgWAx-5V2gx4%y>#zkW^$L1na-eivQ~!4^a^JxRj}5k?QG(L>U2hpIX7!kZhIaCT^qD>nwH)CnGmK?iIHdSWvlo z8b2XMjL3w{NrcpF7TGxEmrVJ~{~BGds&(KaI_ zj?pMpUg7E}53(0}qSx+|MkV0Mp*@Y)CuZ{Kw@YpKjuB7R;N$513?uA|o_fSr^*@1_ zw_t^a>UCS3vLn9dXt|<3uU;7}On;X3LAXLI5A2ZcvIkq#nZQ~lXTm4j1d?UlDWLRg z(0HD<~cMsTRo`tKC!juc9YPba6xctxt9PKm2;M)Am%z@bfo{)QmffhPe+~d zW{KJ-wB7a3WrjKkY^8 z`e#(GvBzs98-CBxbIt47;tFXM(Tg}3cVN8rcCYYzlt(qyb^>Z*`KGzQ3YYufVg!5W z(Bx$OtM{@O}T>cOoA=%{;`NDD}K7}%eezisxR)$Fm~ ztLOaO-&K*(F8L>IfnrrR1)Wv~DJ7rygvDa)NrH3&f0NC<@5)#))ldCnhx}r@P%(}H zqUwdLu?acvrYP0qeIN>^{o%!1VfAJuD{k?>pB|yQw;v;AU?3I>)gj;alWqH4u%fhu zt5zXAIp|7}KcTr(VR+Ry7=p}QsKMO(uS4_lltLoEd?qA4AO)Uf-@b6ZT2gs1IO4?c zOgYZoz(11MN`Fpq zE%)o&TeIP|8$a0Zf6DzBBt&>4tyU!a8WXNMcI>|K+v7LkvZ;^3^YbusJL91BbR7#% z`}BVGTee7=ckq}|Wr>x!5BsfOsSV?Wh)NK&nckKS=0yha&DmL@L!7$rV@Np|Hqc$re^rA^+(6#M|-nubYS^~ zrBlf#u&Nz24ZS}8#woRde_T+`yXx9Q18YnT$H@?N^k-@+2GW#oZYLoX`ES{@ED)I9gca2xkZ;CzZbYme;;%^=hn&%@1p2p>| z-R#zHM?4|>}^453LQ0;V| zM{M6g$`Ku9ayidn^O_=DE<#VXaBm)%yClG%1fr}FTV5~h66qnM|G}gkC~18@ z-#a0X2ZXT?JE6Uk32|R@Gd$c0Nt0_^l@o8@xo-cb`adaPm8`$zW*}_FN4S6%k(xna zko^m>gddaU_TaqfP+p~UlYjAX-77bfi=JBcQVu_d?6O$(Fsfxw``C{R`>5p3o=|A{ zj!M?i@JTIbE_` zWY+Rk-J3&=zh_D&I^BxvIE9qRs`@GQjD~|sgVdhmIo!oUisZ*wGsepuzF<6d--`8A z>UvH*#J>Y+{_r3Cqw=6dcqi1{;M(52*t+ZsY*F@5aKb&oPWXrB`W4wVQt>R0YLZxU zr(6DYX@pNHslZ^;t;$9f`0kl{dk~>=?|WvoZsJ*zsK4aU{?x}V&+(pZ4&j|eaRdg{mcWsbo;dnGbzyv-5 z=DvR5PY?sI|HRju2R{`0JAx&k+_=t|Y|_6ERVJb)T$RoJt7tzO)Q2zPWpgX4daj2U zwH~+vO|{lyac^K<5z69PNK{dcmOWA!WeFv(Bx_d7GY=k;jq>!nTG9N{I-TXUO{2F- zRP=S*UGoS27cCAk1~`PR6Hx{0^2#ctwDYxkNKxlqnP91iLP9-fY$(lSWkmN_wA1|L zcqnrsg48F1YjeQUU{tJ@ZMz5wsCVpS8Nqh&W^TGoQy-3?z9WqOCO3^irOz@j1?ZATnQFe zjra~24!IoRWZ4kroD`2dIzYh?2DYKM6dBFz%~jTR7wvgB1pWSBACcTr@w+XaP<+ut zbV>l9Xf6*e{=HAcC>rGfb>1qOJW>o6oQfHo=IvTrbx0_rBurRF#7cYaJ~Hf9p?N!a zO`H;%Iu>kD@AFooMmB^~Ck`*J*~fXG2~$Z^3wtY8BkR7YH~u@7&grme^4)hS2&3|L z#K-jy`LW_w>7ny9%N`y*wMDpo%g$)x3DU`Kj7Le7QC(T1H}KO??TtW!6f(E}svD$& zVg4s(J$!UJvsJ|V{Rdl8Li=U4OmgdFBKzSdl}jOnoq%H+*{rRVq{cV!@$RVLV0&=W zvFQ)E%_)+*0&(#A2(#o_)yL(}Q?u}jJl2jqeh!_t{&5y4vF_w&y)#8gK2Yw7Dqm^ z`R*XUBkp0|jdXr06B-hT67gf-W#BJeI#;|_`ZX8V<)%SZkRchx>d(H*?HE>GCGYEK zxG3ARqkb!N^cY`gF&w896=472E?mWry+-sQ;o!Dz5Ou$O{~jhXE+$R39?f;*`D!lP zPyCwtOl3x8|LImI#r&$wG&J`C1*;0jW{WH!RQq9dt_=hIkpDSP0y55^z(1NfxcB7Q zgz6W}$`CLi1|zxAovGEz|2>Q@v1S=Pe5n|XIK6bOU&`;){!3)l^?TQr8z-LCiHkab z^p}6%{)q6>nItXjX`|>etXPuzu`%`IQm}`w?jG>ksx$>`;2GNBo1^v3s8zF-HxJS< z?2<%lu#I?pdqmRWS<)z73zy{}c2Uvk(nH85Aj-yUVwc=>TvKuLojCx>1)UjpRE26^ z4ZAM>rC2XrFCiBcKJt`DBrCw~C%$(@wi;ol;jvf=v$*PaeSafu;(g-Y&{a!3H0M71 zJun(O(&uRlMiq`S;pRbu&7m#(8n4r!0$U_p#&+7M;~vGNP{~@8@({{ezci~?9su=} zPt)dBG#1F*hBKFZyPh)@hu-}?(_l@@t@KupsoV`WQ+rJFwK@>1vX>wL(hWh6!0-NQ z+-kT!JEfM);5O3X5UV!#V00sxE zEWRKRLw66uMq6X2Mk08F zKhxmhH&TV8*ZJCBG0r~-`#WegAV9>Ek=0_IGdlFqqbXtZRVoiK2yqj(QX5>Uri{vk zsqVEt9d+yXiL!|=dQf4!TNr%e+$6I1Mg-{=b`<*ORV$F}m<=a?cbhV^@bcxoZc+tB z7Hev{Rc0At96lYcFJMQJV`ZjKkvpYZ zwZ)eUbN2iSkm03K62snn$a(VNC7Fa)Qrvv@m9Kyv8m@)7UBz@xuy z8LtQ_J|-~4NIyb0ofk-)4S0u; z;IH@Blc6*5XPSl1sN*z#EW;q5wYEfN#mUlJ?-s5L98COabaSimX4`}cU zA>pQLhlWd5wsLx~o%b%=+(Nk%uwuRj-mAKUc``IXDAhlm%IF7p)wU@2St{z6A9>s%sDA?Gx7J`JYy7;loC`hQ z-nYc+l!@R>!7~j}J8nRV0R(&Sr}HEL_x_(iIZ=X+{Mim|u#e?VI-4}{pLupUg0Yr~ zZPb`3S{xPto7`eK3^2V7e!m(y-^E8#Y&XwDmonJBgMMpjV}p|e+cdpWJn6!W3#AQ# zuIf$3h&cg2N$l4!8XepCP=6^10WZ@HDmH);`|S^<8)AQgU^d9zf@Qug?WS74xn^>ED zRYA{a<8jxi?>a}1!JFak{(foq#v(sLFn3xQ=k2u@Dzt1@4Yyfcr>Q6Rw1SV~9}(Jg zQ}pp^>IvPH%M!o9UH!w8aZAlQhWF+!S2C`(cVZoY)SK?FLruz3opn$B$qjLFvlc|4 zfQyxt@Mw55s&`LJr(`xU zmDgv3HA0%+%u*}od53KjryLbmKTnzZWKfeWp>!b7lZ9wue12NLYlBPUClSJI7@>;< z?B*XprW*Ao$m1~MyG1|5}eTOF;JS0_x_Vx=1sqP zPF8OQ0xjA38XvtBgI1#%c5R!M3IjDsNpIo$ZLauP_+zPXV z-(pbQ)SL7To5Te5htlf)+M#$3oXvO&_7oIic_{h|MQnta8V?!i#5^HNCi$SANr+0{ zD~twyDbFN2vb$+ii@&=)+;}vyfP7H9^UjsTjQh=D0@XPw-bkMIlxQ5K;G)$(b3EUv zm7PUn+FXnSV$1KU3zI(9fT*RBclQdT5LHCVB%g@QZuk@8_6juz%G5U+*MFfeLmC|C zfJA8`m8<_l*BfUm_zxJHw4!;Yfh)s5=3tJco{kTR$fCU?R9qAh40NW7;-dX#&jp{u zcwPxQj3w4|R!^{|CoUI7JDvy2iBUvs;k);W?n*mS{=hOl$70X#^eLy&5)bAhxTtJ_ zATY^+eqHgEE^H|XWs*B}3FiVv$!tNzS@N}%@4K?s!qV z9aY7T9g9kjir>9fOR{I5cqmNg#_Y2ow>BGqJlI~@kz^nqDt~RNOW0j5{mkvd#sZp}3a?qrJ4zp3L?C4`K zc!oSUC1nB10uT%vw3*OsxV=ZyH$`PG}= z@*3mmZG*Oc-sY+_>`>8Z`y# zAeodM?-+OrK%*8k*Tlam+MYht2~-g-q@IV9%$~+VwerLNra_D&@+{BU<6~v5yi%c` z^Xw_IRVhfgC2S0x$ZJuk-t-byUQy;fg<2kDnn)1B}p% zyjmlgrVe+D`aWR0d*$_~+rqawA>+?dbeDpE@uD2seDICe6ba-@``aJ>q1i*T zB&{N4x=Gwq#us#1T_rF;TgG&4MMM2}Z-Eh_9cd zZjn%cL_o`WR`*DQ9uu_lq+?xrFQL;WdWmHMAao_at!NwQK=Ybn-pgQ5r*qBlV?oo;=&KNh27gXgZ!oj+xbZ? zXuJWGqZBfbiZEumO&PNxzzVWJ9XAOleE>O)?K}P;VgnU{Y??xq&n@5>s6S5il?_6_ zo#>3ncI+D{P4M|ju!drWq3ey%-<)7yvmUUtwI|O9%JCnHhJpd zkW2V5G3AP^9l=^>iHxb6$YLr&?4#g503#@B159~jRHBxKY8Ey`aB5qbGiF(aTZK+?+2}&A5416PWmZYzJ^D=C77r=Fh(pF@TMe`yo3)5<#+YyVbUHUazZ-cm+b(RGmz2m`ohfee)T$G~^n^Lpfz3ExF8VkPbCe zBNunT=O%4BqL`f~%$&wt9q0UPmS6A)qrpWnpjL{xINh=9_GhBCfwJJlw}>g}7~YDy zql#ONaaj0SGhbEh~e7^}L%xkHUxnwo@it8~*INtsHlg z{Lzz!*VI>aYx`DW=51_X2UTX7@J*Zh!R7g$Nu7Gmj_Y$vSmiLckq(Jl!;sIMMLiy4|upbdTQwAW^&|o9ige}ukboSL_n*?!hP3v zsGJPA$>z%U=Vkhhr;<}rANx2qU#eM0yW=B1Zw&wCnf|j0>H|(ZIan#fH|@g)#lG4) zR+cnbe2U{qz@@whXOTEYndrFgUIE94i>i?Gd|!{9-z!6^q5TxSy$pue&8MSCG^kzz zNIanxD$mg}l@l3J7s{*3{FVJn103yY$5$tZ0BrHN8ZQN8oaONHLWUEG4FiZ;;3Tx2 zmo(xYAF0MZ@gGLj;G|4@-D94GaA}tVw}P6ttzV~N2zt)LhY^~V>5^2Qz@p%~l(}np zVX}u20{L&S#VmDXeU)iZZz37wDma>0vhpeZ4717|x4>fLyt>+)Cyf^7cBbCHEL~VX zrgKV1uE@0yRkHe$)P`$LG)P&N!z}+2C6neWCtzZ}wWwsD-M?V| zJ*E}bA@5A-X!Mu#P!to6FcgxbU-&fH+=ok38ruk9htKfBQ%2wNk*PcrCamy>02}y-^8d;_N zV_j{Fdpx-A1-_q^3?;-O#M$4BZ=y%ib`4?o+m-tyTLN^(UCvUzmuZ*MhbaU}Q*$vt z)zUI}^sP3Xl8_iF?{jdZBkeeQ31GGj_A=q$MN<(*AFHzhb>OanbF0y69aiYt%AU!U zUTsB8HJ}sKx3p|kvjCc6R;`vDK=l(}2x&9s-5b$jD%8aL4CLEHN7LSVA6}#yo+LW4 zg_kUN{FyEKWq$g9EK6TLKocEufpqXguU8@V?gyd+(L41`{Z>VYs=EqKM-};TxkR=^ z`L^+S`M(iUS}|94GYvT*PVBx6H(#;HE3Si)Rg~waS(uRb{pgSwnZ@a)N+-y8(S7@3 zsXVzMDfUC~c8&Gy(uoyg0*sRHOSu%s{UIr0g<-7Ea7aEx*YGh93H7TmL{?>({^Vhs z$m-)=Mvs$N!bu7*crS^ za_aui!F!hJxtAYROHzGC<)n1p@EM_hs^xLxRdc)G`si{DCL$!1RKb*JD3dGlWXBF5 zv{@f)DgQ5gwsK)}#q6TyVT1lL&r6T$I-@$}tc0i2 z>~H2cKJBMn68AG?WTU2}x8kQDN>e9YZX%rCrA(1~x4GAR1T!djU4Nmri0P?7mOmjK+varlR(T96aswaAM67eB zNqO+7JUMDC$~7I->MqJ@;`akDfDPw;o$>YNC{-WF-M*NQPf-qdyuXP8#C!Hs5jKD2 zDri>!7Ey>K6S0=LAEBZ`<=Tf7l-dG*on(fg&QV5|cZ8}_@tEl;qmTQK9lupP9mtae z1v2`wAJxi^6LqB1P~THAQ2rM;uk#Dqm%?Y0wRRr1jq5Z!53rP#Mw&(00c!7(a6_8I zda4-HVIvT%Ih38l$*dbQE*M6nl(KyV8b^LIJrPst%0q#FOpuZ5Dvw1}QEc3qL{HK` znuu~Zej4HlEV0nnB&~*lI(MZPl29T%+0!8BOeeG}DYgVukejDw(IaY4Ci5P!ZUcMBhsz2NU(I_k_(`eu;I)q^noXztlgA(m~ zqico)Gwj)jW;gLZZt~qHh$()wH#$o_((x-$r~nPePn2D!e{wLwmPC;8yb#Yi#V<&r z4!O(}zQ?41L`GyjmU_<(n8cx&s^8_2sIYH)ro&6&3o(03dmE5RzcSOr3zsYc3RLQ{ z9O;d#(k^{e&G$Y*c^3worejgZ+sdCu(h&6mE{LD{hiMT+ufTlZE*A}6^i|kMd8Ph$ zT{+ckfFt%0AgbR9)_5Yl(7*L{NCK>G0+7@+vYxf8OLpZ32xdG+P>1O$7zvwuB!%e9 zMIQ^Qsa5Gx0HpBTFzVL%**c*s(#T@bIW%e#+bh2t+)F=aQ4d5Dnb`eXJsv{rsJ2p> zqn^R>x)XvL<1dPaU$8LWHv>pZ&U3lqcP77-^R;TXyUyi)%!M)lMC%aGQ4&kw+n-+1 zNLexRH2&?{HVSYQqG)Zq^4uHrW3nNp z+0!?lIcuhKtRywY3tcITY5<0^t}q}jMSvSw8{_Q4G0?{v{U6@}K1BijHEG2Ch@EiU zSk{$b3m&k|OW#e)m$ky!D0hJW2oT7N>ffSKei#hps-+gzbvwkL zwiS##i$Ql!1DZm;d--wyEoiVima;(}a>V@F^sX0@(#T!bv!4~uBdkW;bPVmvE+2T5 z?n|&j|6J0Jav|9(SSl4#; zTa`zyU=UR$OZgYgZv4Mh7|eu3Y4*e40V*p0wT-gB|F?NwJnes%hA1yUZJCviVdqEu zr7vcY7yGoLAk8>hF7q|ri+fz`b8tl;fv|@=u@-Me-$@AN_zoP42$^ntY7lTxLrU# z=f5@`{Ssr@iyFMx|F3ErWkH&;)Xn9lr6@J2fS+HS6o>x}o&CpwzvDMbtI%p%iZCF$ z&ZZ#!4I5|b8^(}|K}^k{J=o}wEvSz4ovZx(%+3%ok}X$d%p;hC(4y3xJJ$z*_!vr z=^FY;W?|e#VLe~{zbx$kDlM`ZOU?E$v+!R4^hNVpGWu8O<>K}3{NEQR`M<56QWlh1 zs4meBpCa=bF?|SBnj-4zPqQy_?Js(5WO@LZAsLE!P$}!j@R^uJ1D&H_e*U4J^eYbM zg@d7t0LCPi+N$03Jb?`i%&j(%9V7n#3M)utHiy1j<^9iR*hSusCJ)@4u3yhEK7|B* z#&t%Y=dR{oRFw7g%<_v=;l=*{gJ6riOJ7Q|USP}dccAN^+9d5=+~Z=On*Wi+{MvVS zh>HKeL5Yj+_Iyd_oyWzCT=e-VgFtz{jYL@ zj#=}s@(Kvas_B#e1Dx^igZ}f+|E^9mjhL#HKJ{7pA4)s_dHeqVqbC1Tp+Nun-&(OL zO3&z(>`i2~y2C3TK$eEt5|l%ILYYAJ8C2AC46M!zHiC>N0)7J^jM4%!iQx5-<4 zq*WF%*eZLJ;#+vHa#GOW1wt-vV41$0(E)XxORSmVS<`29J za{=rN-~*180w$B|GDEfqE2S=3XIDqvR}^0oeSK~*=b(YRnwN#F^kb%(RBgEeG%5eWK2Kb$@nVfYF-|D1t!13R8X4jFYLpg_YB2hg0%k_F(EJO^zaoI7A?SKHUt% z@QY>k!wLtG5iuPJq2+l0-apH)$6Fg&-p_X$~dRQ!9a{!2QvjzUA^7p5GjvhyFflZmjP%F#u+Xwiku2hw@KYh zzR|abwNVDwHb?0CK(O%QsBTIlak(B_4M+4d{8_Ncs<|j+oas=pU1!?`iCQw6bJ-dTGRzA6h~2=t!^HgG!`1eG}D5_lo+TEufHM>(K&IoFQFf}jgVj`Mv1tWIB#r!w4fL+29K1VxF@K4 zbB`NcL zWFkZU3_IPKYq*giQA_@QMqhrX#m2I{b}31s=v=LK$n0&Z1w?Mv#55yPRufBWdWS$D3RA-yy=~MD5HY>UYNF$e{{3-LE7+`VY zG8|0|3iXPa05qu`2yhI|mw5ZK4@q;@c!Jr0kQR)`SBK$W_F2P9I{su3x0G<{UeER;cFqmYlDdIqr+8?NXALb0q91_8k^QHFhvuoLX}Xa zn7ezL2s)YkEIBsQ=+VpnhqSj2i}Gvtz8N|Mq#Hz9njs|x=~C(Lp<(DQ1ra4A1cnqP zM4BO`MnH1t?i55|D5c}Q{M|R+`+fF)j$%D`s75t!u@(zUSvGMm&+Pi~GFL zqoyWCh<+_E8lhKvg`0(KYCb?`r22ltB3u@O0nF%jpLu^LBevTyiS^0R9eFh8i`Zy7 z9Qtiky`5CNa0HB3qsOqGX$wc6f);~*1*L5-rInm+XVJ*PThfY1XpItUd%j1TI)CnLDeD(jt$Kq&qw?$_F|+N&r<3AS znKy_BEmmhy9U0z^#5$0#k-hsX_4|_s`wAU(XR;W_9w#>KlZ3pS0Xp!H zCeG4oYMwc8scO-&fDdJCCMHubhI<%cGDO(n$4NxX=L3R3@V3VE@WW;he+9sTXu%2A zn%>XF^2Q3p2#J`8z@mkg*5f*V;Qvt)3*$vMRmKpln5~bRpd#LTS3nub$Iu>R|5M}C zi8PjmcrW$^j4)_2NPH#G*oRF#3CS%6y4nr`SS!65zn-%NMT&S3PsQ_xg$t6kXCQOt z-skP%5c!_$W&DzT3j&*@z7;H7Oaf)k4Vfgf1x5~~H8Kt+SM_EzOA>7e^8j&hf!3K{ zc*T^@-`ng-SLuskE<@p;xqr4VA`o}iVf5V-QP=P5Ma%^n*97znrv08ZwvakJC;=g7 zRK2CWp%>l{I492+G@fBl;Q-N8d_r5)$XdshAqJ8SlRtY4Mc6dD-h-@TwbeH+ZJpwq zr{#Dx>Et=@XDe1E< zj8c4_nea6Yta#6pZ91_1-zFmrG#L_PDGOjAwlAKl5&Y3cx>L{R0#};+6m>j*`&Iy)P?mE!$_tzHWrE=Yae1?~3sY z#VelWdQ|Aqzs|;Jh!&!2+nsB#D~S}LnHF=IPXQNUn_r=$LI~V~v06{_cKQK#R3}vC zUavhX-d+E@)9;b}5cY~jEb6CA*uF^-mKc@>mKK%=Lj1XRc7CEdHtiRavTfHco?13O ztJs<(IX0v_KF7Lr`gT`WZ?MK_|B^#s$A?<<^CDVJ6`~GxJs7Ml!vd*e-^aBZ{Ej8r zvAB(3bpFcD9Vr*%S%{3Zmwi5G9^JHv8}Y_27}I49#WZlU1Gj?E-7P0z>;K9-N^xw2 z<7+`DYR-oVkVYM`rG7KdAJfhs>5IGf0T!E_*z7+!=Th(_eu5&hk%EGuRQpLyL*<+4 z1SB~%OE}Xphel>f=#siYlr?}~CK2Yf0t^Y}=0nFRC6+8Fa^|N&8T6k8l1j;7yV2rx zED@hT?BE`JlcCZspweWaw? z1qb6r$b4^BI=$Gl@?#ncUpofXVH_7-hVUb>B0ZDOJN{}#9w$Pfr$$^C-r%TQ)6?9fcY|;4NehviDZg*Z_NABE@TOm4W zVVJaC-(<7U#{d`>X`*f^zW~43$bo%}&U4B7Xco~3vN~KNGFm{+UwFF2NbpFR4G99I z15+aZoSB3mi{zNKDMU-dpb{3vc%1YbV$q&1eVoK?C6r-BmUwG<%TP1QpLSNUS-c3NVhl*>%l~E~tkIsq-Os zsF&r{Ffm*zd9qr4aN|Gm3w%gP;CSi0HF3k8`SnLY`|p}nnn2_ZeECPi!EYAdb|ZOVt*J$paU{p|L>64rd~ccx`u$Y^IxT zs+uaQVSU76@Jxna5d&d_y0#|kGD>(t@E*xpyCpz!M&yo+$ zGuuw8bgaV!PMJ?P!E9nDtr(&uwi}78V?AS!pDWGQMXa57EW0Wc!-a!o3AF;yu7At- z4zze6*iq&(k%9`4I_8zY+1knq1ru>cb#c3rb8xxvcx11@kHxp&KS%yqa@aj&8I%Rn zp-DZKqU*UM-~0AxtPlDY^U4f!=jkfj#|VqYppMrWDnF4sPvj}1KfHY#)zls!+3}T5 zSjIAcSaNN-XHJ3gBdOBe=gNJXqJ3{+49Y#LPxAfz8lY2e^Ei1c;-Z$8uq#rC^1d(u zyM%Q5D!dpF(R8SVuI(1qN|LnYrpPGW-Z$-zjAo;U4QG&LG(UI4k;*&i0F&(ETY5sc zY{SJzuK0Q^C|1FX%nI)lmND82HtL7o4wB1nh4=$p0edUUQO18UxgZkHEFm`_B3>7F zn-#~+yt(LD7DqZUyyGU2QfID5Idd>ci2FIq)DY5*<5_i)S!N|8hr3Z2?XRxW%b@Aa z+5H0VPI`IOi244+bR#VI6U6L`y}~)LIld1np|!s^FJ0 zA3xqIH3)Ew&C$XQJ=a#UsUVss0wj1j=$E4lOMTWZT)cR!w0Jxg)Ac{lp;y;=kvf!0 z%}nJ*YpX-;30RY5kGB9y8@>NKm4FPkK*Ru!=q}zq07t}K&gm$0; zxe7SIOy0L$w^44l8j%;M0hNouKLGA4u$kOfm~q}NRH6^G79L~**a$ny=SA@^TRuiv zI^fK^K-6u)fks7tU@)^Fl=PkW8L2-fy`zlUjK}-^gXJNp1Ibe$#c}lA9iYvG-b-ok zVg0jxx5PgFE=+2)1r2JS#H{3*lpl4&pZGrUdtm!fHw55T2)yUN+-pcOdYg*w_D@cD z*g*DE0MW(BPVjW;U z;`v9d{ke%rgA0)CK@uBlRtMb38C}nr7agBZTKF%!xuL7NMQ& zBPc9DB}+JCnm+RY5fHV@F!vQBx`WZ#UN)M@b!Fp)<&w|Gg_fQG_}CxmE_$;%j{nXl z7qJfK1fP?<%C(;(r~%+QB;vT$AkI)h!pA$l@3TwB+uC_kLDupzRKZXt|;kHSu@wBUU59cxJmb#OEK7drwv4r2+$JPg~57XK@ z>lXk&K{9e=rp%Hd<^2GT{`>gbcc z31`t;$iV5#bP=k$Hqw{f>SsGEC_jjFv@Lv9QQr4WZmnR$US*PzoDeuf)Wrj@`Ik{m zkh5Qc-Li^awUHpcVeNZoQDDL#oBf*}e`AcnpUCjz`;wP{>i$GXrYsJ|iyw>qGm_)i z4r#U1I*IVJIs2urFR2ixtM;!sFiHRyej`9bn+jUQ`ZDQe(Ja~QEiVIXmB+!*=*-Y$ zHTZv?QyNMnEMo!?`eu-K=|byeJF|&~{tUVr&2KiBKoSjJzIGvC-I$`0mrDsY!SRQ! zB1!j1$}JjGNrm8=xL^IF=qi}doEt8XQk!G6u$jfkfxU-y<`S6usXhLQFklV|5_{al z=yl?HnEnFBKms@z#(q94$cMzl`Eom(F$i_#dPBnd2A$nNKVT z1(--`+Lfb8x?dF^vEY>Fh5@*qBC3i7ewX)(m(s6pi<9=BHGV2>hNBqi--IpcBu&=|2{2wBh7(+U6L%B9{wp@IR z$hTOm#G6m=3XkUT&IdlZ27VBj*Q@b9=1~lW^Ak`#qZbw;oCfdx0|H_^^~!fEyg}roWQuaz|`?1WMNVJ%L9pp@;aMuRU z@oXO?^Drt-?E+JM?&H2cSxil~=-zunEC}}p^^wD7MiK8-rfzU&+8(S3FBhv^j6mw1 z4gU3ZOw`vXaQf+IY}Jt@y*w(-E%?sz>mXY$=?)fsP6zqxN!tR&mHBK>>c&X(1$pfB zS9W-h7+|W;1+=(aESy!TtIbkb(&~}&P><%f?Vk~DM)BF0+nufHVL3ez1M~TiJ9pI| zwdSeu*!Ck+(Lzn8ujdG@iu@f5OkHM(ulGwFrhxG?IMpW8h{+Q0bNndwg7-m#Ud@~_ z6cIR54vaG9%N%ULbpA9VDz(<1}PLjo=G+ch*mT9ts*F(x8uZ>A}}P=tO@hYwsJ+*fC$@S@KU&2w%jAim~TxT!@FR{ zTF~Hv#Fv>_*bZFv&V84A*Dg^}9XdbX`5w593`{a2NN9)|`gaZ}nV}!8lbp|(!X>cG zS|Tuxa-VL$ogd0FoNnFDs0`uhHi!qrgl|@@is>?DHjLt&AtT$rBepvVxot(9I)442 zFE1i7;fU~r3BdUfOayZY^TM`IKj)h-EF&2{m^HD1-T_B)EVx(I3%N$zF@O+ zA=&Iy^>eO(+%1+?Yj@w|E8B~=hSfN?<+DEii7X{cbYXdctuG@$Vp-*ucWK!9)d-6t z!^qEas{L4}zC76Se&nc9beF6u(V!EOwWD7&vHZsCq&+-tzVRXm!JAnA;xI>@`;c2+ z^?fc(Pqf3x6Q~5g!L@d^ zp{M_zPUIIj7;NF`asvVpatc`*;rBv5qc-}cn7+mssKAe(oWqt!YPtc8`zx29R{@r@ zEFz1u>g|Zu{m%3cH^3RP?78A>HMpQ|;C@j>yDjxvEC`6DBZIoOVq+g~r@&fqg$tE7K4vQ< z6?3?^cXh!Jv(1IWm~zEyOt?luPIk9U-uB1N&gkE1)>cz@1LA+Q+n!Z?Xe?s& zou``zI0gqBnN4kq&6jEAMKjZ1Dp%g0+6RBzX~zDw^K5*cP(@mpuh*eg&nl?4tk=Bl zRqB_YSeN&AtCBQ~fVZOrxUmI)hJ*(ecsujh5K?!q(kg%Zyr$df!*)b_QoJ+FNXq_J z&htv;HQ)|COBqEtSB_4g`v@;$w$4Q4%g8i=Ar+N7QBr&-soBHjFH&| zHqsqWJs4`KP4;wiG$t~v@$EoouzF#)#$TQvaUjzB;r-xeIVscl z?Wb>?#$$1*CFL55vRoegDc>ZXdLsue9s@TsjcH@?G)e}Ad<1n;81#xThc0}^t_s@V zxqf*&q>&VVTo}$%C=n!+6X6WxgI?J^5R5x7YvYl6_~nztwW-aRaEVC3!Ptil-6_@p z`Bn$#e5WZ}8h=02r(Uwl2Q^(PIdA#rfBG!f0)Nh^SQbC83q|x(q^qk>L_aSIPBXt7 z=vOi9()VM8M&FoRDS`i|F`hr$x4hTD`%>V?nWp2K`hx_0s5M8os8PBS@Qi=0Kw*|U zWh|5-{drX%MR8rmK2$n3zkm#E3OdlMJFw0*Vo82xGO;0INi%SpSFU^7B*s=D}yBr;G`$ zZ@SF&1GrCKix!>l01kp6%8G@_R$iW5#k^$l^(ywrB)J7AG4DOyY`Rr6ZYq2Fh+k8- z5eH%;Lw~gb-^lAud6Vs2DPS=KB!JhYR z{v1`w#YyYso+?nJ-aS=~ImC7hQ#5U6cMKcIzF($Vh00zX;3C$kVVA3g(1Fs)9ZN=c z-$6=@%(T^;wO6eS;-*L^SueJWCm&DgaOGdbTMzu49ns7c^zn9LB9z~Af$ZuqKJb;! z&N!rYgwZ>W)#~0ZxP^)kiQ@cDoKD4zA_t8H(rc?CssZ7`j+(Cr%7!!U<_MJQS7k{| zTn-Mkbcm>kdd^(?wQFw3F79j+y#xx0HV$DBl$nIDD&i4l)?wULu@#FwOK~V5vX~ znLsW@BH{4>nVx;f$;DO0zEmWoF>=tFq1&SEvr)rd^)yu@AXfO>M(QyDc2eBzOgr$Z zdJueOQzLP7q8j5-B41F>V4Mu`lQw%WTDa}_<@F@oxT0tb^3`wbb;c$szvDRhH|J*s zQ}G_jA0Ck=FG%k;Z9(Ad5?6#62hLA-GV}-mT+V-ey`C(qI5JU+#?n~CIF7p`BOSa( zE;H}r71EeAxw;VspSosz%rEN92!6Xkjk@N=vUseWiDjX?#jeHZj2?`QeBiL{3Xy)s z$id64JUeO*keeK&)wAB{<#qbV?0S6&C{vZ0iU%?vLc|nIwyXBrEA5o_L_Hu@H{FQJ za_jj9?eZGFMiCo#t616SN$x{(CAPW=d5VPqqu+(VaadzHwP5^~UVYq!_%8DVK+ICUj^2juoQL^ZJ6K*9Q{-5!^7Vadt>&1DWG&;twp=`K%oJ4Fyr7WV( z9^|~LP^HY}|H#E>0KZqMg>54;k(yY?gHizA0g(__o?wfpNA_%s97b7Wirv90$v=4t zP9I--@u!IdoOitCs+>(}pz5*H3>b9L)QYT@`>sP@f~<-e9KRHW$2p7Lp~Gq` ztCkW5^4$TZMR8e9%o27eeRXrT6E(>y91>Ja^fy&YzN0KMW^fQ{vb6(c5lxGc3MHI=?ZBp$!_)YO_cFa!r7h`) zFN$6|!j~ibEE8(M)MbgSFw*B@_p1$)6A`TMFSH5CieBD}E3?5mf3ObUTmC-u(M&Py zMZvO>{4snstcu4OLiZ(zG9oMv2w(BkRH+KvzYu+_7#~ou?XIIv(g1;GCN?o+uxq>y z>A!nz$Ep;M{F(njC;kdrx6)wLgV1*Uax1(Z~lZ7K%(0A!1zY}@9e zm5UW$6#h8`Uy3|z3F4zg&Tz6=4+JQwJ>6+aGJ4`^l00V-%?18*zeO4xmNmL;Vmo15 zm$DNv_-DK+VYmf))QYqSf`uYo%lV17mZ^@&+~QZ)3dN=RCG-Qy8g>=*XH0x96?Gzw z9;`^d!LeU7`&zR$Y<3t-{QsmLe@`-MmC+31@yk_`I$eGHXTqz%;tCLn>Kljh)ihv2 z1-#59dd0aE55y$@T{4NO06l}PA&3yFV|8)HB9Epj;veGDTQenQ3jlD2Fx~iFZZ6hF z?l4gtj7PJnpUkI!^H=SP55k|jG5_Tg@_qh9v{|zJ(@aDroO)=X8zVvSi&md?v(hY; zVc~mMB7stLFCeHan#|_~fqHgR?UHX%0A@!lL{U^*M!`Q}JiuNnm++?agZ=9v2F8}U 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%0AK440txV2f*#lSw|hGK+Lhbj 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 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 08/58] 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 09/58] 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 10/58] 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 11/58] 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 12/58] 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 13/58] 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 14/58] 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 15/58] 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 16/58] 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 17/58] 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 18/58] 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 19/58] 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 20/58] 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 21/58] [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 22/58] 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 23/58] 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 24/58] 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 25/58] 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 26/58] [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 27/58] 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 28/58] [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 29/58] 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 30/58] 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 31/58] 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 32/58] 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 33/58] [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 34/58] 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 35/58] '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 36/58] 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 37/58] 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, From ac145ec3892a8483f1f7b71b0b8decb2ebf3a741 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:26:15 +0100 Subject: [PATCH 38/58] 'MDAnalysis.analysis.density' parallelization (#4729) * Fixes #4677 * parallelize DensityAnalysis * Changes made in this Pull Request: - added backends and aggregators to DensityAnalysis in analysis.density - added client_DensityAnalysis in conftest.py - added client_DensityAnalysis to the tests in test_density.py * Update CHANGELOG --------- Co-authored-by: Yuxuan Zhuang Co-authored-by: Oliver Beckstein --- package/CHANGELOG | 1 + package/MDAnalysis/analysis/density.py | 36 ++-- .../MDAnalysisTests/analysis/conftest.py | 8 + .../MDAnalysisTests/analysis/test_density.py | 162 ++++++++++++------ 4 files changed, 145 insertions(+), 62 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 03255eb4ad3..909020a8661 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -25,6 +25,7 @@ Fixes the function to prevent shared state. (Issue #4655) Enhancements + * Enables parallelization for analysis.density.DensityAnalysis (Issue #4677, PR #4729) * 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) diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index f5c270e7dcc..eb2522531f2 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -169,7 +169,7 @@ from ..lib import distances from MDAnalysis.lib.log import ProgressBar -from .base import AnalysisBase +from .base import AnalysisBase, ResultsGroup import logging @@ -395,8 +395,16 @@ class DensityAnalysis(AnalysisBase): :func:`_set_user_grid` is now a method of :class:`DensityAnalysis`. :class:`Density` results are now stored in a :class:`MDAnalysis.analysis.base.Results` instance. - + .. 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, atomgroup, delta=1.0, metadata=None, padding=2.0, @@ -412,7 +420,12 @@ def __init__(self, atomgroup, delta=1.0, self._ydim = ydim self._zdim = zdim - def _prepare(self): + # The grid with its dimensions has to be set up in __init__ + # so that parallel analysis works correctly: each process + # needs to have a results._grid of the same size and the + # same self._bins and self._arange (so this cannot happen + # in _prepare(), which is executed in parallel on different + # parts of the trajectory). coord = self._atomgroup.positions if (self._gridcenter is not None or any([self._xdim, self._ydim, self._zdim])): @@ -465,7 +478,7 @@ def _prepare(self): grid, edges = np.histogramdd(np.zeros((1, 3)), bins=bins, range=arange, density=False) grid *= 0.0 - self._grid = grid + self.results._grid = grid self._edges = edges self._arange = arange self._bins = bins @@ -474,21 +487,22 @@ def _single_frame(self): h, _ = np.histogramdd(self._atomgroup.positions, bins=self._bins, range=self._arange, density=False) - # reduce (proposed change #2542 to match the parallel version in pmda.density) - # return self._reduce(self._grid, h) - # - # serial code can simply do - self._grid += h + self.results._grid += h def _conclude(self): # average: - self._grid /= float(self.n_frames) - density = Density(grid=self._grid, edges=self._edges, + self.results._grid /= float(self.n_frames) + density = Density(grid=self.results._grid, edges=self._edges, units={'length': "Angstrom"}, parameters={'isDensity': False}) density.make_density() self.results.density = density + def _get_aggregator(self): + return ResultsGroup(lookup={ + '_grid': ResultsGroup.ndarray_sum} + ) + @property def density(self): wmsg = ("The `density` attribute was deprecated in MDAnalysis 2.0.0 " diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index 91e9bd760b8..df17c05c064 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -16,6 +16,7 @@ ) from MDAnalysis.analysis.nucleicacids import NucPairDist from MDAnalysis.analysis.contacts import Contacts +from MDAnalysis.analysis.density import DensityAnalysis from MDAnalysis.lib.util import is_installed @@ -157,3 +158,10 @@ def client_NucPairDist(request): @pytest.fixture(scope="module", params=params_for_cls(Contacts)) def client_Contacts(request): return request.param + + +# MDAnalysis.analysis.density + +@pytest.fixture(scope='module', params=params_for_cls(DensityAnalysis)) +def client_DensityAnalysis(request): + return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index c99b671e3db..68dbed6839d 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -230,12 +230,20 @@ def universe(self): class TestDensityAnalysis(DensityParameters): - def check_DensityAnalysis(self, ag, ref_meandensity, - tmpdir, runargs=None, **kwargs): + def check_DensityAnalysis( + self, + ag, + ref_meandensity, + tmpdir, + client_DensityAnalysis, + runargs=None, + **kwargs + ): runargs = runargs if runargs else {} with tmpdir.as_cwd(): - D = density.DensityAnalysis( - ag, delta=self.delta, **kwargs).run(**runargs) + D = density.DensityAnalysis(ag, delta=self.delta, **kwargs).run( + **runargs, **client_DensityAnalysis + ) assert_almost_equal(D.results.density.grid.mean(), ref_meandensity, err_msg="mean density does not match") D.results.density.export(self.outfile) @@ -247,23 +255,25 @@ def check_DensityAnalysis(self, ag, ref_meandensity, ) @pytest.mark.parametrize("mode", ("static", "dynamic")) - def test_run(self, mode, universe, tmpdir): + def test_run(self, mode, universe, tmpdir, client_DensityAnalysis): updating = (mode == "dynamic") self.check_DensityAnalysis( universe.select_atoms(self.selections[mode], updating=updating), self.references[mode]['meandensity'], - tmpdir=tmpdir + tmpdir=tmpdir, + client_DensityAnalysis=client_DensityAnalysis, ) - def test_sliced(self, universe, tmpdir): + def test_sliced(self, universe, tmpdir, client_DensityAnalysis): self.check_DensityAnalysis( universe.select_atoms(self.selections['static']), self.references['static_sliced']['meandensity'], tmpdir=tmpdir, + client_DensityAnalysis=client_DensityAnalysis, runargs=dict(start=1, stop=-1, step=2), ) - def test_userdefn_eqbox(self, universe, tmpdir): + def test_userdefn_eqbox(self, universe, tmpdir, client_DensityAnalysis): with warnings.catch_warnings(): # Do not need to see UserWarning that box is too small warnings.simplefilter("ignore") @@ -271,101 +281,150 @@ def test_userdefn_eqbox(self, universe, tmpdir): universe.select_atoms(self.selections['static']), self.references['static_defined']['meandensity'], tmpdir=tmpdir, + client_DensityAnalysis=client_DensityAnalysis, gridcenter=self.gridcenters['static_defined'], xdim=10.0, ydim=10.0, zdim=10.0, ) - def test_userdefn_neqbox(self, universe, tmpdir): + def test_userdefn_neqbox(self, universe, tmpdir, client_DensityAnalysis): self.check_DensityAnalysis( universe.select_atoms(self.selections['static']), self.references['static_defined_unequal']['meandensity'], tmpdir=tmpdir, + client_DensityAnalysis=client_DensityAnalysis, gridcenter=self.gridcenters['static_defined'], xdim=10.0, ydim=15.0, zdim=20.0, ) - def test_userdefn_boxshape(self, universe): + def test_userdefn_boxshape(self, universe, client_DensityAnalysis): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=1.0, xdim=8.0, ydim=12.0, zdim=17.0, - gridcenter=self.gridcenters['static_defined']).run() + universe.select_atoms(self.selections["static"]), + delta=1.0, + xdim=8.0, + ydim=12.0, + zdim=17.0, + gridcenter=self.gridcenters["static_defined"], + ).run(**client_DensityAnalysis) assert D.results.density.grid.shape == (8, 12, 17) - def test_warn_userdefn_padding(self, universe): + def test_warn_userdefn_padding(self, universe, client_DensityAnalysis): regex = (r"Box padding \(currently set at 1\.0\) is not used " r"in user defined grids\.") with pytest.warns(UserWarning, match=regex): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=self.delta, xdim=100.0, ydim=100.0, zdim=100.0, padding=1.0, - gridcenter=self.gridcenters['static_defined']).run(step=5) - - def test_warn_userdefn_smallgrid(self, universe): + universe.select_atoms(self.selections["static"]), + delta=self.delta, + xdim=100.0, + ydim=100.0, + zdim=100.0, + padding=1.0, + gridcenter=self.gridcenters["static_defined"], + ).run(step=5, **client_DensityAnalysis) + + def test_warn_userdefn_smallgrid(self, universe, client_DensityAnalysis): regex = ("Atom selection does not fit grid --- " "you may want to define a larger box") with pytest.warns(UserWarning, match=regex): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=self.delta, xdim=1.0, ydim=2.0, zdim=2.0, padding=0.0, - gridcenter=self.gridcenters['static_defined']).run(step=5) - - def test_ValueError_userdefn_gridcenter_shape(self, universe): + universe.select_atoms(self.selections["static"]), + delta=self.delta, + xdim=1.0, + ydim=2.0, + zdim=2.0, + padding=0.0, + gridcenter=self.gridcenters["static_defined"], + ).run(step=5, **client_DensityAnalysis) + + def test_ValueError_userdefn_gridcenter_shape( + self, universe, client_DensityAnalysis + ): # Test len(gridcenter) != 3 with pytest.raises(ValueError, match="Gridcenter must be a 3D coordinate"): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=self.delta, xdim=10.0, ydim=10.0, zdim=10.0, - gridcenter=self.gridcenters['error1']).run(step=5) + universe.select_atoms(self.selections["static"]), + delta=self.delta, + xdim=10.0, + ydim=10.0, + zdim=10.0, + gridcenter=self.gridcenters["error1"], + ).run(step=5, **client_DensityAnalysis) - def test_ValueError_userdefn_gridcenter_type(self, universe): + def test_ValueError_userdefn_gridcenter_type( + self, universe, client_DensityAnalysis + ): # Test gridcenter includes non-numeric strings with pytest.raises(ValueError, match="Gridcenter must be a 3D coordinate"): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=self.delta, xdim=10.0, ydim=10.0, zdim=10.0, - gridcenter=self.gridcenters['error2']).run(step=5) + universe.select_atoms(self.selections["static"]), + delta=self.delta, + xdim=10.0, + ydim=10.0, + zdim=10.0, + gridcenter=self.gridcenters["error2"], + ).run(step=5, **client_DensityAnalysis) - def test_ValueError_userdefn_gridcenter_missing(self, universe): + def test_ValueError_userdefn_gridcenter_missing( + self, universe, client_DensityAnalysis + ): # Test no gridcenter provided when grid dimensions are given regex = ("Gridcenter or grid dimensions are not provided") with pytest.raises(ValueError, match=regex): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=self.delta, xdim=10.0, ydim=10.0, zdim=10.0).run(step=5) + universe.select_atoms(self.selections["static"]), + delta=self.delta, + xdim=10.0, + ydim=10.0, + zdim=10.0, + ).run(step=5, **client_DensityAnalysis) - def test_ValueError_userdefn_xdim_type(self, universe): + def test_ValueError_userdefn_xdim_type(self, universe, + client_DensityAnalysis): # Test xdim != int or float with pytest.raises(ValueError, match="xdim, ydim, and zdim must be numbers"): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=self.delta, xdim="MDAnalysis", ydim=10.0, zdim=10.0, - gridcenter=self.gridcenters['static_defined']).run(step=5) + universe.select_atoms(self.selections["static"]), + delta=self.delta, + xdim="MDAnalysis", + ydim=10.0, + zdim=10.0, + gridcenter=self.gridcenters["static_defined"], + ).run(step=5, **client_DensityAnalysis) - def test_ValueError_userdefn_xdim_nanvalue(self, universe): + def test_ValueError_userdefn_xdim_nanvalue(self, universe, + client_DensityAnalysis): # Test xdim set to NaN value regex = ("Gridcenter or grid dimensions have NaN element") with pytest.raises(ValueError, match=regex): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static']), - delta=self.delta, xdim=np.nan, ydim=10.0, zdim=10.0, - gridcenter=self.gridcenters['static_defined']).run(step=5) + universe.select_atoms(self.selections["static"]), + delta=self.delta, + xdim=np.nan, + ydim=10.0, + zdim=10.0, + gridcenter=self.gridcenters["static_defined"], + ).run(step=5, **client_DensityAnalysis) - def test_warn_noatomgroup(self, universe): + def test_warn_noatomgroup(self, universe, client_DensityAnalysis): regex = ("No atoms in AtomGroup at input time frame. " "This may be intended; please ensure that " "your grid selection covers the atomic " "positions you wish to capture.") with pytest.warns(UserWarning, match=regex): D = density.DensityAnalysis( - universe.select_atoms(self.selections['none']), - delta=self.delta, xdim=1.0, ydim=2.0, zdim=2.0, padding=0.0, - gridcenter=self.gridcenters['static_defined']).run(step=5) - - def test_ValueError_noatomgroup(self, universe): + universe.select_atoms(self.selections["none"]), + delta=self.delta, + xdim=1.0, + ydim=2.0, + zdim=2.0, + padding=0.0, + gridcenter=self.gridcenters["static_defined"], + ).run(step=5, **client_DensityAnalysis) + + def test_ValueError_noatomgroup(self, universe, client_DensityAnalysis): with pytest.raises(ValueError, match="No atoms in AtomGroup at input" " time frame. Grid for density" " could not be automatically" @@ -374,12 +433,13 @@ def test_ValueError_noatomgroup(self, universe): " defined grid will " "need to be provided instead."): D = density.DensityAnalysis( - universe.select_atoms(self.selections['none'])).run(step=5) + universe.select_atoms(self.selections["none"]) + ).run(step=5, **client_DensityAnalysis) - def test_warn_results_deprecated(self, universe): + def test_warn_results_deprecated(self, universe, client_DensityAnalysis): D = density.DensityAnalysis( universe.select_atoms(self.selections['static'])) - D.run(stop=1) + D.run(stop=1, **client_DensityAnalysis) wmsg = "The `density` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): assert_equal(D.density.grid, D.results.density.grid) From 4f143fdf02ddf9cdb43b0e69edc7cb41bb455556 Mon Sep 17 00:00:00 2001 From: Egor Marin Date: Thu, 19 Dec 2024 23:57:40 +0300 Subject: [PATCH 39/58] add active PRs to black ignore (#4815) --- maintainer/active_files.lst | 137 ++++++++++++++++++++++++++ maintainer/active_files_package.lst | 44 +++++++++ maintainer/active_files_testsuite.lst | 21 ++++ package/pyproject.toml | 48 ++++++++- testsuite/pyproject.toml | 26 ++++- 5 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 maintainer/active_files.lst create mode 100644 maintainer/active_files_package.lst create mode 100644 maintainer/active_files_testsuite.lst diff --git a/maintainer/active_files.lst b/maintainer/active_files.lst new file mode 100644 index 00000000000..525bb70720e --- /dev/null +++ b/maintainer/active_files.lst @@ -0,0 +1,137 @@ +# 4841 add `relprop` for atom selection and corresponding UT +package/MDAnalysis/core/selection.py +testsuite/MDAnalysisTests/core/test_atomselections.py +# --------------------- +# 4822 Enabling of parallelization of `analysis.atomicdistances.AtomicDistances` +package/CHANGELOG +package/MDAnalysis/analysis/atomicdistances.py +testsuite/MDAnalysisTests/analysis/test_atomicdistances.py +# --------------------- +# 4816 Implementation of `CMSParser` +package/MDAnalysis/topology/CMSParser.py +package/MDAnalysis/topology/__init__.py +# --------------------- +# 4815 add active PRs to black ignore +maintainer/active_files_package.lst +maintainer/active_files_testsuite.lst +package/pyproject.toml +testsuite/pyproject.toml +# --------------------- +# 4812 Bump the github-actions group with 4 updates +.github/workflows/deploy.yaml +.github/workflows/gh-ci.yaml +# --------------------- +# 4800 Transfer of `fasteners` dependency to `filelock` +.github/actions/setup-deps/action.yaml +azure-pipelines.yml +maintainer/conda/environment.yml +package/CHANGELOG +package/MDAnalysis/coordinates/XDR.py +package/pyproject.toml +package/requirements.txt +testsuite/MDAnalysisTests/coordinates/test_xdr.py +# --------------------- +# 4790 Implementation of SugarSelection +package/MDAnalysis/core/selection.py +testsuite/MDAnalysisTests/core/test_atomselections.py +testsuite/MDAnalysisTests/data/6kya.pdb +testsuite/MDAnalysisTests/data/GLYCAM_sugars.pdb +testsuite/MDAnalysisTests/datafiles.py +# --------------------- +# 4745 'MDAnalysis.analysis.diffusionmap' parallelization +package/MDAnalysis/analysis/diffusionmap.py +testsuite/MDAnalysisTests/analysis/conftest.py +testsuite/MDAnalysisTests/analysis/test_diffusionmap.py +# --------------------- +# 4738 'MDAnalysis.analysis.align' parallelization +package/MDAnalysis/analysis/align.py +testsuite/MDAnalysisTests/analysis/conftest.py +testsuite/MDAnalysisTests/analysis/test_align.py +# --------------------- +# 4734 distopia 0.3.0 compatibility changes +.github/actions/setup-deps/action.yaml +package/CHANGELOG +package/MDAnalysis/lib/_distopia.py +package/MDAnalysis/lib/distances.py +testsuite/MDAnalysisTests/lib/test_distances.py +# --------------------- +# 4714 Dask timeseries prototype +.gitignore +package/MDAnalysis/analysis/dasktimeseries.py +package/MDAnalysis/coordinates/H5MD.py +tmp/env.yaml +tmp/lazyts.ipynb +# --------------------- +# 4712 Implementing `gemmi`-based mmcif reader (with easy extension to PDB/PDBx and mmJSON) +.github/actions/setup-deps/action.yaml +package/MDAnalysis/coordinates/MMCIF.py +package/MDAnalysis/coordinates/__init__.py +package/MDAnalysis/topology/MMCIFParser.py +package/MDAnalysis/topology/PDBParser.py +package/MDAnalysis/topology/__init__.py +package/MDAnalysis/topology/tpr/utils.py +package/pyproject.toml +testsuite/MDAnalysisTests/coordinates/test_mmcif.py +testsuite/MDAnalysisTests/data/mmcif/1YJP.cif +testsuite/MDAnalysisTests/data/mmcif/1YJP.cif.gz +testsuite/MDAnalysisTests/data/mmcif/7ETN.cif +testsuite/MDAnalysisTests/data/mmcif/7ETN.cif.gz +testsuite/MDAnalysisTests/datafiles.py +testsuite/MDAnalysisTests/topology/test_mmcif.py +# --------------------- +# 4681 [WIP] Cif reader +package/MDAnalysis/coordinates/CIF.py +package/MDAnalysis/coordinates/PDBx.py +package/MDAnalysis/coordinates/__init__.py +package/MDAnalysis/topology/PDBxParser.py +package/MDAnalysis/topology/__init__.py +package/doc/sphinx/source/documentation_pages/coordinates/PDBx.rst +testsuite/MDAnalysisTests/coordinates/test_cif.py +# --------------------- +# 4584 Limit threads usage in numpy during test to avoid time-out +testsuite/MDAnalysisTests/analysis/test_encore.py +testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py +# --------------------- +# 4535 Pathlib object handling for Universe, SingleFrameReaderBase and Toplogy parsers (Issue #3937) +package/CHANGELOG +package/MDAnalysis/coordinates/base.py +package/MDAnalysis/core/universe.py +package/MDAnalysis/topology/base.py +testsuite/MDAnalysisTests/coordinates/base.py +testsuite/MDAnalysisTests/coordinates/test_gro.py +testsuite/MDAnalysisTests/topology/base.py +# --------------------- +# 4533 Fix: modernise HBA to use AG as objects internally instead of selection strings - code only PR (Issue #3933) +package/AUTHORS +package/CHANGELOG +package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis.py +testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py +# --------------------- +# 4417 Accessible Surface Area calculations +package/AUTHORS +package/CHANGELOG +package/MDAnalysis/analysis/sasa.py +package/doc/sphinx/source/documentation_pages/analysis/sasa.rst +package/doc/sphinx/source/references.bib +testsuite/MDAnalysisTests/analysis/test_sasa.py +# --------------------- +# 4334 Deprecate `ts` argument to _read_next_timestep +package/MDAnalysis/coordinates/DCD.py +package/MDAnalysis/coordinates/DLPoly.py +package/MDAnalysis/coordinates/GMS.py +package/MDAnalysis/coordinates/MOL2.py +package/MDAnalysis/coordinates/PDB.py +package/MDAnalysis/coordinates/TRJ.py +package/MDAnalysis/coordinates/TRR.py +package/MDAnalysis/coordinates/TRZ.py +package/MDAnalysis/coordinates/TXYZ.py +package/MDAnalysis/coordinates/XTC.py +package/MDAnalysis/coordinates/XYZ.py +package/MDAnalysis/coordinates/base.py +package/MDAnalysis/coordinates/chain.py +package/MDAnalysis/coordinates/chemfiles.py +package/MDAnalysis/coordinates/memory.py +# --------------------- +# 4046 Added an info method to Universe Class +package/MDAnalysis/core/universe.py +# --------------------- diff --git a/maintainer/active_files_package.lst b/maintainer/active_files_package.lst new file mode 100644 index 00000000000..a1889549c1e --- /dev/null +++ b/maintainer/active_files_package.lst @@ -0,0 +1,44 @@ +package/MDAnalysis/core/selection\.py +| package/MDAnalysis/analysis/atomicdistances\.py +| package/MDAnalysis/topology/CMSParser\.py +| package/MDAnalysis/topology/__init__\.py +| package/MDAnalysis/coordinates/XDR\.py +| package/MDAnalysis/core/selection\.py +| package/MDAnalysis/analysis/diffusionmap\.py +| package/MDAnalysis/analysis/align\.py +| package/MDAnalysis/lib/_distopia\.py +| package/MDAnalysis/lib/distances\.py +| package/MDAnalysis/analysis/dasktimeseries\.py +| package/MDAnalysis/coordinates/H5MD\.py +| package/MDAnalysis/coordinates/MMCIF\.py +| package/MDAnalysis/coordinates/__init__\.py +| package/MDAnalysis/topology/MMCIFParser\.py +| package/MDAnalysis/topology/PDBParser\.py +| package/MDAnalysis/topology/__init__\.py +| package/MDAnalysis/topology/tpr/utils\.py +| package/MDAnalysis/coordinates/CIF\.py +| package/MDAnalysis/coordinates/PDBx\.py +| package/MDAnalysis/coordinates/__init__\.py +| package/MDAnalysis/topology/PDBxParser\.py +| package/MDAnalysis/topology/__init__\.py +| package/MDAnalysis/coordinates/base\.py +| package/MDAnalysis/core/universe\.py +| package/MDAnalysis/topology/base\.py +| package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis\.py +| package/MDAnalysis/analysis/sasa\.py +| package/MDAnalysis/coordinates/DCD\.py +| package/MDAnalysis/coordinates/DLPoly\.py +| package/MDAnalysis/coordinates/GMS\.py +| package/MDAnalysis/coordinates/MOL2\.py +| package/MDAnalysis/coordinates/PDB\.py +| package/MDAnalysis/coordinates/TRJ\.py +| package/MDAnalysis/coordinates/TRR\.py +| package/MDAnalysis/coordinates/TRZ\.py +| package/MDAnalysis/coordinates/TXYZ\.py +| package/MDAnalysis/coordinates/XTC\.py +| package/MDAnalysis/coordinates/XYZ\.py +| package/MDAnalysis/coordinates/base\.py +| package/MDAnalysis/coordinates/chain\.py +| package/MDAnalysis/coordinates/chemfiles\.py +| package/MDAnalysis/coordinates/memory\.py +| package/MDAnalysis/core/universe\.py diff --git a/maintainer/active_files_testsuite.lst b/maintainer/active_files_testsuite.lst new file mode 100644 index 00000000000..4898eafde48 --- /dev/null +++ b/maintainer/active_files_testsuite.lst @@ -0,0 +1,21 @@ +testsuite/MDAnalysisTests/core/test_atomselections\.py +| testsuite/MDAnalysisTests/analysis/test_atomicdistances\.py +| testsuite/MDAnalysisTests/coordinates/test_xdr\.py +| testsuite/MDAnalysisTests/core/test_atomselections\.py +| testsuite/MDAnalysisTests/datafiles\.py +| testsuite/MDAnalysisTests/analysis/conftest\.py +| testsuite/MDAnalysisTests/analysis/test_diffusionmap\.py +| testsuite/MDAnalysisTests/analysis/conftest\.py +| testsuite/MDAnalysisTests/analysis/test_align\.py +| testsuite/MDAnalysisTests/lib/test_distances\.py +| testsuite/MDAnalysisTests/coordinates/test_mmcif\.py +| testsuite/MDAnalysisTests/datafiles\.py +| testsuite/MDAnalysisTests/topology/test_mmcif\.py +| testsuite/MDAnalysisTests/coordinates/test_cif\.py +| testsuite/MDAnalysisTests/analysis/test_encore\.py +| testsuite/MDAnalysisTests/parallelism/test_multiprocessing\.py +| testsuite/MDAnalysisTests/coordinates/base\.py +| testsuite/MDAnalysisTests/coordinates/test_gro\.py +| testsuite/MDAnalysisTests/topology/base\.py +| testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis\.py +| testsuite/MDAnalysisTests/analysis/test_sasa\.py diff --git a/package/pyproject.toml b/package/pyproject.toml index 05bce424867..5853b74a210 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -137,5 +137,51 @@ tables\.py | MDAnalysis/transformations/.*\.py ) ''' -extend-exclude = '__pycache__' +extend-exclude = ''' +( +__pycache__ +| MDAnalysis/core/selection\.py +| MDAnalysis/analysis/atomicdistances\.py +| MDAnalysis/topology/CMSParser\.py +| MDAnalysis/topology/__init__\.py +| MDAnalysis/coordinates/XDR\.py +| MDAnalysis/core/selection\.py +| MDAnalysis/analysis/diffusionmap\.py +| MDAnalysis/analysis/align\.py +| MDAnalysis/analysis/dasktimeseries\.py +| MDAnalysis/coordinates/H5MD\.py +| MDAnalysis/coordinates/MMCIF\.py +| MDAnalysis/coordinates/__init__\.py +| MDAnalysis/topology/MMCIFParser\.py +| MDAnalysis/topology/PDBParser\.py +| MDAnalysis/topology/__init__\.py +| MDAnalysis/topology/tpr/utils\.py +| MDAnalysis/coordinates/CIF\.py +| MDAnalysis/coordinates/PDBx\.py +| MDAnalysis/coordinates/__init__\.py +| MDAnalysis/topology/PDBxParser\.py +| MDAnalysis/topology/__init__\.py +| MDAnalysis/coordinates/base\.py +| MDAnalysis/core/universe\.py +| MDAnalysis/topology/base\.py +| MDAnalysis/analysis/hydrogenbonds/hbond_analysis\.py +| MDAnalysis/analysis/sasa\.py +| MDAnalysis/coordinates/DCD\.py +| MDAnalysis/coordinates/DLPoly\.py +| MDAnalysis/coordinates/GMS\.py +| MDAnalysis/coordinates/MOL2\.py +| MDAnalysis/coordinates/PDB\.py +| MDAnalysis/coordinates/TRJ\.py +| MDAnalysis/coordinates/TRR\.py +| MDAnalysis/coordinates/TRZ\.py +| MDAnalysis/coordinates/TXYZ\.py +| MDAnalysis/coordinates/XTC\.py +| MDAnalysis/coordinates/XYZ\.py +| MDAnalysis/coordinates/base\.py +| MDAnalysis/coordinates/chain\.py +| MDAnalysis/coordinates/chemfiles\.py +| MDAnalysis/coordinates/memory\.py +| MDAnalysis/core/universe\.py +) +''' required-version = '24' diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 359430e131e..9c660c95482 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -164,5 +164,29 @@ setup\.py | MDAnalysisTests/transformations/.*\.py ) ''' -extend-exclude = '__pycache__' +extend-exclude = ''' +( +__pycache__ +| testsuite/MDAnalysisTests/core/test_atomselections\.py +| testsuite/MDAnalysisTests/analysis/test_atomicdistances\.py +| testsuite/MDAnalysisTests/coordinates/test_xdr\.py +| testsuite/MDAnalysisTests/core/test_atomselections\.py +| testsuite/MDAnalysisTests/datafiles\.py +| testsuite/MDAnalysisTests/analysis/conftest\.py +| testsuite/MDAnalysisTests/analysis/test_diffusionmap\.py +| testsuite/MDAnalysisTests/analysis/conftest\.py +| testsuite/MDAnalysisTests/analysis/test_align\.py +| testsuite/MDAnalysisTests/coordinates/test_mmcif\.py +| testsuite/MDAnalysisTests/datafiles\.py +| testsuite/MDAnalysisTests/topology/test_mmcif\.py +| testsuite/MDAnalysisTests/coordinates/test_cif\.py +| testsuite/MDAnalysisTests/analysis/test_encore\.py +| testsuite/MDAnalysisTests/parallelism/test_multiprocessing\.py +| testsuite/MDAnalysisTests/coordinates/base\.py +| testsuite/MDAnalysisTests/coordinates/test_gro\.py +| testsuite/MDAnalysisTests/topology/base\.py +| testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis\.py +| testsuite/MDAnalysisTests/analysis/test_sasa\.py +) +''' required-version = '24' From 9110a6efe2765802856d028e650ebb0117d336bf Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Sat, 21 Dec 2024 09:24:08 +0100 Subject: [PATCH 40/58] [fmt] Format converters modules and tests (#4850) --- package/MDAnalysis/converters/OpenMM.py | 44 +- package/MDAnalysis/converters/OpenMMParser.py | 48 +- package/MDAnalysis/converters/ParmEd.py | 188 ++++-- package/MDAnalysis/converters/ParmEdParser.py | 68 +- package/MDAnalysis/converters/RDKit.py | 157 +++-- package/MDAnalysis/converters/RDKitParser.py | 57 +- package/MDAnalysis/converters/base.py | 5 +- package/pyproject.toml | 1 + .../MDAnalysisTests/converters/test_base.py | 16 +- .../MDAnalysisTests/converters/test_openmm.py | 35 +- .../converters/test_openmm_parser.py | 70 +- .../MDAnalysisTests/converters/test_parmed.py | 162 +++-- .../converters/test_parmed_parser.py | 216 ++++--- .../MDAnalysisTests/converters/test_rdkit.py | 605 ++++++++++-------- .../converters/test_rdkit_parser.py | 108 ++-- testsuite/pyproject.toml | 1 + 16 files changed, 1072 insertions(+), 709 deletions(-) diff --git a/package/MDAnalysis/converters/OpenMM.py b/package/MDAnalysis/converters/OpenMM.py index 227a99ebe59..76a743b7736 100644 --- a/package/MDAnalysis/converters/OpenMM.py +++ b/package/MDAnalysis/converters/OpenMM.py @@ -77,13 +77,17 @@ class OpenMMSimulationReader(base.SingleFrameReaderBase): """ format = "OPENMMSIMULATION" - units = {"time": "ps", "length": "nm", "velocity": "nm/ps", - "force": "kJ/(mol*nm)", "energy": "kJ/mol"} + units = { + "time": "ps", + "length": "nm", + "velocity": "nm/ps", + "force": "kJ/(mol*nm)", + "energy": "kJ/mol", + } @staticmethod def _format_hint(thing): - """Can this reader read *thing*? - """ + """Can this reader read *thing*?""" try: from openmm.app import Simulation except ImportError: @@ -104,20 +108,23 @@ def _read_first_frame(self): self.ts.triclinic_dimensions = self.convert_pos_from_native( self.ts.triclinic_dimensions, inplace=False ) - self.ts.dimensions[3:] = _sanitize_box_angles(self.ts.dimensions[3:]) + self.ts.dimensions[3:] = _sanitize_box_angles( + self.ts.dimensions[3:] + ) self.convert_velocities_from_native(self.ts._velocities) self.convert_forces_from_native(self.ts._forces) self.convert_time_from_native(self.ts.dt) def _mda_timestep_from_omm_context(self): - """ Construct Timestep object from OpenMM context """ + """Construct Timestep object from OpenMM context""" try: import openmm.unit as u except ImportError: # pragma: no cover import simtk.unit as u - state = self.filename.context.getState(-1, getVelocities=True, - getForces=True, getEnergy=True) + state = self.filename.context.getState( + -1, getVelocities=True, getForces=True, getEnergy=True + ) n_atoms = self.filename.context.getSystem().getNumParticles() @@ -125,13 +132,14 @@ def _mda_timestep_from_omm_context(self): ts.frame = 0 ts.data["time"] = state.getTime()._value ts.data["potential_energy"] = ( - state.getPotentialEnergy().in_units_of(u.kilojoule/u.mole)._value + state.getPotentialEnergy().in_units_of(u.kilojoule / u.mole)._value ) ts.data["kinetic_energy"] = ( - state.getKineticEnergy().in_units_of(u.kilojoule/u.mole)._value + state.getKineticEnergy().in_units_of(u.kilojoule / u.mole)._value ) ts.triclinic_dimensions = state.getPeriodicBoxVectors( - asNumpy=True)._value + asNumpy=True + )._value ts.dimensions[3:] = _sanitize_box_angles(ts.dimensions[3:]) ts.positions = state.getPositions(asNumpy=True)._value ts.velocities = state.getVelocities(asNumpy=True)._value @@ -153,8 +161,7 @@ class OpenMMAppReader(base.SingleFrameReaderBase): @staticmethod def _format_hint(thing): - """Can this reader read *thing*? - """ + """Can this reader read *thing*?""" try: from openmm import app except ImportError: @@ -163,8 +170,7 @@ def _format_hint(thing): except ImportError: return False else: - return isinstance(thing, (app.PDBFile, app.Modeller, - app.PDBxFile)) + return isinstance(thing, (app.PDBFile, app.Modeller, app.PDBxFile)) def _read_first_frame(self): self.n_atoms = self.filename.topology.getNumAtoms() @@ -177,10 +183,12 @@ def _read_first_frame(self): self.ts.triclinic_dimensions = self.convert_pos_from_native( self.ts.triclinic_dimensions, inplace=False ) - self.ts.dimensions[3:] = _sanitize_box_angles(self.ts.dimensions[3:]) + self.ts.dimensions[3:] = _sanitize_box_angles( + self.ts.dimensions[3:] + ) def _mda_timestep_from_omm_app(self): - """ Construct Timestep object from OpenMM Application object """ + """Construct Timestep object from OpenMM Application object""" omm_object = self.filename n_atoms = omm_object.topology.getNumAtoms() @@ -198,7 +206,7 @@ def _mda_timestep_from_omm_app(self): def _sanitize_box_angles(angles): - """ Ensure box angles correspond to first quadrant + """Ensure box angles correspond to first quadrant See `discussion on unitcell angles `_ """ diff --git a/package/MDAnalysis/converters/OpenMMParser.py b/package/MDAnalysis/converters/OpenMMParser.py index a8b2866085e..03fa06a60bb 100644 --- a/package/MDAnalysis/converters/OpenMMParser.py +++ b/package/MDAnalysis/converters/OpenMMParser.py @@ -84,9 +84,7 @@ class OpenMMTopologyParser(TopologyReaderBase): @staticmethod def _format_hint(thing): - """Can this Parser read object *thing*? - - """ + """Can this Parser read object *thing*?""" try: from openmm import app except ImportError: @@ -98,7 +96,7 @@ def _format_hint(thing): return isinstance(thing, app.Topology) def _mda_topology_from_omm_topology(self, omm_topology): - """ Construct mda topology from omm topology + """Construct mda topology from omm topology Can be used for any openmm object that contains a topology object @@ -130,9 +128,11 @@ def _mda_topology_from_omm_topology(self, omm_topology): try: from simtk.unit import daltons except ImportError: - msg = ("OpenMM is required for the OpenMMParser but " - "it's not installed. Try installing it with \n" - "conda install -c conda-forge openmm") + msg = ( + "OpenMM is required for the OpenMMParser but " + "it's not installed. Try installing it with \n" + "conda install -c conda-forge openmm" + ) raise ImportError(msg) atom_resindex = [a.residue.index for a in omm_topology.atoms()] @@ -168,19 +168,21 @@ def _mda_topology_from_omm_topology(self, omm_topology): if elem.symbol.capitalize() in SYMB2Z: validated_elements.append(elem.symbol) else: - validated_elements.append('') + validated_elements.append("") atomtypes.append(elem.symbol) masses.append(elem.mass.value_in_unit(daltons)) else: - validated_elements.append('') + validated_elements.append("") masses.append(0.0) - atomtypes.append('X') + atomtypes.append("X") if not all(validated_elements): if any(validated_elements): - 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( + "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. " @@ -189,10 +191,12 @@ def _mda_topology_from_omm_topology(self, omm_topology): "to_guess=['masses', 'types']). " "(for MDAnalysis version 2.x " "this is done automatically," - " but it will be removed in 3.0).") + " but it will be removed in 3.0)." + ) - attrs.append(Elements(np.array(validated_elements, - dtype=object))) + attrs.append( + Elements(np.array(validated_elements, dtype=object)) + ) else: wmsg = ( @@ -205,7 +209,8 @@ def _mda_topology_from_omm_topology(self, omm_topology): "These can be guessed using " "universe.guess_TopologyAttrs(" "to_guess=['masses', 'types']) " - "See MDAnalysis.guessers.") + "See MDAnalysis.guessers." + ) warnings.warn(wmsg) else: @@ -239,9 +244,7 @@ class OpenMMAppTopologyParser(OpenMMTopologyParser): @staticmethod def _format_hint(thing): - """Can this Parser read object *thing*? - - """ + """Can this Parser read object *thing*?""" try: from openmm import app except ImportError: @@ -252,10 +255,7 @@ def _format_hint(thing): else: return isinstance( thing, - ( - app.PDBFile, app.Modeller, - app.Simulation, app.PDBxFile - ) + (app.PDBFile, app.Modeller, app.Simulation, app.PDBxFile), ) def parse(self, **kwargs): diff --git a/package/MDAnalysis/converters/ParmEd.py b/package/MDAnalysis/converters/ParmEd.py index b808f6b1484..69b0c0364e8 100644 --- a/package/MDAnalysis/converters/ParmEd.py +++ b/package/MDAnalysis/converters/ParmEd.py @@ -93,10 +93,11 @@ class ParmEdReader(SingleFrameReaderBase): """Coordinate reader for ParmEd.""" - format = 'PARMED' + + format = "PARMED" # Structure.coordinates always in Angstrom - units = {'time': None, 'length': 'Angstrom'} + units = {"time": None, "length": "Angstrom"} @staticmethod def _format_hint(thing): @@ -115,8 +116,7 @@ def _format_hint(thing): def _read_first_frame(self): self.n_atoms = len(self.filename.atoms) - self.ts = ts = self._Timestep(self.n_atoms, - **self._ts_kwargs) + self.ts = ts = self._Timestep(self.n_atoms, **self._ts_kwargs) if self.filename.coordinates is not None: ts._pos = self.filename.coordinates @@ -129,12 +129,12 @@ def _read_first_frame(self): MDA2PMD = { - 'tempfactor': 'bfactor', - 'gbscreen': 'screen', - 'altLoc': 'altloc', - 'nbindex': 'nb_idx', - 'solventradius': 'solvent_radius', - 'id': 'number' + "tempfactor": "bfactor", + "gbscreen": "screen", + "altLoc": "altloc", + "nbindex": "nb_idx", + "solventradius": "solvent_radius", + "id": "number", } @@ -160,8 +160,8 @@ class ParmEdConverter(base.ConverterBase): """ - lib = 'PARMED' - units = {'time': None, 'length': 'Angstrom'} + lib = "PARMED" + units = {"time": None, "length": "Angstrom"} def convert(self, obj): """Write selection at current trajectory frame to :class:`~parmed.structure.Structure`. @@ -181,9 +181,11 @@ def convert(self, obj): if NumpyVersion(np.__version__) >= "2.0.0": ermsg = "ParmEd is not compatible with NumPy 2.0+" else: - ermsg = ("ParmEd is required for ParmEdConverter but is not " - "installed. Try installing it with \n" - "pip install parmed") + ermsg = ( + "ParmEd is required for ParmEdConverter but is not " + "installed. Try installing it with \n" + "pip install parmed" + ) raise ImportError(errmsg) try: # make sure to use atoms (Issue 46) @@ -196,63 +198,80 @@ def convert(self, obj): try: names = ag_or_ts.names except (AttributeError, NoDataError): - names = itertools.cycle(('X',)) - missing_topology.append('names') + names = itertools.cycle(("X",)) + missing_topology.append("names") try: resnames = ag_or_ts.resnames except (AttributeError, NoDataError): - resnames = itertools.cycle(('UNK',)) - missing_topology.append('resnames') + resnames = itertools.cycle(("UNK",)) + missing_topology.append("resnames") if missing_topology: warnings.warn( "Supplied AtomGroup was missing the following attributes: " "{miss}. These will be written with default values. " "Alternatively these can be supplied as keyword arguments." - "".format(miss=', '.join(missing_topology))) + "".format(miss=", ".join(missing_topology)) + ) try: positions = ag_or_ts.positions except (AttributeError, NoDataError): - positions = [None]*ag_or_ts.n_atoms + positions = [None] * ag_or_ts.n_atoms try: velocities = ag_or_ts.velocities except (AttributeError, NoDataError): - velocities = [None]*ag_or_ts.n_atoms + velocities = [None] * ag_or_ts.n_atoms atom_kwargs = [] - for atom, name, resname, xyz, vel in zip(ag_or_ts, names, resnames, - positions, velocities): - akwargs = {'name': name} - chain_seg = {'segid': atom.segid} - for attrname in ('mass', 'charge', 'type', - 'altLoc', 'tempfactor', - 'occupancy', 'gbscreen', 'solventradius', - 'nbindex', 'rmin', 'epsilon', 'rmin14', - 'epsilon14', 'id'): + for atom, name, resname, xyz, vel in zip( + ag_or_ts, names, resnames, positions, velocities + ): + akwargs = {"name": name} + chain_seg = {"segid": atom.segid} + for attrname in ( + "mass", + "charge", + "type", + "altLoc", + "tempfactor", + "occupancy", + "gbscreen", + "solventradius", + "nbindex", + "rmin", + "epsilon", + "rmin14", + "epsilon14", + "id", + ): try: - akwargs[MDA2PMD.get(attrname, attrname)] = getattr(atom, attrname) + akwargs[MDA2PMD.get(attrname, attrname)] = getattr( + atom, attrname + ) except AttributeError: pass try: el = atom.element.lower().capitalize() - akwargs['atomic_number'] = SYMB2Z[el] + akwargs["atomic_number"] = SYMB2Z[el] except (KeyError, AttributeError): try: tp = atom.type.lower().capitalize() - akwargs['atomic_number'] = SYMB2Z[tp] + akwargs["atomic_number"] = SYMB2Z[tp] except (KeyError, AttributeError): pass try: - chain_seg['chain'] = atom.chainID + chain_seg["chain"] = atom.chainID except AttributeError: pass try: - chain_seg['inscode'] = atom.icode + chain_seg["inscode"] = atom.icode except AttributeError: pass - atom_kwargs.append((akwargs, resname, atom.resid, chain_seg, xyz, vel)) + atom_kwargs.append( + (akwargs, resname, atom.resid, chain_seg, xyz, vel) + ) struct = pmd.Structure() @@ -264,9 +283,12 @@ def convert(self, obj): if vel is not None: atom.vx, atom.vy, atom.vz = vel - atom.atom_type = pmd.AtomType(akwarg['name'], None, - akwarg['mass'], - atomic_number=akwargs.get('atomic_number')) + atom.atom_type = pmd.AtomType( + akwarg["name"], + None, + akwarg["mass"], + atomic_number=akwargs.get("atomic_number"), + ) struct.add_atom(atom, resname, resid, **kw) try: @@ -274,12 +296,15 @@ def convert(self, obj): except AttributeError: struct.box = None - if hasattr(ag_or_ts, 'universe'): - atomgroup = {atom: index for index, - atom in enumerate(list(ag_or_ts))} - get_atom_indices = functools.partial(get_indices_from_subset, - atomgroup=atomgroup, - universe=ag_or_ts.universe) + if hasattr(ag_or_ts, "universe"): + atomgroup = { + atom: index for index, atom in enumerate(list(ag_or_ts)) + } + get_atom_indices = functools.partial( + get_indices_from_subset, + atomgroup=atomgroup, + universe=ag_or_ts.universe, + ) else: get_atom_indices = lambda x: x @@ -290,8 +315,9 @@ def convert(self, obj): pass else: for p in params: - atoms = [struct.atoms[i] for i in map(get_atom_indices, - p.indices)] + atoms = [ + struct.atoms[i] for i in map(get_atom_indices, p.indices) + ] try: for obj in p.type: bond = pmd.Bond(*atoms, type=obj.type, order=obj.order) @@ -301,7 +327,7 @@ def convert(self, obj): bond.type.list = struct.bond_types except (TypeError, AttributeError): order = p.order if p.order is not None else 1 - btype = getattr(p.type, 'type', None) + btype = getattr(p.type, "type", None) bond = pmd.Bond(*atoms, type=btype, order=order) struct.bonds.append(bond) @@ -311,40 +337,62 @@ def convert(self, obj): # dihedrals try: - params = ag_or_ts.dihedrals.atomgroup_intersection(ag_or_ts, - strict=True) + params = ag_or_ts.dihedrals.atomgroup_intersection( + ag_or_ts, strict=True + ) except AttributeError: pass else: for p in params: - atoms = [struct.atoms[i] for i in map(get_atom_indices, - p.indices)] + atoms = [ + struct.atoms[i] for i in map(get_atom_indices, p.indices) + ] try: for obj in p.type: - imp = getattr(obj, 'improper', False) - ign = getattr(obj, 'ignore_end', False) - dih = pmd.Dihedral(*atoms, type=obj.type, - ignore_end=ign, improper=imp) + imp = getattr(obj, "improper", False) + ign = getattr(obj, "ignore_end", False) + dih = pmd.Dihedral( + *atoms, type=obj.type, ignore_end=ign, improper=imp + ) struct.dihedrals.append(dih) if isinstance(dih.type, pmd.DihedralType): struct.dihedral_types.append(dih.type) dih.type.list = struct.dihedral_types except (TypeError, AttributeError): - btype = getattr(p.type, 'type', None) - imp = getattr(p.type, 'improper', False) - ign = getattr(p.type, 'ignore_end', False) - dih = pmd.Dihedral(*atoms, type=btype, - improper=imp, ignore_end=ign) + btype = getattr(p.type, "type", None) + imp = getattr(p.type, "improper", False) + ign = getattr(p.type, "ignore_end", False) + dih = pmd.Dihedral( + *atoms, type=btype, improper=imp, ignore_end=ign + ) struct.dihedrals.append(dih) if isinstance(dih.type, pmd.DihedralType): struct.dihedral_types.append(dih.type) dih.type.list = struct.dihedral_types for param, pmdtype, trackedlist, typelist, clstype in ( - ('ureybradleys', pmd.UreyBradley, struct.urey_bradleys, struct.urey_bradley_types, pmd.BondType), - ('angles', pmd.Angle, struct.angles, struct.angle_types, pmd.AngleType), - ('impropers', pmd.Improper, struct.impropers, struct.improper_types, pmd.ImproperType), - ('cmaps', pmd.Cmap, struct.cmaps, struct.cmap_types, pmd.CmapType) + ( + "ureybradleys", + pmd.UreyBradley, + struct.urey_bradleys, + struct.urey_bradley_types, + pmd.BondType, + ), + ( + "angles", + pmd.Angle, + struct.angles, + struct.angle_types, + pmd.AngleType, + ), + ( + "impropers", + pmd.Improper, + struct.impropers, + struct.improper_types, + pmd.ImproperType, + ), + ("cmaps", pmd.Cmap, struct.cmaps, struct.cmap_types, pmd.CmapType), ): try: params = getattr(ag_or_ts, param) @@ -353,8 +401,10 @@ def convert(self, obj): pass else: for v in values: - atoms = [struct.atoms[i] for i in map(get_atom_indices, - v.indices)] + atoms = [ + struct.atoms[i] + for i in map(get_atom_indices, v.indices) + ] try: for parmed_obj in v.type: @@ -364,7 +414,7 @@ def convert(self, obj): typelist.append(p.type) p.type.list = typelist except (TypeError, AttributeError): - vtype = getattr(v.type, 'type', None) + vtype = getattr(v.type, "type", None) p = pmdtype(*atoms, type=vtype) trackedlist.append(p) diff --git a/package/MDAnalysis/converters/ParmEdParser.py b/package/MDAnalysis/converters/ParmEdParser.py index 31ed9bee410..331f14e279a 100644 --- a/package/MDAnalysis/converters/ParmEdParser.py +++ b/package/MDAnalysis/converters/ParmEdParser.py @@ -118,7 +118,7 @@ Angles, Dihedrals, Impropers, - CMaps + CMaps, ) from ..core.topology import Topology @@ -136,7 +136,8 @@ class ParmEdParser(TopologyReaderBase): """ For ParmEd structures """ - format = 'PARMED' + + format = "PARMED" @staticmethod def _format_hint(thing): @@ -228,30 +229,27 @@ def parse(self, **kwargs): try: elements.append(Z2SYMB[z]) except KeyError: - elements.append('') + elements.append("") # Make Atom TopologyAttrs for vals, Attr, dtype in ( - (names, Atomnames, object), - (masses, Masses, np.float32), - (charges, Charges, np.float32), - (types, Atomtypes, object), - (elements, Elements, object), - (serials, Atomids, np.int32), - (chainids, ChainIDs, object), - - (altLocs, AltLocs, object), - (bfactors, Tempfactors, np.float32), - (occupancies, Occupancies, np.float32), - - (screens, GBScreens, np.float32), - (solvent_radii, SolventRadii, np.float32), - (nonbonded_indices, NonbondedIndices, np.int32), - - (rmins, RMins, np.float32), - (epsilons, Epsilons, np.float32), - (rmin14s, RMin14s, np.float32), - (epsilon14s, Epsilon14s, np.float32), + (names, Atomnames, object), + (masses, Masses, np.float32), + (charges, Charges, np.float32), + (types, Atomtypes, object), + (elements, Elements, object), + (serials, Atomids, np.int32), + (chainids, ChainIDs, object), + (altLocs, AltLocs, object), + (bfactors, Tempfactors, np.float32), + (occupancies, Occupancies, np.float32), + (screens, GBScreens, np.float32), + (solvent_radii, SolventRadii, np.float32), + (nonbonded_indices, NonbondedIndices, np.int32), + (rmins, RMins, np.float32), + (epsilons, Epsilons, np.float32), + (rmin14s, RMin14s, np.float32), + (epsilon14s, Epsilon14s, np.float32), ): attrs.append(Attr(np.array(vals, dtype=dtype))) @@ -262,7 +260,8 @@ def parse(self, **kwargs): residx, (resids, resnames, chainids, segids) = change_squash( (resids, resnames, chainids, segids), - (resids, resnames, chainids, segids)) + (resids, resnames, chainids, segids), + ) n_residues = len(resids) attrs.append(Resids(resids)) @@ -311,8 +310,11 @@ def parse(self, **kwargs): bond_types = list(map(squash_identical, bond_types)) bond_orders = list(map(squash_identical, bond_orders)) - attrs.append(Bonds(bond_values, types=bond_types, guessed=False, - order=bond_orders)) + attrs.append( + Bonds( + bond_values, types=bond_types, guessed=False, order=bond_orders + ) + ) for pmdlist, na, values, types in ( (structure.urey_bradleys, 2, ub_values, ub_types), @@ -323,7 +325,7 @@ def parse(self, **kwargs): ): for p in pmdlist: - atoms = ['atom{}'.format(i) for i in range(1, na+1)] + atoms = ["atom{}".format(i) for i in range(1, na + 1)] idx = tuple(getattr(p, a).idx for a in atoms) if idx not in values: values[idx] = [p] @@ -345,9 +347,13 @@ def parse(self, **kwargs): types = list(map(squash_identical, types)) attrs.append(Attr(vals, types=types, guessed=False, order=None)) - top = Topology(n_atoms, n_residues, n_segments, - attrs=attrs, - atom_resindex=residx, - residue_segindex=segidx) + top = Topology( + n_atoms, + n_residues, + n_segments, + attrs=attrs, + atom_resindex=residx, + residue_segindex=segidx, + ) return top diff --git a/package/MDAnalysis/converters/RDKit.py b/package/MDAnalysis/converters/RDKit.py index aa78a7ea0c8..c9348cb8fd4 100644 --- a/package/MDAnalysis/converters/RDKit.py +++ b/package/MDAnalysis/converters/RDKit.py @@ -128,8 +128,18 @@ # anion charges are directly handled by the code using the typical valence # of the atom MONATOMIC_CATION_CHARGES = { - 3: 1, 11: 1, 19: 1, 37: 1, 47: 1, 55: 1, - 12: 2, 20: 2, 29: 2, 30: 2, 38: 2, 56: 2, + 3: 1, + 11: 1, + 19: 1, + 37: 1, + 47: 1, + 55: 1, + 12: 2, + 20: 2, + 29: 2, + 30: 2, + 38: 2, + 56: 2, 26: 2, # Fe could also be +3 13: 3, } @@ -153,10 +163,11 @@ class RDKitReader(memory.MemoryReader): .. versionadded:: 2.0.0 """ - format = 'RDKIT' + + format = "RDKIT" # Structure.coordinates always in Angstrom - units = {'time': None, 'length': 'Angstrom'} + units = {"time": None, "length": "Angstrom"} @staticmethod def _format_hint(thing): @@ -180,14 +191,15 @@ def __init__(self, filename, **kwargs): RDKit molecule """ n_atoms = filename.GetNumAtoms() - coordinates = np.array([ - conf.GetPositions() for conf in filename.GetConformers()], - dtype=np.float32) + coordinates = np.array( + [conf.GetPositions() for conf in filename.GetConformers()], + dtype=np.float32, + ) if coordinates.size == 0: warnings.warn("No coordinates found in the RDKit molecule") coordinates = np.empty((1, n_atoms, 3), dtype=np.float32) coordinates[:] = np.nan - super(RDKitReader, self).__init__(coordinates, order='fac', **kwargs) + super(RDKitReader, self).__init__(coordinates, order="fac", **kwargs) class RDKitConverter(base.ConverterBase): @@ -313,11 +325,12 @@ class RDKitConverter(base.ConverterBase): """ - lib = 'RDKIT' - units = {'time': None, 'length': 'Angstrom'} + lib = "RDKIT" + units = {"time": None, "length": "Angstrom"} - def convert(self, obj, cache=True, NoImplicit=True, max_iter=200, - force=False): + def convert( + self, obj, cache=True, NoImplicit=True, max_iter=200, force=False + ): """Write selection at current trajectory frame to :class:`~rdkit.Chem.rdchem.Mol`. @@ -342,16 +355,19 @@ def convert(self, obj, cache=True, NoImplicit=True, max_iter=200, try: from rdkit import Chem except ImportError: - raise ImportError("RDKit is required for the RDKitConverter but " - "it's not installed. Try installing it with \n" - "conda install -c conda-forge rdkit") + raise ImportError( + "RDKit is required for the RDKitConverter but " + "it's not installed. Try installing it with \n" + "conda install -c conda-forge rdkit" + ) try: # make sure to use atoms (Issue 46) ag = obj.atoms except AttributeError: - raise TypeError("No `atoms` attribute in object of type {}, " - "please use a valid AtomGroup or Universe".format( - type(obj))) from None + raise TypeError( + "No `atoms` attribute in object of type {}, " + "please use a valid AtomGroup or Universe".format(type(obj)) + ) from None # parameters passed to atomgroup_to_mol kwargs = dict(NoImplicit=NoImplicit, max_iter=max_iter, force=force) @@ -366,9 +382,11 @@ def convert(self, obj, cache=True, NoImplicit=True, max_iter=200, 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") + 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()) @@ -409,7 +427,8 @@ def atomgroup_to_mol(ag, NoImplicit=True, max_iter=200, force=False): "The `elements` attribute is required for the RDKitConverter " "but is not present in this AtomGroup. Please refer to the " "documentation to guess elements from other attributes or " - "type `help(MDAnalysis.topology.guessers)`") from None + "type `help(MDAnalysis.topology.guessers)`" + ) from None if "H" not in ag.elements: if force: @@ -424,7 +443,8 @@ def atomgroup_to_mol(ag, NoImplicit=True, max_iter=200, force=False): "the parameter ``NoImplicit=False`` when using the converter " "to allow implicit hydrogens and disable inferring bond " "orders and charges. You can also use ``force=True`` to " - "ignore this error.") + "ignore this error." + ) # attributes accepted in PDBResidueInfo object pdb_attrs = {} @@ -433,9 +453,12 @@ def atomgroup_to_mol(ag, NoImplicit=True, max_iter=200, force=False): pdb_attrs[attr] = getattr(ag, attr) resnames = pdb_attrs.get("resnames", None) if resnames is None: + def get_resname(idx): return "" + else: + def get_resname(idx): return resnames[idx] @@ -473,7 +496,8 @@ def get_resname(idx): except NoDataError: warnings.warn( "No `bonds` attribute in this AtomGroup. Guessing bonds based " - "on atoms coordinates") + "on atoms coordinates" + ) ag.guess_bonds() for bond in ag.bonds: @@ -492,15 +516,17 @@ def get_resname(idx): mol = _standardize_patterns(mol, max_iter) # reorder atoms to match MDAnalysis, since the reactions from # _standardize_patterns will mess up the original order - order = np.argsort([atom.GetIntProp("_MDAnalysis_index") - for atom in mol.GetAtoms()]) + order = np.argsort( + [atom.GetIntProp("_MDAnalysis_index") for atom in mol.GetAtoms()] + ) mol = Chem.RenumberAtoms(mol, order.astype(int).tolist()) # sanitize if possible err = Chem.SanitizeMol(mol, catchErrors=True) if err: - warnings.warn("Could not sanitize molecule: " - f"failed during step {err!r}") + warnings.warn( + "Could not sanitize molecule: " f"failed during step {err!r}" + ) return mol @@ -515,7 +541,7 @@ def set_converter_cache_size(maxsize): conversions in memory. Using ``maxsize=None`` will remove all limits to the cache size, i.e. everything is cached. """ - global atomgroup_to_mol # pylint: disable=global-statement + global atomgroup_to_mol # pylint: disable=global-statement atomgroup_to_mol = lru_cache(maxsize=maxsize)(atomgroup_to_mol.__wrapped__) @@ -597,9 +623,12 @@ def _atom_sorter(atom): order and charge infering code to get the correct state on the first try. Currently sorts by number of unpaired electrons, then by number of heavy atom neighbors (i.e. atoms at the edge first).""" - num_heavy_neighbors = len([ - neighbor for neighbor in atom.GetNeighbors() - if neighbor.GetAtomicNum() > 1] + num_heavy_neighbors = len( + [ + neighbor + for neighbor in atom.GetNeighbors() + if neighbor.GetAtomicNum() > 1 + ] ) return (-_get_nb_unpaired_electrons(atom)[0], num_heavy_neighbors) @@ -630,16 +659,19 @@ def _infer_bo_and_charges(mol): """ # heavy atoms sorted by number of heavy atom neighbors (lower first) then # NUE (higher first) - atoms = sorted([atom for atom in mol.GetAtoms() - if atom.GetAtomicNum() > 1], - key=_atom_sorter) + atoms = sorted( + [atom for atom in mol.GetAtoms() if atom.GetAtomicNum() > 1], + key=_atom_sorter, + ) for atom in atoms: # monatomic ions if atom.GetDegree() == 0: - atom.SetFormalCharge(MONATOMIC_CATION_CHARGES.get( - atom.GetAtomicNum(), - -_get_nb_unpaired_electrons(atom)[0])) + atom.SetFormalCharge( + MONATOMIC_CATION_CHARGES.get( + atom.GetAtomicNum(), -_get_nb_unpaired_electrons(atom)[0] + ) + ) mol.UpdatePropertyCache(strict=False) continue # get NUE for each possible valence @@ -654,8 +686,11 @@ def _infer_bo_and_charges(mol): if (len(nue) == 1) and (nue[0] <= 0): continue else: - neighbors = sorted(atom.GetNeighbors(), reverse=True, - key=lambda a: _get_nb_unpaired_electrons(a)[0]) + neighbors = sorted( + atom.GetNeighbors(), + reverse=True, + key=lambda a: _get_nb_unpaired_electrons(a)[0], + ) # check if one of the neighbors has a common NUE for na in neighbors: # get NUE for the neighbor @@ -663,13 +698,12 @@ def _infer_bo_and_charges(mol): # smallest common NUE common_nue = min( min([i for i in nue if i >= 0], default=0), - min([i for i in na_nue if i >= 0], default=0) + min([i for i in na_nue if i >= 0], default=0), ) # a common NUE of 0 means we don't need to do anything if common_nue != 0: # increase bond order - bond = mol.GetBondBetweenAtoms( - atom.GetIdx(), na.GetIdx()) + bond = mol.GetBondBetweenAtoms(atom.GetIdx(), na.GetIdx()) order = common_nue + 1 bond.SetBondType(RDBONDORDER[order]) mol.UpdatePropertyCache(strict=False) @@ -843,14 +877,17 @@ def _rebuild_conjugated_bonds(mol, max_iter=200): pattern = Chem.MolFromSmarts("[*-{1-2}]-,=[*+0]=,#[*+0]") # pattern used to finish fixing a series of conjugated bonds base_end_pattern = Chem.MolFromSmarts( - "[*-{1-2}]-,=[*+0]=,#[*+0]-,=[*-{1-2}]") + "[*-{1-2}]-,=[*+0]=,#[*+0]-,=[*-{1-2}]" + ) # used when there's an odd number of matches for `pattern` odd_end_pattern = Chem.MolFromSmarts( "[*-]-[*+0]=[*+0]-[*-,$([#7;X3;v3]),$([#6+0,#7+1]=O)," - "$([S;D4;v4]-[O-])]") + "$([S;D4;v4]-[O-])]" + ) # number of unique matches with the pattern - n_matches = len(set([match[0] - for match in mol.GetSubstructMatches(pattern)])) + n_matches = len( + set([match[0] for match in mol.GetSubstructMatches(pattern)]) + ) # nothing to standardize if n_matches == 0: return @@ -886,21 +923,27 @@ def _rebuild_conjugated_bonds(mol, max_iter=200): ): for neighbor in term_atom.GetNeighbors(): bond = mol.GetBondBetweenAtoms(anion2, neighbor.GetIdx()) - if (neighbor.GetAtomicNum() == 8 and - bond.GetBondTypeAsDouble() == 2): + if ( + neighbor.GetAtomicNum() == 8 + and bond.GetBondTypeAsDouble() == 2 + ): bond.SetBondType(Chem.BondType.SINGLE) neighbor.SetFormalCharge(-1) break # edge-case 2: S=O # [*-]-*=*-[Sv4]-[O-] --> *=*-*=[Sv6]=O # transform -[O-] to =O - elif (term_atom.GetAtomicNum() == 16 and - term_atom.GetFormalCharge() == 0): + elif ( + term_atom.GetAtomicNum() == 16 + and term_atom.GetFormalCharge() == 0 + ): for neighbor in term_atom.GetNeighbors(): bond = mol.GetBondBetweenAtoms(anion2, neighbor.GetIdx()) - if (neighbor.GetAtomicNum() == 8 and - neighbor.GetFormalCharge() == -1 and - bond.GetBondTypeAsDouble() == 1): + if ( + neighbor.GetAtomicNum() == 8 + and neighbor.GetFormalCharge() == -1 + and bond.GetBondTypeAsDouble() == 1 + ): bond.SetBondType(Chem.BondType.DOUBLE) neighbor.SetFormalCharge(0) break @@ -976,5 +1019,7 @@ def _rebuild_conjugated_bonds(mol, max_iter=200): return # reached max_iter - warnings.warn("The standardization could not be completed within a " - "reasonable number of iterations") + warnings.warn( + "The standardization could not be completed within a " + "reasonable number of iterations" + ) diff --git a/package/MDAnalysis/converters/RDKitParser.py b/package/MDAnalysis/converters/RDKitParser.py index 24c730ac061..b837ea60a80 100644 --- a/package/MDAnalysis/converters/RDKitParser.py +++ b/package/MDAnalysis/converters/RDKitParser.py @@ -160,7 +160,8 @@ class RDKitParser(TopologyReaderBase): the type attribute get the same values as the element attribute. """ - format = 'RDKIT' + + format = "RDKIT" @staticmethod def _format_hint(thing): @@ -203,19 +204,24 @@ def parse(self, **kwargs): try: atom = mol.GetAtomWithIdx(0) except RuntimeError: - top = Topology(n_atoms=0, n_res=0, n_seg=0, - attrs=None, - atom_resindex=None, - residue_segindex=None) + top = Topology( + n_atoms=0, + n_res=0, + n_seg=0, + attrs=None, + atom_resindex=None, + residue_segindex=None, + ) return top # check if multiple charges present - if atom.HasProp('_GasteigerCharge') and ( - atom.HasProp('_TriposPartialCharge') + if atom.HasProp("_GasteigerCharge") and ( + atom.HasProp("_TriposPartialCharge") ): warnings.warn( - 'Both _GasteigerCharge and _TriposPartialCharge properties ' - 'are present. Using Gasteiger charges by default.') + "Both _GasteigerCharge and _TriposPartialCharge properties " + "are present. Using Gasteiger charges by default." + ) for atom in mol.GetAtoms(): ids.append(atom.GetIdx()) @@ -224,7 +230,7 @@ def parse(self, **kwargs): aromatics.append(atom.GetIsAromatic()) chiralities.append(_rdkit_atom_to_RS(atom)) mi = atom.GetMonomerInfo() - if mi: # atom name and residue info are present + if mi: # atom name and residue info are present names.append(mi.GetName().strip()) resnums.append(mi.GetResidueNumber()) resnames.append(mi.GetResidueName()) @@ -237,23 +243,25 @@ def parse(self, **kwargs): else: # atom name (MOL2 only) try: - names.append(atom.GetProp('_TriposAtomName')) + names.append(atom.GetProp("_TriposAtomName")) except KeyError: pass # atom type (MOL2 only) try: - atomtypes.append(atom.GetProp('_TriposAtomType')) + atomtypes.append(atom.GetProp("_TriposAtomType")) except KeyError: pass # gasteiger charge (computed): # if the user took the time to compute them, make it a priority # over charges read from a MOL2 file try: - charges.append(atom.GetDoubleProp('_GasteigerCharge')) + charges.append(atom.GetDoubleProp("_GasteigerCharge")) except KeyError: # partial charge (MOL2 only) try: - charges.append(atom.GetDoubleProp('_TriposPartialCharge')) + charges.append( + atom.GetDoubleProp("_TriposPartialCharge") + ) except KeyError: pass @@ -277,7 +285,7 @@ def parse(self, **kwargs): (elements, Elements, object), (masses, Masses, np.float32), (aromatics, Aromaticities, bool), - (chiralities, RSChirality, 'U1'), + (chiralities, RSChirality, "U1"), ): attrs.append(Attr(np.array(vals, dtype=dtype))) @@ -312,7 +320,7 @@ def parse(self, **kwargs): if charges: attrs.append(Charges(np.array(charges, dtype=np.float32))) else: - pass # no guesser yet + pass # no guesser yet # PDB only for vals, Attr, dtype in ( @@ -332,7 +340,8 @@ def parse(self, **kwargs): icodes = np.array(icodes, dtype=object) residx, (resnums, resnames, icodes, segids) = change_squash( (resnums, resnames, icodes, segids), - (resnums, resnames, icodes, segids)) + (resnums, resnames, icodes, segids), + ) n_residues = len(resnums) for vals, Attr, dtype in ( (resnums, Resids, np.int32), @@ -354,13 +363,17 @@ def parse(self, **kwargs): attrs.append(Segids(segids)) else: n_segments = 1 - attrs.append(Segids(np.array(['SYSTEM'], dtype=object))) + attrs.append(Segids(np.array(["SYSTEM"], dtype=object))) segidx = None # create topology - top = Topology(n_atoms, n_residues, n_segments, - attrs=attrs, - atom_resindex=residx, - residue_segindex=segidx) + top = Topology( + n_atoms, + n_residues, + n_segments, + attrs=attrs, + atom_resindex=residx, + residue_segindex=segidx, + ) return top diff --git a/package/MDAnalysis/converters/base.py b/package/MDAnalysis/converters/base.py index 234d4f7da4e..5f3d0e32509 100644 --- a/package/MDAnalysis/converters/base.py +++ b/package/MDAnalysis/converters/base.py @@ -41,7 +41,7 @@ class _Convertermeta(type): def __init__(cls, name, bases, classdict): type.__init__(type, name, bases, classdict) try: - fmt = asiterable(classdict['lib']) + fmt = asiterable(classdict["lib"]) except KeyError: pass else: @@ -51,8 +51,7 @@ def __init__(cls, name, bases, classdict): class ConverterBase(IOBase, metaclass=_Convertermeta): - """Base class for converting to other libraries. - """ + """Base class for converting to other libraries.""" def __repr__(self): return "<{cls}>".format(cls=self.__class__.__name__) diff --git a/package/pyproject.toml b/package/pyproject.toml index 5853b74a210..7699f7c3b47 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -135,6 +135,7 @@ tables\.py | MDAnalysis/visualization/.*\.py | MDAnalysis/lib/.*\.py^ | MDAnalysis/transformations/.*\.py +| MDAnalysis/converters/.*\.py ) ''' extend-exclude = ''' diff --git a/testsuite/MDAnalysisTests/converters/test_base.py b/testsuite/MDAnalysisTests/converters/test_base.py index 99a4deca03d..a8bebca0c31 100644 --- a/testsuite/MDAnalysisTests/converters/test_base.py +++ b/testsuite/MDAnalysisTests/converters/test_base.py @@ -28,18 +28,22 @@ def test_coordinate_converterbase_warning(): from MDAnalysis.coordinates.base import ConverterBase import MDAnalysis.converters.base - wmsg = ("ConverterBase moved from coordinates.base." - "ConverterBase to converters.base.ConverterBase " - "and will be removed from coordinates.base " - "in MDAnalysis release 3.0.0") + wmsg = ( + "ConverterBase moved from coordinates.base." + "ConverterBase to converters.base.ConverterBase " + "and will be removed from coordinates.base " + "in MDAnalysis release 3.0.0" + ) with pytest.warns(DeprecationWarning, match=wmsg): + class DerivedConverter(ConverterBase): pass assert issubclass(DerivedConverter, ConverterBase) - assert not issubclass(DerivedConverter, - MDAnalysis.converters.base.ConverterBase) + assert not issubclass( + DerivedConverter, MDAnalysis.converters.base.ConverterBase + ) def test_converters_converterbase_no_warning(): diff --git a/testsuite/MDAnalysisTests/converters/test_openmm.py b/testsuite/MDAnalysisTests/converters/test_openmm.py index 4405547b8c2..0983583d563 100644 --- a/testsuite/MDAnalysisTests/converters/test_openmm.py +++ b/testsuite/MDAnalysisTests/converters/test_openmm.py @@ -43,7 +43,7 @@ pytest.skip(allow_module_level=True) -class TestOpenMMBasicSimulationReader(): +class TestOpenMMBasicSimulationReader: @pytest.fixture def omm_sim_uni(self): system = mm.System() @@ -57,7 +57,8 @@ def omm_sim_uni(self): positions = np.ones((5, 3)) * unit.angstrom integrator = mm.LangevinIntegrator( 273 * unit.kelvin, - 1.0 / unit.picoseconds, 2.0 * unit.femtoseconds, + 1.0 / unit.picoseconds, + 2.0 * unit.femtoseconds, ) simulation = app.Simulation(topology, system, integrator) simulation.context.setPositions(positions) @@ -67,11 +68,13 @@ def omm_sim_uni(self): def test_dimensions(self, omm_sim_uni): assert_allclose( omm_sim_uni.trajectory.ts.dimensions, - np.array([20., 20., 20., 90., 90., 90.]), + np.array([20.0, 20.0, 20.0, 90.0, 90.0, 90.0]), rtol=0, atol=1e-3, - err_msg=("OpenMMBasicSimulationReader failed to get unitcell " - "dimensions from OpenMM Simulation Object"), + err_msg=( + "OpenMMBasicSimulationReader failed to get unitcell " + "dimensions from OpenMM Simulation Object" + ), ) def test_coordinates(self, omm_sim_uni): @@ -84,7 +87,7 @@ def test_basic_topology(self, omm_sim_uni): assert omm_sim_uni.residues.n_residues == 1 assert omm_sim_uni.residues.resnames[0] == "RES" assert omm_sim_uni.segments.n_segments == 1 - assert omm_sim_uni.segments.segids[0] == '0' + assert omm_sim_uni.segments.segids[0] == "0" assert len(omm_sim_uni.bonds.indices) == 0 def test_data(self, omm_sim_uni): @@ -107,8 +110,10 @@ def test_dimensions(self): self.ref.trajectory.ts.dimensions, rtol=0, atol=1e-3, - err_msg=("OpenMMPDBFileReader failed to get unitcell dimensions " - "from OpenMMPDBFile"), + err_msg=( + "OpenMMPDBFileReader failed to get unitcell dimensions " + "from OpenMMPDBFile" + ), ) def test_coordinates(self): @@ -133,8 +138,10 @@ def test_dimensions(self): self.ref.trajectory.ts.dimensions, rtol=0, atol=1e-3, - err_msg=("OpenMMModellerReader failed to get unitcell dimensions " - "from OpenMMModeller"), + err_msg=( + "OpenMMModellerReader failed to get unitcell dimensions " + "from OpenMMModeller" + ), ) def test_coordinates(self): @@ -167,8 +174,10 @@ def test_dimensions(self): self.ref.trajectory.ts.dimensions, rtol=0, atol=1e-3, - err_msg=("OpenMMSimulationReader failed to get unitcell " - "dimensions from OpenMMSimulation"), + err_msg=( + "OpenMMSimulationReader failed to get unitcell " + "dimensions from OpenMMSimulation" + ), ) def test_coordinates(self): @@ -176,7 +185,7 @@ def test_coordinates(self): rp = self.ref.atoms.positions assert_allclose(up, rp, rtol=0, atol=1e-3) - @pytest.mark.xfail(reason='OpenMM pickling not supported yet') + @pytest.mark.xfail(reason="OpenMM pickling not supported yet") def test_pickle_singleframe_reader(self): """ See `OpenMM SwigPyObject serialisation discussion `_ diff --git a/testsuite/MDAnalysisTests/converters/test_openmm_parser.py b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py index 12fdbd4857a..7188499fd8d 100644 --- a/testsuite/MDAnalysisTests/converters/test_openmm_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_openmm_parser.py @@ -53,7 +53,7 @@ class OpenMMTopologyBase(ParserBase): "bonds", "chainIDs", "elements", - "types" + "types", ] expected_n_bonds = 0 @@ -113,12 +113,12 @@ def test_segids(self, top): assert top.segids.values == [] def test_elements(self, top): - if 'elements' in self.expected_attrs: + if "elements" in self.expected_attrs: assert len(top.elements.values) == self.expected_n_atoms assert isinstance(top.elements.values, np.ndarray) assert all(isinstance(elem, str) for elem in top.elements.values) else: - assert not hasattr(top, 'elements') + assert not hasattr(top, "elements") def test_atomtypes(self, top): assert len(top.types.values) == self.expected_n_atoms @@ -131,15 +131,17 @@ def test_masses(self, top): assert len(top.masses.values) == self.expected_n_atoms if self.expected_n_atoms: assert isinstance(top.masses.values, np.ndarray) - assert all(isinstance(mass, np.float64) - for mass in top.masses.values) + assert all( + isinstance(mass, np.float64) for mass in top.masses.values + ) 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] + 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 @@ -156,7 +158,7 @@ class OpenMMAppTopologyBase(OpenMMTopologyBase): "bonds", "chainIDs", "elements", - "types" + "types", ] expected_n_bonds = 0 @@ -193,8 +195,10 @@ class TestOpenMMTopologyParserWithPartialElements(OpenMMTopologyBase): expected_n_bonds = 25533 def test_with_partial_elements(self): - wmsg1 = ("Element information missing for some atoms. " - "These have been given an empty element record ") + wmsg1 = ( + "Element information missing for some atoms. " + "These have been given an empty element record " + ) wmsg2 = ( "For absent elements, atomtype has been " @@ -202,14 +206,15 @@ def test_with_partial_elements(self): "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).") + " but it will be removed in 3.0)." + ) with pytest.warns(UserWarning) as warnings: mda_top = self.parser(self.ref_filename).parse() - assert mda_top.types.values[3344] == 'X' - assert mda_top.types.values[3388] == 'X' - assert mda_top.elements.values[3344] == '' - assert mda_top.elements.values[3388] == '' + assert mda_top.types.values[3344] == "X" + 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 @@ -233,7 +238,8 @@ def test_no_elements_warn(): "but it will be removed in MDAnalysis v3.0. " "These can be guessed using " "universe.guess_TopologyAttrs(to_guess=['masses', 'types']) " - "See MDAnalysis.guessers.") + "See MDAnalysis.guessers." + ) with pytest.warns(UserWarning) as warnings: mda_top = parser(omm_top).parse() @@ -243,26 +249,26 @@ def test_no_elements_warn(): def test_invalid_element_symbols(): parser = mda.converters.OpenMMParser.OpenMMTopologyParser omm_top = Topology() - bad1 = Element(0, "*", "*", 0*daltons) - bad2 = Element(0, "?", "?", 6*daltons) + bad1 = Element(0, "*", "*", 0 * daltons) + bad2 = Element(0, "?", "?", 6 * daltons) bad3 = None - silver = Element.getBySymbol('Ag') + silver = Element.getBySymbol("Ag") chain = omm_top.addChain() - res = omm_top.addResidue('R', chain) - omm_top.addAtom(name='Ag', element=silver, residue=res) - omm_top.addAtom(name='Bad1', element=bad1, residue=res) - omm_top.addAtom(name='Bad2', element=bad2, residue=res) - omm_top.addAtom(name='Bad3', element=bad3, residue=res) + res = omm_top.addResidue("R", chain) + omm_top.addAtom(name="Ag", element=silver, residue=res) + omm_top.addAtom(name="Bad1", element=bad1, residue=res) + omm_top.addAtom(name="Bad2", element=bad2, residue=res) + omm_top.addAtom(name="Bad3", element=bad3, residue=res) mda_top = parser(omm_top).parse() - assert mda_top.types.values[0] == 'Ag' - assert mda_top.types.values[1] == '*' - assert mda_top.types.values[2] == '?' - assert mda_top.types.values[3] == 'X' - assert mda_top.elements.values[0] == 'Ag' - assert mda_top.elements.values[1] == '' - assert mda_top.elements.values[2] == '' - assert mda_top.elements.values[3] == '' + assert mda_top.types.values[0] == "Ag" + assert mda_top.types.values[1] == "*" + assert mda_top.types.values[2] == "?" + assert mda_top.types.values[3] == "X" + assert mda_top.elements.values[0] == "Ag" + assert mda_top.elements.values[1] == "" + assert mda_top.elements.values[2] == "" + assert mda_top.elements.values[3] == "" assert mda_top.masses.values[0] == 107.86822 assert mda_top.masses.values[1] == 0.0 assert mda_top.masses.values[2] == 6.0 diff --git a/testsuite/MDAnalysisTests/converters/test_parmed.py b/testsuite/MDAnalysisTests/converters/test_parmed.py index e0503bd9dad..337495cf8f6 100644 --- a/testsuite/MDAnalysisTests/converters/test_parmed.py +++ b/testsuite/MDAnalysisTests/converters/test_parmed.py @@ -24,7 +24,7 @@ import MDAnalysis as mda import numpy as np -from numpy.testing import (assert_allclose, assert_equal) +from numpy.testing import assert_allclose, assert_equal from numpy.lib import NumpyVersion from MDAnalysisTests.coordinates.base import _SingleFrameReader @@ -46,10 +46,9 @@ # TODO: remove this guard when parmed has a release # that support NumPy 2 if NumpyVersion(np.__version__) < "2.0.0": - pmd = pytest.importorskip('parmed') + pmd = pytest.importorskip("parmed") else: - pmd = pytest.importorskip('parmed_skip_with_numpy2') - + pmd = pytest.importorskip("parmed_skip_with_numpy2") class TestParmEdReaderGRO: @@ -60,18 +59,20 @@ class TestParmEdReaderGRO: def test_dimensions(self): assert_allclose( - self.universe.trajectory.ts.dimensions, + self.universe.trajectory.ts.dimensions, self.ref.trajectory.ts.dimensions, rtol=0, atol=1e-3, - err_msg=("ParmEdReader failed to get unitcell dimensions " - "from ParmEd")) - + err_msg=( + "ParmEdReader failed to get unitcell dimensions " "from ParmEd" + ), + ) + def test_coordinates(self): up = self.universe.atoms.positions rp = self.ref.atoms.positions assert_allclose(up, rp, rtol=0, atol=1e-3) - + class BaseTestParmEdReader(_SingleFrameReader): def setUp(self): @@ -80,27 +81,31 @@ def setUp(self): def test_dimensions(self): assert_allclose( - self.universe.trajectory.ts.dimensions, + self.universe.trajectory.ts.dimensions, self.ref.trajectory.ts.dimensions, rtol=0, atol=1e-3, - err_msg=("ParmEdReader failed to get unitcell dimensions " - "from ParmEd")) - + err_msg=( + "ParmEdReader failed to get unitcell dimensions " "from ParmEd" + ), + ) + def test_coordinates(self): up = self.universe.atoms.positions rp = self.ref.atoms.positions assert_allclose(up, rp, rtol=0, atol=1e-3) - + class TestParmEdReaderPDB(BaseTestParmEdReader): ref_filename = RefAdKSmall.filename - + def test_uses_ParmEdReader(self): from MDAnalysis.coordinates.ParmEd import ParmEdReader - assert isinstance(self.universe.trajectory, ParmEdReader), "failed to choose ParmEdReader" + assert isinstance( + self.universe.trajectory, ParmEdReader + ), "failed to choose ParmEdReader" def _parmed_param_eq(a, b): @@ -108,52 +113,66 @@ def _parmed_param_eq(a, b): b_idx = [b.atom1.idx, b.atom2.idx] for i in (3, 4, 5): - atom = 'atom{}'.format(i) + atom = "atom{}".format(i) if hasattr(a, atom): if not hasattr(b, atom): return False a_idx.append(getattr(a, atom).idx) b_idx.append(getattr(b, atom).idx) - + atoms = a_idx == b_idx or a_idx == b_idx[::-1] return atoms and a.type == b.type class BaseTestParmEdConverter: - equal_atom_attrs = ('name', 'altloc') - almost_equal_atom_attrs = ('mass', 'charge', 'occupancy') - expected_attrs = ('atoms', 'bonds', 'angles', 'dihedrals', 'impropers', - 'cmaps', 'urey_bradleys') - - - @pytest.fixture(scope='class') + equal_atom_attrs = ("name", "altloc") + almost_equal_atom_attrs = ("mass", "charge", "occupancy") + expected_attrs = ( + "atoms", + "bonds", + "angles", + "dihedrals", + "impropers", + "cmaps", + "urey_bradleys", + ) + + @pytest.fixture(scope="class") def ref(self): # skip_bonds controls whether to search for bonds if it's not in the file - return pmd.load_file(self.ref_filename, skip_bonds=True) + return pmd.load_file(self.ref_filename, skip_bonds=True) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self, ref): return mda.Universe(self.ref_filename) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def output(self, universe): - return universe.atoms.convert_to('PARMED') + return universe.atoms.convert_to("PARMED") - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def roundtrip(self, ref): u = mda.Universe(ref) - return u.atoms.convert_to('PARMED') + return u.atoms.convert_to("PARMED") def test_equivalent_connectivity_counts(self, universe, output): - for attr in ('atoms', 'bonds', 'angles', 'dihedrals', 'impropers', - 'cmaps', 'urey_bradleys'): + for attr in ( + "atoms", + "bonds", + "angles", + "dihedrals", + "impropers", + "cmaps", + "urey_bradleys", + ): u = getattr(universe, attr, []) o = getattr(output, attr) assert len(u) == len(o) - - @pytest.mark.parametrize('attr', ('bonds', 'angles', 'dihedrals', 'impropers', - 'cmaps')) + + @pytest.mark.parametrize( + "attr", ("bonds", "angles", "dihedrals", "impropers", "cmaps") + ) def test_equivalent_connectivity_values(self, universe, output, attr): u = getattr(universe._topology, attr, []) vals = u.values if u else [] @@ -190,17 +209,25 @@ def test_equivalent_atoms(self, ref, output): for attr in self.equal_atom_attrs: ra = getattr(r, attr) oa = getattr(o, attr) - assert ra == oa, 'atom {} not equal for atoms {} and {}'.format(attr, r, o) - + assert ( + ra == oa + ), "atom {} not equal for atoms {} and {}".format(attr, r, o) + for attr in self.almost_equal_atom_attrs: ra = getattr(r, attr) oa = getattr(o, attr) - assert_allclose(ra, oa, rtol=0, atol=1e-2, - err_msg=(f'atom {attr} not almost equal for atoms ' - f'{r} and {o}')) - - @pytest.mark.parametrize('attr', ('bonds', 'angles', 'impropers', - 'cmaps')) + assert_allclose( + ra, + oa, + rtol=0, + atol=1e-2, + err_msg=( + f"atom {attr} not almost equal for atoms " + f"{r} and {o}" + ), + ) + + @pytest.mark.parametrize("attr", ("bonds", "angles", "impropers", "cmaps")) def test_equivalent_connectivity_types(self, ref, roundtrip, attr): original = getattr(ref, attr) for p in getattr(roundtrip, attr): @@ -211,9 +238,14 @@ def test_equivalent_connectivity_types(self, ref, roundtrip, attr): def test_equivalent_dihedrals(self, ref, roundtrip): original = ref.dihedrals for p in roundtrip.dihedrals: - assert any((_parmed_param_eq(p, q) and - p.improper == q.improper and - p.ignore_end == q.ignore_end) for q in original) + assert any( + ( + _parmed_param_eq(p, q) + and p.improper == q.improper + and p.ignore_end == q.ignore_end + ) + for q in original + ) def test_missing_attr(self): n_atoms = 10 @@ -221,9 +253,10 @@ def test_missing_attr(self): u.add_TopologyAttr("resid", [1]) u.add_TopologyAttr("segid", ["DUM"]) u.add_TopologyAttr("mass", [1] * n_atoms) - with pytest.warns(UserWarning, - match="Supplied AtomGroup was missing the following " - "attributes"): + with pytest.warns( + UserWarning, + match="Supplied AtomGroup was missing the following " "attributes", + ): # should miss names and resnames u.atoms.convert_to("PARMED") @@ -234,29 +267,36 @@ class BaseTestParmEdConverterSubset(BaseTestParmEdConverter): end_i = 0 skip_i = 1 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(self): # skip_bonds controls whether to search for bonds if it's not in the file struct = pmd.load_file(self.ref_filename, skip_bonds=True) - return struct[self.start_i:self.end_i:self.skip_i] + return struct[self.start_i : self.end_i : self.skip_i] - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): u = mda.Universe(self.ref_filename) - return mda.Merge(u.atoms[self.start_i:self.end_i:self.skip_i]) + return mda.Merge(u.atoms[self.start_i : self.end_i : self.skip_i]) class BaseTestParmEdConverterFromParmed(BaseTestParmEdConverter): - equal_atom_attrs = ('name', 'number', 'altloc') + equal_atom_attrs = ("name", "number", "altloc") - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self, ref): return mda.Universe(ref) def test_equivalent_connectivity_counts(self, ref, output): - for attr in ('atoms', 'bonds', 'angles', 'dihedrals', 'impropers', - 'cmaps', 'urey_bradleys'): + for attr in ( + "atoms", + "bonds", + "angles", + "dihedrals", + "impropers", + "cmaps", + "urey_bradleys", + ): r = getattr(ref, attr) o = getattr(output, attr) assert len(r) == len(o) @@ -292,14 +332,16 @@ class TestParmEdConverterGROSubset(BaseTestParmEdConverterSubset): start_i = 5 end_i = 100 + # TODO: Add Subset test for PRMs when mda.Merge accepts Universes without positions + class TestParmEdConverterPDB(BaseTestParmEdConverter): ref_filename = PDB_small - # Neither MDAnalysis nor ParmEd read the mass column + # Neither MDAnalysis nor ParmEd read the mass column # of PDBs and are liable to guess wrong - almost_equal_atom_attrs = ('charge', 'occupancy') + almost_equal_atom_attrs = ("charge", "occupancy") def test_equivalent_coordinates(self, ref, output): assert_allclose(ref.coordinates, output.coordinates, rtol=0, atol=1e-3) diff --git a/testsuite/MDAnalysisTests/converters/test_parmed_parser.py b/testsuite/MDAnalysisTests/converters/test_parmed_parser.py index ea73e8dc000..1a11876fd4f 100644 --- a/testsuite/MDAnalysisTests/converters/test_parmed_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_parmed_parser.py @@ -26,25 +26,42 @@ import MDAnalysis as mda from MDAnalysisTests.topology.base import ParserBase -from MDAnalysisTests.datafiles import ( - PSF_NAMD_GBIS, - PRM -) +from MDAnalysisTests.datafiles import PSF_NAMD_GBIS, PRM -pmd = pytest.importorskip('parmed') +pmd = pytest.importorskip("parmed") class BaseTestParmedParser(ParserBase): parser = mda.converters.ParmEdParser.ParmEdParser - expected_attrs = ['ids', 'names', 'types', 'masses', - 'charges', 'altLocs', 'occupancies', - 'tempfactors', 'gbscreens', 'solventradii', - 'nbindices', 'rmins', 'epsilons', 'rmin14s', - 'epsilon14s', 'elements', 'chainIDs', - 'resids', 'resnames', 'resnums', - 'segids', - 'bonds', 'ureybradleys', 'angles', - 'dihedrals', 'impropers', 'cmaps'] + expected_attrs = [ + "ids", + "names", + "types", + "masses", + "charges", + "altLocs", + "occupancies", + "tempfactors", + "gbscreens", + "solventradii", + "nbindices", + "rmins", + "epsilons", + "rmin14s", + "epsilon14s", + "elements", + "chainIDs", + "resids", + "resnames", + "resnums", + "segids", + "bonds", + "ureybradleys", + "angles", + "dihedrals", + "impropers", + "cmaps", + ] expected_n_atoms = 0 expected_n_residues = 1 @@ -69,40 +86,61 @@ def test_creates_universe(self, filename): assert isinstance(u, mda.Universe) def test_bonds_total_counts(self, top, filename): - unique = set([(a.atom1.idx, a.atom2.idx) - for a in filename.bonds]) + unique = set([(a.atom1.idx, a.atom2.idx) for a in filename.bonds]) assert len(top.bonds.values) == len(unique) def test_angles_total_counts(self, top, filename): - unique = set([(a.atom1.idx, a.atom2.idx, a.atom3.idx) - for a in filename.angles]) + unique = set( + [(a.atom1.idx, a.atom2.idx, a.atom3.idx) for a in filename.angles] + ) assert len(top.angles.values) == len(unique) def test_dihedrals_total_counts(self, top, filename): - unique = set([(a.atom1.idx, a.atom2.idx, a.atom3.idx, a.atom4.idx) - for a in filename.dihedrals]) + unique = set( + [ + (a.atom1.idx, a.atom2.idx, a.atom3.idx, a.atom4.idx) + for a in filename.dihedrals + ] + ) assert len(top.dihedrals.values) == len(unique) def test_impropers_total_counts(self, top, filename): - unique = set([(a.atom1.idx, a.atom2.idx, a.atom3.idx, a.atom4.idx) - for a in filename.impropers]) + unique = set( + [ + (a.atom1.idx, a.atom2.idx, a.atom3.idx, a.atom4.idx) + for a in filename.impropers + ] + ) assert len(top.impropers.values) == len(unique) def test_cmaps_total_counts(self, top, filename): - unique = set([(a.atom1.idx, a.atom2.idx, a.atom3.idx, - a.atom4.idx, a.atom5.idx) - for a in filename.cmaps]) + unique = set( + [ + ( + a.atom1.idx, + a.atom2.idx, + a.atom3.idx, + a.atom4.idx, + a.atom5.idx, + ) + for a in filename.cmaps + ] + ) assert len(top.cmaps.values) == len(unique) def test_ureybradleys_total_counts(self, top, filename): - unique = set([(a.atom1.idx, a.atom2.idx) - for a in filename.urey_bradleys]) + unique = set( + [(a.atom1.idx, a.atom2.idx) for a in filename.urey_bradleys] + ) assert len(top.ureybradleys.values) == len(unique) def test_elements(self, top): for erange, evals in zip(self.elems_ranges, self.expected_elems): - assert_equal(top.elements.values[erange[0]:erange[1]], evals, - "unexpected element match") + assert_equal( + top.elements.values[erange[0] : erange[1]], + evals, + "unexpected element match", + ) class TestParmedParserPSF(BaseTestParmedParser): @@ -121,20 +159,21 @@ class TestParmedParserPSF(BaseTestParmedParser): expected_n_cmaps = 212 elems_ranges = ((100, 120),) # No atomic numbers set by parmed == no elements - expected_elems = (np.array( - ['N', 'H', 'C', 'H', 'C', 'H', 'H', 'C', 'H', 'C', 'H', 'H', 'H', 'C', - 'H', 'H', 'H', 'C', 'O', 'N',], dtype=object),) + expected_elems = (np.array(list("NHCHCHHCHCHHHCHHHCON"), dtype=object),) def test_bonds_atom_counts(self, universe): assert len(universe.atoms[[0]].bonds) == 4 assert len(universe.atoms[[42]].bonds) == 1 - @pytest.mark.parametrize('value', ( - (0, 1), - (0, 2), - (0, 3), - (0, 4), - )) + @pytest.mark.parametrize( + "value", + ( + (0, 1), + (0, 2), + (0, 3), + (0, 4), + ), + ) def test_bonds_identity(self, top, value): vals = top.bonds.values assert value in vals or value[::-1] in vals @@ -150,11 +189,14 @@ def test_angles_atom_counts(self, universe): assert len(universe.atoms[[0]].angles), 9 assert len(universe.atoms[[42]].angles), 2 - @pytest.mark.parametrize('value', ( - (1, 0, 2), - (1, 0, 3), - (1, 0, 4), - )) + @pytest.mark.parametrize( + "value", + ( + (1, 0, 2), + (1, 0, 3), + (1, 0, 4), + ), + ) def test_angles_identity(self, top, value): vals = top.angles.values assert value in vals or value[::-1] in vals @@ -162,20 +204,22 @@ def test_angles_identity(self, top, value): def test_dihedrals_atom_counts(self, universe): assert len(universe.atoms[[0]].dihedrals) == 14 - @pytest.mark.parametrize('value', ( - (0, 4, 6, 7), - (0, 4, 6, 8), - (0, 4, 6, 9), - (0, 4, 17, 18), - )) + @pytest.mark.parametrize( + "value", + ( + (0, 4, 6, 7), + (0, 4, 6, 8), + (0, 4, 6, 9), + (0, 4, 17, 18), + ), + ) def test_dihedrals_identity(self, top, value): vals = top.dihedrals.values assert value in vals or value[::-1] in vals - @pytest.mark.parametrize('value', ( - (17, 19, 21, 41, 43), - (60, 62, 64, 79, 81) - )) + @pytest.mark.parametrize( + "value", ((17, 19, 21, 41, 43), (60, 62, 64, 79, 81)) + ) def test_cmaps_identity(self, top, value): vals = top.cmaps.values assert value in vals or value[::-1] in vals @@ -191,21 +235,24 @@ class TestParmedParserPRM(BaseTestParmedParser): expected_n_atoms = 252 expected_n_residues = 14 elems_ranges = ((0, 8), (30, 38)) - expected_elems = (np.array(['N', 'H', 'H', 'H', 'C', 'H', 'C', 'H'], - dtype=object), - np.array(['H', 'C', 'H', 'H', 'C', 'C', 'H', 'C'], - dtype=object)) + expected_elems = ( + np.array(["N", "H", "H", "H", "C", "H", "C", "H"], dtype=object), + np.array(["H", "C", "H", "H", "C", "C", "H", "C"], dtype=object), + ) def test_bonds_atom_counts(self, universe): assert len(universe.atoms[[0]].bonds) == 4 assert len(universe.atoms[[42]].bonds) == 1 - @pytest.mark.parametrize('value', ( - (10, 11), - (10, 12), - (4, 6), - (4, 10), - )) + @pytest.mark.parametrize( + "value", + ( + (10, 11), + (10, 12), + (4, 6), + (4, 10), + ), + ) def test_bonds_identity(self, top, value): vals = top.bonds.values assert value in vals or value[::-1] in vals @@ -222,12 +269,15 @@ def test_angles_atom_counts(self, universe): assert len(universe.atoms[[0]].angles), 9 assert len(universe.atoms[[42]].angles), 2 - @pytest.mark.parametrize('value', ( - (11, 10, 12), - (10, 12, 14), - (6, 4, 10), - (4, 10, 11), - )) + @pytest.mark.parametrize( + "value", + ( + (11, 10, 12), + (10, 12, 14), + (6, 4, 10), + (4, 10, 11), + ), + ) def test_angles_identity(self, top, value): vals = top.angles.values assert value in vals or value[::-1] in vals @@ -235,25 +285,29 @@ def test_angles_identity(self, top, value): def test_dihedrals_atom_counts(self, universe): assert len(universe.atoms[[0]].dihedrals) == 14 - @pytest.mark.parametrize('value', ( - (11, 10, 12, 14), - (10, 12, 14, 16), - )) + @pytest.mark.parametrize( + "value", + ( + (11, 10, 12, 14), + (10, 12, 14, 16), + ), + ) def test_dihedrals_identity(self, top, value): vals = top.dihedrals.values assert value in vals or value[::-1] in vals def test_dihedral_types(self, universe): ag = universe.atoms[[10, 12, 14, 16]] - dih = universe.dihedrals.atomgroup_intersection(ag, - strict=True)[0] + dih = universe.dihedrals.atomgroup_intersection(ag, strict=True)[0] assert len(dih.type) == 4 - for i, (phi_k, per) in enumerate(( - (2.0, 1), - (2.0, 2), - (0.4, 3), - (0.0, 4), - )): + for i, (phi_k, per) in enumerate( + ( + (2.0, 1), + (2.0, 2), + (0.4, 3), + (0.0, 4), + ) + ): assert dih.type[i].type.phi_k == phi_k assert dih.type[i].type.per == per diff --git a/testsuite/MDAnalysisTests/converters/test_rdkit.py b/testsuite/MDAnalysisTests/converters/test_rdkit.py index 1d56c4c5f62..a455e8ddea9 100644 --- a/testsuite/MDAnalysisTests/converters/test_rdkit.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit.py @@ -35,30 +35,35 @@ from numpy.testing import assert_allclose, assert_equal try: - from MDAnalysis.converters.RDKit import (RDATTRIBUTES, - _add_mda_attr_to_rdkit, - _atom_sorter, - _infer_bo_and_charges, - _rebuild_conjugated_bonds, - _set_atom_property, - _standardize_patterns) + from MDAnalysis.converters.RDKit import ( + RDATTRIBUTES, + _add_mda_attr_to_rdkit, + _atom_sorter, + _infer_bo_and_charges, + _rebuild_conjugated_bonds, + _set_atom_property, + _standardize_patterns, + ) from rdkit import Chem from rdkit.Chem import AllChem except ImportError: pass -requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), - reason="requires RDKit") +requires_rdkit = pytest.mark.skipif( + import_not_available("rdkit"), reason="requires RDKit" +) -@pytest.mark.skipif(not import_not_available("rdkit"), - reason="only for min dependencies build") +@pytest.mark.skipif( + not import_not_available("rdkit"), reason="only for min dependencies build" +) class TestRequiresRDKit(object): def test_converter_requires_rdkit(self): u = mda.Universe(PDB_full) - with pytest.raises(ImportError, - match="RDKit is required for the RDKitConverter"): + with pytest.raises( + ImportError, match="RDKit is required for the RDKitConverter" + ): u.atoms.convert_to("RDKIT") @@ -104,22 +109,28 @@ def product(request): def is_isomorphic(mol, ref, useChirality=False): - return (mol.HasSubstructMatch(ref, useChirality=useChirality) - and ref.HasSubstructMatch(mol, useChirality=useChirality)) + return mol.HasSubstructMatch( + ref, useChirality=useChirality + ) and ref.HasSubstructMatch(mol, useChirality=useChirality) @requires_rdkit class TestRDKitReader(object): - @pytest.mark.parametrize("rdmol, n_frames", [ - ("mol2_mol", 1), - ("smiles_mol", 3), - ], indirect=["rdmol"]) + @pytest.mark.parametrize( + "rdmol, n_frames", + [ + ("mol2_mol", 1), + ("smiles_mol", 3), + ], + indirect=["rdmol"], + ) def test_coordinates(self, rdmol, n_frames): universe = mda.Universe(rdmol) assert universe.trajectory.n_frames == n_frames - expected = np.array([ - conf.GetPositions() for conf in rdmol.GetConformers()], - dtype=np.float32) + expected = np.array( + [conf.GetPositions() for conf in rdmol.GetConformers()], + dtype=np.float32, + ) assert_equal(expected, universe.trajectory.coordinate_array) def test_no_coordinates(self): @@ -133,8 +144,9 @@ def test_compare_mol2reader(self): universe = mda.Universe(MolFactory.mol2_mol()) mol2 = mda.Universe(mol2_molecule) assert universe.trajectory.n_frames == mol2.trajectory.n_frames - assert_equal(universe.trajectory.ts.positions, - mol2.trajectory.ts.positions) + assert_equal( + universe.trajectory.ts.positions, mol2.trajectory.ts.positions + ) @requires_rdkit @@ -148,16 +160,18 @@ def mol2(self): u = mda.Universe(mol2_molecule) # add elements guesser = DefaultGuesser(None) - elements = np.array([guesser.guess_atom_element(x) for x in u.atoms.types], - dtype=object) - u.add_TopologyAttr('elements', elements) + elements = np.array( + [guesser.guess_atom_element(x) for x in u.atoms.types], + dtype=object, + ) + u.add_TopologyAttr("elements", elements) return u @pytest.fixture def peptide(self): u = mda.Universe(GRO) elements = mda.guesser.DefaultGuesser(None).guess_types(u.atoms.names) - u.add_TopologyAttr('elements', elements) + u.add_TopologyAttr("elements", elements) return u.select_atoms("resid 2-12") @pytest.fixture @@ -171,28 +185,35 @@ def test_no_atoms_attr(self): @pytest.mark.parametrize("smi", ["[H]", "C", "O", "[He]"]) def test_single_atom_mol(self, smi): - u = mda.Universe.from_smiles(smi, addHs=False, - generate_coordinates=False) + u = mda.Universe.from_smiles( + smi, addHs=False, generate_coordinates=False + ) mol = u.atoms.convert_to.rdkit(NoImplicit=False) assert mol.GetNumAtoms() == 1 assert mol.GetAtomWithIdx(0).GetSymbol() == smi.strip("[]") - @pytest.mark.parametrize("resname, n_atoms, n_fragments", [ - ("PRO", 14, 1), - ("ILE", 38, 1), - ("ALA", 20, 2), - ("GLY", 21, 3), - ]) + @pytest.mark.parametrize( + "resname, n_atoms, n_fragments", + [ + ("PRO", 14, 1), + ("ILE", 38, 1), + ("ALA", 20, 2), + ("GLY", 21, 3), + ], + ) def test_mol_from_selection(self, peptide, resname, n_atoms, n_fragments): mol = peptide.select_atoms("resname %s" % resname).convert_to("RDKIT") assert n_atoms == mol.GetNumAtoms() assert n_fragments == len(Chem.GetMolFrags(mol)) - @pytest.mark.parametrize("sel_str, atom_index", [ - ("resid 1", 0), - ("resname LYS and name NZ", 1), - ("resid 34 and altloc B", 2), - ]) + @pytest.mark.parametrize( + "sel_str, atom_index", + [ + ("resid 1", 0), + ("resname LYS and name NZ", 1), + ("resid 34 and altloc B", 2), + ], + ) def test_monomer_info(self, pdb, sel_str, atom_index): sel = pdb.select_atoms(sel_str) mda_atom = sel.atoms[atom_index] @@ -209,8 +230,9 @@ def test_monomer_info(self, pdb, sel_str, atom_index): assert mda_atom.segindex == mi.GetSegmentNumber() assert mda_atom.tempfactor == mi.GetTempFactor() - @pytest.mark.parametrize("rdmol", ["mol2_mol", "smiles_mol"], - indirect=True) + @pytest.mark.parametrize( + "rdmol", ["mol2_mol", "smiles_mol"], indirect=True + ) def test_identical_topology(self, rdmol): u = mda.Universe(rdmol) umol = u.atoms.convert_to("RDKIT") @@ -220,27 +242,26 @@ def test_identical_topology(self, rdmol): assert_equal(u.atoms.elements, u2.atoms.elements) assert_equal(u.atoms.names, u2.atoms.names) assert_allclose( - u.atoms.positions, - u2.atoms.positions, - rtol=0, - atol=1e-7) + u.atoms.positions, u2.atoms.positions, rtol=0, atol=1e-7 + ) def test_raise_requires_elements(self): u = mda.Universe(mol2_molecule) # Delete topology attribute (PR #3069) - u.del_TopologyAttr('elements') + u.del_TopologyAttr("elements") with pytest.raises( AttributeError, - match="`elements` attribute is required for the RDKitConverter" + match="`elements` attribute is required for the RDKitConverter", ): u.atoms.convert_to("RDKIT") def test_warn_guess_bonds(self): u = mda.Universe(PDB_helix) - with pytest.warns(UserWarning, - match="No `bonds` attribute in this AtomGroup"): + with pytest.warns( + UserWarning, match="No `bonds` attribute in this AtomGroup" + ): u.atoms.convert_to("RDKIT") def test_bonds_outside_sel(self): @@ -249,9 +270,10 @@ def test_bonds_outside_sel(self): ag.convert_to.rdkit(NoImplicit=False) def test_error_no_hydrogen(self, uo2): - with pytest.raises(AttributeError, - match="the converter requires all hydrogens to be " - "explicit"): + with pytest.raises( + AttributeError, + match="the converter requires all hydrogens to be " "explicit", + ): uo2.atoms.convert_to("RDKIT") def test_error_no_hydrogen_implicit(self, uo2): @@ -260,28 +282,32 @@ def test_error_no_hydrogen_implicit(self, uo2): uo2.atoms.convert_to.rdkit(NoImplicit=False) def test_warning_no_hydrogen_force(self, uo2): - with pytest.warns(UserWarning, - match="Forcing to continue the conversion"): + with pytest.warns( + UserWarning, match="Forcing to continue the conversion" + ): uo2.atoms.convert_to.rdkit(NoImplicit=False, force=True) - @pytest.mark.parametrize("attr, value, expected", [ - ("names", "N", " N "), - ("names", "CA", " CA "), - ("names", "CAT", " CAT"), - ("names", "N1", " N1 "), - ("names", "CE2", " CE2"), - ("names", "C12", " C12"), - ("names", "HD12", "HD12"), - ("names", "C123", "C123"), - ("altLocs", "A", "A"), - ("chainIDs", "B", "B"), - ("icodes", "C", "C"), - ("occupancies", 0.5, 0.5), - ("resnames", "LIG", "LIG"), - ("resids", 123, 123), - ("segindices", 1, 1), - ("tempfactors", 0.8, 0.8), - ]) + @pytest.mark.parametrize( + "attr, value, expected", + [ + ("names", "N", " N "), + ("names", "CA", " CA "), + ("names", "CAT", " CAT"), + ("names", "N1", " N1 "), + ("names", "CE2", " CE2"), + ("names", "C12", " C12"), + ("names", "HD12", "HD12"), + ("names", "C123", "C123"), + ("altLocs", "A", "A"), + ("chainIDs", "B", "B"), + ("icodes", "C", "C"), + ("occupancies", 0.5, 0.5), + ("resnames", "LIG", "LIG"), + ("resids", 123, 123), + ("segindices", 1, 1), + ("tempfactors", 0.8, 0.8), + ], + ) def test_add_mda_attr_to_rdkit(self, attr, value, expected): mi = Chem.AtomPDBResidueInfo() _add_mda_attr_to_rdkit(attr, value, mi) @@ -297,10 +323,13 @@ def test_other_attributes(self, mol2, idx): assert mda_atom.segid == rdatom.GetProp("_MDAnalysis_segid") assert mda_atom.type == rdatom.GetProp("_MDAnalysis_type") - @pytest.mark.parametrize("sel_str", [ - "resname ALA", - "resname PRO and segid A", - ]) + @pytest.mark.parametrize( + "sel_str", + [ + "resname ALA", + "resname PRO and segid A", + ], + ) def test_index_property(self, pdb, sel_str): ag = pdb.select_atoms(sel_str) mol = ag.convert_to.rdkit(NoImplicit=False) @@ -320,7 +349,8 @@ def test_assign_stereochemistry(self, mol2): def test_trajectory_coords(self): u = mda.Universe.from_smiles( - "CCO", numConfs=3, rdkit_kwargs=dict(randomSeed=42)) + "CCO", numConfs=3, rdkit_kwargs=dict(randomSeed=42) + ) for ts in u.trajectory: mol = u.atoms.convert_to("RDKIT") positions = mol.GetConformer().GetPositions() @@ -441,26 +471,34 @@ def assert_isomorphic_resonance_structure(self, mol, ref): """ isomorphic = mol.HasSubstructMatch(ref) if not isomorphic: - isomorphic = bool(Chem.ResonanceMolSupplier(mol) - .GetSubstructMatch(ref)) - assert isomorphic, f"{Chem.MolToSmiles(ref)} != {Chem.MolToSmiles(mol)}" - - @pytest.mark.parametrize("smi, out", [ - ("C(-[H])(-[H])(-[H])-[H]", "C"), - ("[C](-[H])(-[H])-[C](-[H])-[H]", "C=C"), - ("[C]1(-[H])-[C](-[H])-[C](-[H])-[C](-[H])-[C](-[H])-[C]1(-[H])", - "c1ccccc1"), - ("C-[C](-[H])-[O]", "C(=O)C"), - ("[H]-[C](-[O])-[N](-[H])-[H]", "C(=O)N"), - ("[N]-[C]-[H]", "N#C"), - ("C-[C](-[O]-[H])-[O]", "CC(=O)O"), - ("[P](-[O]-[H])(-[O]-[H])(-[O]-[H])-[O]", "P(O)(O)(O)=O"), - ("[P](-[O]-[H])(-[O]-[H])(-[O])-[O]", "P([O-])(O)(O)=O"), - ("[P](-[O]-[H])(-[O])(-[O])-[O]", "P([O-])([O-])(O)=O"), - ("[P](-[O])(-[O])(-[O])-[O]", "P([O-])([O-])([O-])=O"), - ("[H]-[O]-[N]-[O]", "ON=O"), - ("[N]-[C]-[O]", "N#C[O-]"), - ]) + isomorphic = bool( + Chem.ResonanceMolSupplier(mol).GetSubstructMatch(ref) + ) + assert ( + isomorphic + ), f"{Chem.MolToSmiles(ref)} != {Chem.MolToSmiles(mol)}" + + @pytest.mark.parametrize( + "smi, out", + [ + ("C(-[H])(-[H])(-[H])-[H]", "C"), + ("[C](-[H])(-[H])-[C](-[H])-[H]", "C=C"), + ( + "[C]1(-[H])-[C](-[H])-[C](-[H])-[C](-[H])-[C](-[H])-[C]1(-[H])", + "c1ccccc1", + ), + ("C-[C](-[H])-[O]", "C(=O)C"), + ("[H]-[C](-[O])-[N](-[H])-[H]", "C(=O)N"), + ("[N]-[C]-[H]", "N#C"), + ("C-[C](-[O]-[H])-[O]", "CC(=O)O"), + ("[P](-[O]-[H])(-[O]-[H])(-[O]-[H])-[O]", "P(O)(O)(O)=O"), + ("[P](-[O]-[H])(-[O]-[H])(-[O])-[O]", "P([O-])(O)(O)=O"), + ("[P](-[O]-[H])(-[O])(-[O])-[O]", "P([O-])([O-])(O)=O"), + ("[P](-[O])(-[O])(-[O])-[O]", "P([O-])([O-])([O-])=O"), + ("[H]-[O]-[N]-[O]", "ON=O"), + ("[N]-[C]-[O]", "N#C[O-]"), + ], + ) def test_infer_bond_orders(self, smi, out): mol = Chem.MolFromSmiles(smi, sanitize=False) mol.UpdatePropertyCache(strict=False) @@ -468,14 +506,19 @@ def test_infer_bond_orders(self, smi, out): Chem.SanitizeMol(mol) mol = Chem.RemoveHs(mol) molref = Chem.MolFromSmiles(out) - assert is_isomorphic(mol, molref), "{} != {}".format(Chem.MolToSmiles(mol), out) - - @pytest.mark.parametrize("smi, atom_idx, charge", [ - ("[C](-[H])(-[H])(-[H])-[O]", 4, -1), - ("[N]-[C]-[O]", 2, -1), - ("[N](-[H])(-[H])(-[H])-[H]", 0, 1), - ("C-[C](-[O])-[O]", 3, -1), - ]) + assert is_isomorphic(mol, molref), "{} != {}".format( + Chem.MolToSmiles(mol), out + ) + + @pytest.mark.parametrize( + "smi, atom_idx, charge", + [ + ("[C](-[H])(-[H])(-[H])-[O]", 4, -1), + ("[N]-[C]-[O]", 2, -1), + ("[N](-[H])(-[H])(-[H])-[H]", 0, 1), + ("C-[C](-[O])-[O]", 3, -1), + ], + ) def test_infer_charges(self, smi, atom_idx, charge): mol = Chem.MolFromSmiles(smi, sanitize=False) mol.UpdatePropertyCache(strict=False) @@ -483,48 +526,62 @@ def test_infer_charges(self, smi, atom_idx, charge): Chem.SanitizeMol(mol) assert mol.GetAtomWithIdx(atom_idx).GetFormalCharge() == charge - @pytest.mark.parametrize("smi, out", [ - ("[S](-[O]-[H])(-[O]-[H])(-[O])-[O]", "S(=O)(=O)(O)O"), - ("[S](-[O]-[H])(-[O])(-[O])-[O]", "S(=O)(=O)([O-])O"), - ("[S](-[O])(-[O])(-[O])-[O]", "S(=O)(=O)([O-])[O-]"), - ("C-[N](-[H])-[C](-[N](-[H])-[H])-[N](-[H])-[H]", - "CNC(N)=[N+](-[H])-[H]"), - ("[O]-[C](-[H])-[C](-[H])-[H]", "C([O-])=C"), - ("C-[N](-[O])-[O]", "C[N+](=O)[O-]"), - ("C(-[N](-[O])-[O])-[N](-[O])-[O]", "C([N+](=O)[O-])[N+](=O)[O-]"), - ("C-[N](-[O])-[O].C-[N](-[O])-[O]", "C[N+](=O)[O-].C[N+](=O)[O-]"), - ("[C-](=O)-C", "[C](=O)-C"), - ("[H]-[N-]-C", "[H]-[N]-C"), - ("[O]-[C]1-[C](-[H])-[C](-[H])-[C](-[H])-[C](-[H])-[C](-[H])1", - "[O-]c1ccccc1"), - ("[O]-[C]1-[C](-[H])-[C](-[H])-[C](-[H])-[C]1-[O]", - "[O-]C1=CC=CC1=O"), - ("[H]-[C]-[C]-[C](-[H])-[C](-[H])-[H]", "C#CC=C"), - ("[H]-[C]-[C]-[C]-[C]-[H]", "C#CC#C"), - ]) + @pytest.mark.parametrize( + "smi, out", + [ + ("[S](-[O]-[H])(-[O]-[H])(-[O])-[O]", "S(=O)(=O)(O)O"), + ("[S](-[O]-[H])(-[O])(-[O])-[O]", "S(=O)(=O)([O-])O"), + ("[S](-[O])(-[O])(-[O])-[O]", "S(=O)(=O)([O-])[O-]"), + ( + "C-[N](-[H])-[C](-[N](-[H])-[H])-[N](-[H])-[H]", + "CNC(N)=[N+](-[H])-[H]", + ), + ("[O]-[C](-[H])-[C](-[H])-[H]", "C([O-])=C"), + ("C-[N](-[O])-[O]", "C[N+](=O)[O-]"), + ("C(-[N](-[O])-[O])-[N](-[O])-[O]", "C([N+](=O)[O-])[N+](=O)[O-]"), + ("C-[N](-[O])-[O].C-[N](-[O])-[O]", "C[N+](=O)[O-].C[N+](=O)[O-]"), + ("[C-](=O)-C", "[C](=O)-C"), + ("[H]-[N-]-C", "[H]-[N]-C"), + ( + "[O]-[C]1-[C](-[H])-[C](-[H])-[C](-[H])-[C](-[H])-[C](-[H])1", + "[O-]c1ccccc1", + ), + ( + "[O]-[C]1-[C](-[H])-[C](-[H])-[C](-[H])-[C]1-[O]", + "[O-]C1=CC=CC1=O", + ), + ("[H]-[C]-[C]-[C](-[H])-[C](-[H])-[H]", "C#CC=C"), + ("[H]-[C]-[C]-[C]-[C]-[H]", "C#CC#C"), + ], + ) def test_standardize_patterns(self, smi, out): mol = Chem.MolFromSmiles(smi, sanitize=False) mol.UpdatePropertyCache(strict=False) mol = self.assign_bond_orders_and_charges(mol) mol = Chem.RemoveHs(mol) molref = Chem.MolFromSmiles(out) - assert is_isomorphic(mol, molref), "{} != {}".format(Chem.MolToSmiles(mol), out) - - @pytest.mark.parametrize("attr, value, getter", [ - ("index", 42, "GetIntProp"), - ("index", np.int8(42), "GetIntProp"), - ("index", np.int16(42), "GetIntProp"), - ("index", np.int32(42), "GetIntProp"), - ("index", np.int64(42), "GetIntProp"), - ("index", np.uint8(42), "GetIntProp"), - ("index", np.uint16(42), "GetIntProp"), - ("index", np.uint32(42), "GetIntProp"), - ("index", np.uint64(42), "GetIntProp"), - ("charge", 4.2, "GetDoubleProp"), - ("charge", np.float32(4.2), "GetDoubleProp"), - ("charge", np.float64(4.2), "GetDoubleProp"), - ("type", "C.3", "GetProp"), - ]) + assert is_isomorphic(mol, molref), "{} != {}".format( + Chem.MolToSmiles(mol), out + ) + + @pytest.mark.parametrize( + "attr, value, getter", + [ + ("index", 42, "GetIntProp"), + ("index", np.int8(42), "GetIntProp"), + ("index", np.int16(42), "GetIntProp"), + ("index", np.int32(42), "GetIntProp"), + ("index", np.int64(42), "GetIntProp"), + ("index", np.uint8(42), "GetIntProp"), + ("index", np.uint16(42), "GetIntProp"), + ("index", np.uint32(42), "GetIntProp"), + ("index", np.uint64(42), "GetIntProp"), + ("charge", 4.2, "GetDoubleProp"), + ("charge", np.float32(4.2), "GetDoubleProp"), + ("charge", np.float64(4.2), "GetDoubleProp"), + ("type", "C.3", "GetProp"), + ], + ) def test_set_atom_property(self, attr, value, getter): atom = Chem.Atom(1) prop = "_MDAnalysis_%s" % attr @@ -536,11 +593,14 @@ def test_ignore_prop(self): _set_atom_property(atom, "foo", {"bar": "baz"}) assert "foo" not in atom.GetPropsAsDict().items() - @pytest.mark.parametrize("smi", [ - "c1ccc(cc1)-c1ccccc1-c1ccccc1", - "c1cc[nH]c1", - "O=C([C@H](CC1=C[NH1+]=CN1)[NH3+])[O-]", - ]) + @pytest.mark.parametrize( + "smi", + [ + "c1ccc(cc1)-c1ccccc1-c1ccccc1", + "c1cc[nH]c1", + "O=C([C@H](CC1=C[NH1+]=CN1)[NH3+])[O-]", + ], + ) def test_transfer_properties(self, smi): mol = Chem.MolFromSmiles(smi) mol = self.add_Hs_remove_bo_and_charges(mol) @@ -554,86 +614,88 @@ def test_transfer_properties(self, smi): new = {} for a in newmol.GetAtoms(): ix = a.GetIntProp("_MDAnalysis_index") - new[ix] = {"_MDAnalysis_index": ix, - "dummy": a.GetProp("dummy")} + new[ix] = {"_MDAnalysis_index": ix, "dummy": a.GetProp("dummy")} props = a.GetPropsAsDict().keys() assert "old_mapno" not in props assert "react_atom_idx" not in props assert new == old - @pytest.mark.parametrize("smi", [ - "c1ccc(cc1)-c1ccccc1-c1ccccc1", - "c1cc[nH]c1", - "c1ccc(cc1)-c1ccc(-c2ccccc2)c(-c2ccccc2)c1-c1ccccc1", - "c1ccc2c(c1)c1ccccc1c1ccccc1c1ccccc1c1ccccc21", - "c1csc(c1)-c1ccoc1-c1cc[nH]c1", - "C1=C2C(=NC=N1)N=CN2", - "CN1C=NC(=C1SC2=NC=NC3=C2NC=N3)[N+](=O)[O-]", - "c1c[nH]c(c1)-c1ccc(s1)-c1ccoc1-c1c[nH]cc1-c1ccccc1", - "C=CC=CC=CC=CC=CC=C", - "NCCCCC([NH3+])C(=O)[O-]", - "CC(C=CC1=C(C)CCCC1(C)C)=CC=CC(C)=CC=[NH+]C", - "C#CC=C", - # HID HIE HIP residues, see PR #2941 - "O=C([C@H](CC1=CNC=N1)N)O", - "O=C([C@H](CC1=CN=CN1)N)O", - "O=C([C@H](CC1=C[NH1+]=CN1)[NH3+])[O-]", - # fixes from PR #3044 - "CCOC(=O)c1cc2cc(C(=O)O)ccc2[nH]1", - "[O-][n+]1cccnc1", - "C[n+]1ccccc1", - "[PH4+]", - "c1nc[nH]n1", - "CC(=O)C=C(C)N", - "CC(C)=CC=C[O-]", - "O=S(C)(C)=NC", - "Cc1ccc2c3ncn(Cc4ccco4)c(O)c-3nc2c1", - "CCCC/C=C/C#CC#CCCCCCCCC(=O)O", - "c1c2c(=O)n3cccc(C)c3nc2n(C)c(=N)c1C(=O)NCc1cnccc1", - "N#Cc1c[nH]c(C(=O)NC(=O)c2cc[n+]([O-])cc2)n1", - "C[C@@H](Oc1cc(F)ccc1Nc1ncnc2cc(N=S3(=O)CCC3)cc(F)c12)C(=O)NCC#N", - "[O-][n+]1onc2ccccc21", - "Cc1cc[n+](CC[n+]2ccc(C)cc2)cc1", - "[O-]c1ccccc1", - "[O-]C=CC=CCC=CC=[N+](C)C", - "C=[N+](-[O-])-C", - "C-[N-]-C(=O)C", - # amino acids - "C[C@H](N)C(=O)O", # A - "NCC(=O)O", # G - "CC[C@H](C)[C@H](N)C(=O)O", # I - "CC(C)C[C@H](N)C(=O)O", # L - "O=C(O)[C@@H]1CCCN1", # P - "CC(C)[C@H](N)C(=O)O", # V - "N[C@@H](Cc1ccccc1)C(=O)O", # F - "N[C@@H](Cc1c[nH]c2ccccc12)C(=O)O", # W - "N[C@@H](Cc1ccc(O)cc1)C(=O)O", # Y - "N[C@@H](CC(=O)O)C(=O)O", # D - "N[C@@H](CCC(=O)O)C(=O)O", # E - "N=C(N)NCCC[C@H](N)C(=O)O", # R - "N[C@@H](Cc1c[nH]cn1)C(=O)O", # H - "NCCCC[C@H](N)C(=O)O", # K - "N[C@@H](CO)C(=O)O", # S - "C[C@@H](O)[C@H](N)C(=O)O", # T - "N[C@@H](CS)C(=O)O", # C - "CSCC[C@H](N)C(=O)O", # M - "NC(=O)C[C@H](N)C(=O)O", # N - "NC(=O)CC[C@H](N)C(=O)O", # Q - # nucleic acids - "Nc1ncnc2c1ncn2[C@H]1C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O1", # A - "Cc1cn([C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)[nH]c1=O", # T - "O=c1ccn([C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)[nH]1", # U - "Nc1ccn([C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)n1", # C - "Nc1nc2c(ncn2[C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)[nH]1", # G - # RDKitConverter benchmark 2022-05-23, fixed by better sorting - "CC(C)NS(C)(=O)=Nc1ccc(-c2ccc(-c3nc4[nH]c(O[C@@H]5CO[C@@H]6[C@H](O)CO[C@H]56)nc4cc3Cl)cc2)cc1 CHEMBL4111464", - "C[n+]1c2ccccc2c(C(=O)O)c2[nH]c3ccc(Br)cc3c21 CHEMBL511591", - "C[n+]1ccccc1-c1ccc(NC(=O)c2ccc(C(=O)Nc3ccc(-c4cccc[n+]4C)cc3)cc2)cc1 CHEMBL3230729", - "Cc1ccc(/C(O)=C(/C([S-])=NCc2ccccc2)[n+]2cccc(CO)c2)cc1[N+](=O)[O-] CHEMBL1596426", - "CC(C)(O)c1cc2c(cc1C(C)(C)O)-c1ccccc1-2 CHEMBL1990966", - "[O-]/N=C1/c2ccccc2-c2nc3ccc(C(F)(F)F)cc3nc21 CHEMBL4557878", - "N#Cc1c[nH]c(C(=O)Nc2ccc(-c3cccc[n+]3[O-])cc2C2=CCCCC2)n1 CHEMBL1172116", - ]) + @pytest.mark.parametrize( + "smi", + [ + "c1ccc(cc1)-c1ccccc1-c1ccccc1", + "c1cc[nH]c1", + "c1ccc(cc1)-c1ccc(-c2ccccc2)c(-c2ccccc2)c1-c1ccccc1", + "c1ccc2c(c1)c1ccccc1c1ccccc1c1ccccc1c1ccccc21", + "c1csc(c1)-c1ccoc1-c1cc[nH]c1", + "C1=C2C(=NC=N1)N=CN2", + "CN1C=NC(=C1SC2=NC=NC3=C2NC=N3)[N+](=O)[O-]", + "c1c[nH]c(c1)-c1ccc(s1)-c1ccoc1-c1c[nH]cc1-c1ccccc1", + "C=CC=CC=CC=CC=CC=C", + "NCCCCC([NH3+])C(=O)[O-]", + "CC(C=CC1=C(C)CCCC1(C)C)=CC=CC(C)=CC=[NH+]C", + "C#CC=C", + # HID HIE HIP residues, see PR #2941 + "O=C([C@H](CC1=CNC=N1)N)O", + "O=C([C@H](CC1=CN=CN1)N)O", + "O=C([C@H](CC1=C[NH1+]=CN1)[NH3+])[O-]", + # fixes from PR #3044 + "CCOC(=O)c1cc2cc(C(=O)O)ccc2[nH]1", + "[O-][n+]1cccnc1", + "C[n+]1ccccc1", + "[PH4+]", + "c1nc[nH]n1", + "CC(=O)C=C(C)N", + "CC(C)=CC=C[O-]", + "O=S(C)(C)=NC", + "Cc1ccc2c3ncn(Cc4ccco4)c(O)c-3nc2c1", + "CCCC/C=C/C#CC#CCCCCCCCC(=O)O", + "c1c2c(=O)n3cccc(C)c3nc2n(C)c(=N)c1C(=O)NCc1cnccc1", + "N#Cc1c[nH]c(C(=O)NC(=O)c2cc[n+]([O-])cc2)n1", + "C[C@@H](Oc1cc(F)ccc1Nc1ncnc2cc(N=S3(=O)CCC3)cc(F)c12)C(=O)NCC#N", + "[O-][n+]1onc2ccccc21", + "Cc1cc[n+](CC[n+]2ccc(C)cc2)cc1", + "[O-]c1ccccc1", + "[O-]C=CC=CCC=CC=[N+](C)C", + "C=[N+](-[O-])-C", + "C-[N-]-C(=O)C", + # amino acids + "C[C@H](N)C(=O)O", # A + "NCC(=O)O", # G + "CC[C@H](C)[C@H](N)C(=O)O", # I + "CC(C)C[C@H](N)C(=O)O", # L + "O=C(O)[C@@H]1CCCN1", # P + "CC(C)[C@H](N)C(=O)O", # V + "N[C@@H](Cc1ccccc1)C(=O)O", # F + "N[C@@H](Cc1c[nH]c2ccccc12)C(=O)O", # W + "N[C@@H](Cc1ccc(O)cc1)C(=O)O", # Y + "N[C@@H](CC(=O)O)C(=O)O", # D + "N[C@@H](CCC(=O)O)C(=O)O", # E + "N=C(N)NCCC[C@H](N)C(=O)O", # R + "N[C@@H](Cc1c[nH]cn1)C(=O)O", # H + "NCCCC[C@H](N)C(=O)O", # K + "N[C@@H](CO)C(=O)O", # S + "C[C@@H](O)[C@H](N)C(=O)O", # T + "N[C@@H](CS)C(=O)O", # C + "CSCC[C@H](N)C(=O)O", # M + "NC(=O)C[C@H](N)C(=O)O", # N + "NC(=O)CC[C@H](N)C(=O)O", # Q + # nucleic acids + "Nc1ncnc2c1ncn2[C@H]1C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O1", # A + "Cc1cn([C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)[nH]c1=O", # T + "O=c1ccn([C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)[nH]1", # U + "Nc1ccn([C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)n1", # C + "Nc1nc2c(ncn2[C@H]2C[C@H](OP(=O)(O)O)[C@@H](COP(=O)(O)O)O2)c(=O)[nH]1", # G + # RDKitConverter benchmark 2022-05-23, fixed by better sorting + "CC(C)NS(C)(=O)=Nc1ccc(-c2ccc(-c3nc4[nH]c(O[C@@H]5CO[C@@H]6[C@H](O)CO[C@H]56)nc4cc3Cl)cc2)cc1 CHEMBL4111464", + "C[n+]1c2ccccc2c(C(=O)O)c2[nH]c3ccc(Br)cc3c21 CHEMBL511591", + "C[n+]1ccccc1-c1ccc(NC(=O)c2ccc(C(=O)Nc3ccc(-c4cccc[n+]4C)cc3)cc2)cc1 CHEMBL3230729", + "Cc1ccc(/C(O)=C(/C([S-])=NCc2ccccc2)[n+]2cccc(CO)c2)cc1[N+](=O)[O-] CHEMBL1596426", + "CC(C)(O)c1cc2c(cc1C(C)(C)O)-c1ccccc1-2 CHEMBL1990966", + "[O-]/N=C1/c2ccccc2-c2nc3ccc(C(F)(F)F)cc3nc21 CHEMBL4557878", + "N#Cc1c[nH]c(C(=O)Nc2ccc(-c3cccc[n+]3[O-])cc2C2=CCCCC2)n1 CHEMBL1172116", + ], + ) def test_order_independant(self, smi): # generate mol with hydrogens but without bond orders ref = Chem.MolFromSmiles(smi) @@ -645,40 +707,61 @@ def test_order_independant(self, smi): self.assert_isomorphic_resonance_structure(m, ref) @pytest.mark.xfail(reason="Not currently tackled by the RDKitConverter") - @pytest.mark.parametrize("smi", [ - "C-[N+]#N", - "C-N=[N+]=[N-]", - "C-[O+]=C", - "C-[N+]#[C-]", - ]) + @pytest.mark.parametrize( + "smi", + [ + "C-[N+]#N", + "C-N=[N+]=[N-]", + "C-[O+]=C", + "C-[N+]#[C-]", + ], + ) def test_order_independant_issue_3339(self, smi): self.test_order_independant(smi) def test_warn_conjugated_max_iter(self): smi = "[C-]C=CC=CC=CC=CC=CC=C[C-]" mol = Chem.MolFromSmiles(smi) - with pytest.warns(UserWarning, - match="reasonable number of iterations"): + with pytest.warns( + UserWarning, match="reasonable number of iterations" + ): _rebuild_conjugated_bonds(mol, 2) - @pytest.mark.parametrize("smi", [ - "[Li+]", "[Na+]", "[K+]", "[Rb+]", "[Ag+]", "[Cs+]", - "[Mg+2]", "[Ca+2]", "[Cu+2]", "[Zn+2]", "[Sr+2]", "[Ba+2]", - "[Al+3]", "[Fe+2]", - "[Cl-]", - "[O-2]", - "[Na+].[Cl-]", - ]) + @pytest.mark.parametrize( + "smi", + [ + "[Li+]", + "[Na+]", + "[K+]", + "[Rb+]", + "[Ag+]", + "[Cs+]", + "[Mg+2]", + "[Ca+2]", + "[Cu+2]", + "[Zn+2]", + "[Sr+2]", + "[Ba+2]", + "[Al+3]", + "[Fe+2]", + "[Cl-]", + "[O-2]", + "[Na+].[Cl-]", + ], + ) def test_ions(self, smi): ref = Chem.MolFromSmiles(smi) stripped_mol = self.add_Hs_remove_bo_and_charges(ref) mol = self.assign_bond_orders_and_charges(stripped_mol) assert is_isomorphic(mol, ref) - @pytest.mark.parametrize("smi", [ - "O=C([C@H](CC1=C[NH1+]=CN1)[NH3+])[O-]", - "O=S(C)(C)=NC", - ]) + @pytest.mark.parametrize( + "smi", + [ + "O=C([C@H](CC1=C[NH1+]=CN1)[NH3+])[O-]", + "O=S(C)(C)=NC", + ], + ) def test_reorder_atoms(self, smi): mol = Chem.MolFromSmiles(smi) mol = Chem.AddHs(mol) @@ -692,9 +775,12 @@ 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", - ]) + @pytest.mark.parametrize( + "smi", + [ + "O=S(C)(C)=NC", + ], + ) def test_warn_empty_coords(self, smi): mol = Chem.MolFromSmiles(smi) mol = Chem.AddHs(mol) @@ -708,15 +794,19 @@ def test_pdb_names(self): u = mda.Universe(PDB_helix) mol = u.atoms.convert_to.rdkit() names = u.atoms.names - rd_names = np.array([a.GetProp("_MDAnalysis_name") - for a in mol.GetAtoms()]) + rd_names = np.array( + [a.GetProp("_MDAnalysis_name") for a in mol.GetAtoms()] + ) assert (names == rd_names).all() - @pytest.mark.parametrize("smi", [ - r"F/C(Br)=C(Cl)/I", - r"F\C(Br)=C(Cl)\I", - "F-C(Br)=C(Cl)-I", - ]) + @pytest.mark.parametrize( + "smi", + [ + r"F/C(Br)=C(Cl)/I", + r"F\C(Br)=C(Cl)\I", + "F-C(Br)=C(Cl)-I", + ], + ) def test_bond_stereo_not_stereoany(self, smi): u = mda.Universe.from_smiles(smi) mol = u.atoms.convert_to.rdkit(force=True) @@ -726,11 +816,14 @@ def test_bond_stereo_not_stereoany(self, smi): def test_atom_sorter(self): mol = Chem.MolFromSmiles( - "[H]-[C](-[H])-[C](-[H])-[C]-[C]-[H]", sanitize=False) + "[H]-[C](-[H])-[C](-[H])-[C]-[C]-[H]", sanitize=False + ) # corresponding mol: C=C-C#C # atom indices: 1 3 5 6 mol.UpdatePropertyCache() - sorted_atoms = sorted([atom for atom in mol.GetAtoms() - if atom.GetAtomicNum() > 1], key=_atom_sorter) + sorted_atoms = sorted( + [atom for atom in mol.GetAtoms() if atom.GetAtomicNum() > 1], + key=_atom_sorter, + ) sorted_indices = [atom.GetIdx() for atom in sorted_atoms] assert sorted_indices == [6, 5, 1, 3] diff --git a/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py index bf432990fa4..f82d74a5615 100644 --- a/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py +++ b/testsuite/MDAnalysisTests/converters/test_rdkit_parser.py @@ -32,15 +32,24 @@ # TODO: remove these shims when RDKit # has a release supporting NumPy 2 -Chem = pytest.importorskip('rdkit.Chem') -AllChem = pytest.importorskip('rdkit.Chem.AllChem') +Chem = pytest.importorskip("rdkit.Chem") +AllChem = pytest.importorskip("rdkit.Chem.AllChem") class RDKitParserBase(ParserBase): parser = mda.converters.RDKitParser.RDKitParser - expected_attrs = ['ids', 'names', 'elements', 'masses', 'aromaticities', - 'resids', 'resnums', 'chiralities', 'segids', 'bonds', - ] + expected_attrs = [ + "ids", + "names", + "elements", + "masses", + "aromaticities", + "resids", + "resnums", + "chiralities", + "segids", + "bonds", + ] expected_n_atoms = 0 expected_n_residues = 1 @@ -53,14 +62,14 @@ def top(self, filename): yield p.parse() def test_creates_universe(self, filename): - u = mda.Universe(filename, format='RDKIT') + u = mda.Universe(filename, format="RDKIT") assert isinstance(u, mda.Universe) 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 = 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) @@ -70,7 +79,7 @@ def test_guessed_attributes(self, filename): class TestRDKitParserMOL2(RDKitParserBase): ref_filename = mol2_molecule - expected_attrs = RDKitParserBase.expected_attrs + ['charges', 'types'] + expected_attrs = RDKitParserBase.expected_attrs + ["charges", "types"] expected_n_atoms = 49 expected_n_residues = 1 @@ -89,7 +98,7 @@ def _create_mol_gasteiger_charges(self): def _remove_tripos_charges(self, mol): for atom in mol.GetAtoms(): atom.ClearProp("_TriposPartialCharge") - + @pytest.fixture def top_gas_tripos(self): mol = self._create_mol_gasteiger_charges() @@ -106,17 +115,21 @@ def top_gasteiger(self): mol = self._create_mol_gasteiger_charges() self._remove_tripos_charges(mol) return self.parser(mol).parse() - def test_bond_orders(self, top, filename): expected = [bond.GetBondTypeAsDouble() for bond in filename.GetBonds()] assert top.bonds.order == expected - - def test_multiple_charge_priority(self, - top_gas_tripos, filename_gasteiger): - expected = np.array([ - a.GetDoubleProp('_GasteigerCharge') for a in - filename_gasteiger.GetAtoms()], dtype=np.float32) + + def test_multiple_charge_priority( + self, top_gas_tripos, filename_gasteiger + ): + expected = np.array( + [ + a.GetDoubleProp("_GasteigerCharge") + for a in filename_gasteiger.GetAtoms() + ], + dtype=np.float32, + ) assert_equal(expected, top_gas_tripos.charges.values) def test_multiple_charge_props_warning(self): @@ -129,37 +142,55 @@ def test_multiple_charge_props_warning(self): # Verify the warning assert len(w) == 1 assert "_GasteigerCharge and _TriposPartialCharge" in str( - w[-1].message) + w[-1].message + ) def test_gasteiger_charges(self, top_gasteiger, filename_gasteiger): - expected = np.array([ - a.GetDoubleProp('_GasteigerCharge') for a in - filename_gasteiger.GetAtoms()], dtype=np.float32) + expected = np.array( + [ + a.GetDoubleProp("_GasteigerCharge") + for a in filename_gasteiger.GetAtoms() + ], + dtype=np.float32, + ) assert_equal(expected, top_gasteiger.charges.values) def test_tripos_charges(self, top, filename): - expected = np.array([ - a.GetDoubleProp('_TriposPartialCharge') for a in filename.GetAtoms() - ], dtype=np.float32) + expected = np.array( + [ + a.GetDoubleProp("_TriposPartialCharge") + for a in filename.GetAtoms() + ], + dtype=np.float32, + ) assert_equal(expected, top.charges.values) def test_aromaticity(self, top, filename): - expected = np.array([ - atom.GetIsAromatic() for atom in filename.GetAtoms()]) + expected = np.array( + [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']) + 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 expected_attrs = RDKitParserBase.expected_attrs + [ - 'resnames', 'altLocs', 'chainIDs', 'occupancies', 'icodes', - 'tempfactors'] - + "resnames", + "altLocs", + "chainIDs", + "occupancies", + "icodes", + "tempfactors", + ] + expected_n_atoms = 137 expected_n_residues = 13 expected_n_segments = 1 @@ -172,15 +203,16 @@ def filename(self): def test_partial_residueinfo_raise_error(self, filename): mol = Chem.RemoveHs(filename) mh = Chem.AddHs(mol) - with pytest.raises(ValueError, - match="ResidueInfo is only partially available"): + with pytest.raises( + ValueError, match="ResidueInfo is only partially available" + ): mda.Universe(mh) 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']) + u = mda.Universe(filename, format="RDKIT") + assert_equal(u.atoms.types[:7], ["N", "H", "C", "H", "C", "H", "H"]) class TestRDKitParserSMILES(RDKitParserBase): @@ -215,5 +247,5 @@ def test_bond_orders(self, top, filename): 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']) + u = mda.Universe(filename, format="RDKIT") + assert_equal(u.atoms.types[:7], ["CA", "C", "C", "C", "C", "C", "O"]) diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 9c660c95482..6be06957d79 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -162,6 +162,7 @@ setup\.py | MDAnalysisTests/auxiliary/.*\.py | MDAnalysisTests/lib/.*\.py | MDAnalysisTests/transformations/.*\.py +| MDAnalysisTests/converters/.*\.py ) ''' extend-exclude = ''' From f855022b523683bea5f92cb9d82a1c1bcdf8b8f5 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 24 Dec 2024 12:05:46 +0100 Subject: [PATCH 41/58] pep8speaks config (#4858) --- .pep8speaks.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .pep8speaks.yml diff --git a/.pep8speaks.yml b/.pep8speaks.yml new file mode 100644 index 00000000000..98b4a206b34 --- /dev/null +++ b/.pep8speaks.yml @@ -0,0 +1,40 @@ +scanner: + diff_only: True + linter: pycodestyle + +pycodestyle: # Valid if scanner.linter is pycodestyle + max-line-length: 79 + ignore: ["E203", "E701"] + exclude: [] + count: False + first: False + show-pep8: False + show-source: False + statistics: False + hang-closing: False + filename: [] + select: [] + +flake8: # Valid if scanner.linter is flake8 + max-line-length: 79 + ignore: ["E203", "E501", "E701"] + exclude: [] + count: False + show-source: False + statistics: False + hang-closing: False + filename: [] + select: [] + +no_blank_comment: True +descending_issues_order: False +only_mention_files_with_errors: True + +message: + opened: + header: "" + footer: "" + updated: + header: "" + footer: "" + no_errors: "There are currently no PEP 8 issues detected in this Pull Request. Cheers! :beers: " From 55cce24003d4c0975b1bba387b9e6ac20638780e Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 24 Dec 2024 12:25:48 +0100 Subject: [PATCH 42/58] fmt selections (#4861) --- package/MDAnalysis/selections/__init__.py | 6 ++-- package/MDAnalysis/selections/base.py | 35 ++++++++++++++--------- package/MDAnalysis/selections/charmm.py | 16 +++++++---- package/MDAnalysis/selections/gromacs.py | 2 +- package/MDAnalysis/selections/jmol.py | 6 ++-- package/MDAnalysis/selections/pymol.py | 10 ++++--- package/MDAnalysis/selections/vmd.py | 2 +- package/pyproject.toml | 1 + 8 files changed, 48 insertions(+), 30 deletions(-) diff --git a/package/MDAnalysis/selections/__init__.py b/package/MDAnalysis/selections/__init__.py index 3ccecf7d0b0..acb552b7cac 100644 --- a/package/MDAnalysis/selections/__init__.py +++ b/package/MDAnalysis/selections/__init__.py @@ -84,6 +84,8 @@ def get_writer(filename: str, defaultformat: str) -> base.SelectionWriterBase: try: return _SELECTION_WRITERS[format] except KeyError: - errmsg = (f"Writing as {format} is not implemented; only " - f"{ _SELECTION_WRITERS.keys()} will work.") + errmsg = ( + f"Writing as {format} is not implemented; only " + f"{ _SELECTION_WRITERS.keys()} will work." + ) raise NotImplementedError(errmsg) from None diff --git a/package/MDAnalysis/selections/base.py b/package/MDAnalysis/selections/base.py index eb55a73897e..d9b49e516de 100644 --- a/package/MDAnalysis/selections/base.py +++ b/package/MDAnalysis/selections/base.py @@ -60,7 +60,7 @@ class _Selectionmeta(type): def __init__(cls, name, bases, classdict): type.__init__(type, name, bases, classdict) try: - fmt = util.asiterable(classdict['format']) + fmt = util.asiterable(classdict["format"]) except KeyError: pass else: @@ -97,17 +97,20 @@ class SelectionWriterBase(metaclass=_Selectionmeta): and closed with the :meth:`close` method or when exiting the `with` statement. """ + #: Name of the format. format = None #: Extension of output files. ext = None #: Special character to continue a line across a newline. - continuation = '' + continuation = "" #: Comment format string; should contain '%s' or ``None`` for no comments. commentfmt = None default_numterms = 8 - def __init__(self, filename, mode="w", numterms=None, preamble=None, **kwargs): + def __init__( + self, filename, mode="w", numterms=None, preamble=None, **kwargs + ): """Set up for writing to *filename*. Parameters @@ -126,8 +129,10 @@ def __init__(self, filename, mode="w", numterms=None, preamble=None, **kwargs): use as defaults for :meth:`write` """ self.filename = util.filename(filename, ext=self.ext) - if not mode in ('a', 'w'): - raise ValueError("mode must be one of 'w', 'a', not {0!r}".format(mode)) + if not mode in ("a", "w"): + raise ValueError( + "mode must be one of 'w', 'a', not {0!r}".format(mode) + ) self.mode = mode self._current_mode = mode[0] if numterms is None or numterms < 0: @@ -154,8 +159,8 @@ def comment(self, s): A newline is appended to non-empty strings. """ if self.commentfmt is None: - return '' - return self.commentfmt % s + '\n' + return "" + return self.commentfmt % s + "\n" def write_preamble(self): """Write a header, depending on the file format.""" @@ -188,7 +193,7 @@ def write(self, selection, number=None, name=None, frame=None, mode=None): frame = u.trajectory.ts.frame except AttributeError: frame = 1 # should catch cases when we are analyzing a single PDB (?) - name = name or self.otherargs.get('name', None) + name = name or self.otherargs.get("name", None) if name is None: if number is None: self.number += 1 @@ -203,13 +208,15 @@ def write(self, selection, number=None, name=None, frame=None, mode=None): out = self._outfile self._write_head(out, name=name) for iatom in range(0, len(selection.atoms), step): - line = selection_terms[iatom:iatom + step] + line = selection_terms[iatom : iatom + step] out.write(" ".join(line)) if len(line) == step and not iatom + step == len(selection.atoms): - out.write(' ' + self.continuation + '\n') - out.write(' ') # safe so that we don't have to put a space at the start of tail + out.write(" " + self.continuation + "\n") + out.write( + " " + ) # safe so that we don't have to put a space at the start of tail self._write_tail(out) - out.write('\n') # always terminate with newline + out.write("\n") # always terminate with newline def close(self): """Close the file @@ -234,11 +241,11 @@ def _translate(self, atoms, **kwargs): def _write_head(self, out, **kwargs): """Initial output to open file object *out*.""" - pass # pylint: disable=unnecessary-pass + pass # pylint: disable=unnecessary-pass def _write_tail(self, out, **kwargs): """Last output to open file object *out*.""" - pass # pylint: disable=unnecessary-pass + pass # pylint: disable=unnecessary-pass # Context manager support to match Coordinate writers # all file handles use a with block in their write method, so these do nothing special diff --git a/package/MDAnalysis/selections/charmm.py b/package/MDAnalysis/selections/charmm.py index 5f9b4b4b9b0..62bbbd04975 100644 --- a/package/MDAnalysis/selections/charmm.py +++ b/package/MDAnalysis/selections/charmm.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 @@ -45,20 +45,26 @@ class SelectionWriter(base.SelectionWriterBase): format = ["CHARMM", "str"] ext = "str" - continuation = '-' + continuation = "-" commentfmt = "! %s" - default_numterms = 4 # be conservative because CHARMM only reads 72 columns + default_numterms = ( + 4 # be conservative because CHARMM only reads 72 columns + ) def _translate(self, atoms, **kwargs): # CHARMM index is 1-based def _index(atom): return "BYNUM {0:d}".format((atom.index + 1)) - return base.join(atoms, ' .or.', _index) + return base.join(atoms, " .or.", _index) def _write_head(self, out, **kwargs): out.write(self.comment("MDAnalysis CHARMM selection")) - out.write("DEFINE {name!s} SELECT ".format(**kwargs) + self.continuation + '\n') + out.write( + "DEFINE {name!s} SELECT ".format(**kwargs) + + self.continuation + + "\n" + ) def _write_tail(self, out, **kwargs): out.write("END") diff --git a/package/MDAnalysis/selections/gromacs.py b/package/MDAnalysis/selections/gromacs.py index 3dc8ea79502..e5de251a4bb 100644 --- a/package/MDAnalysis/selections/gromacs.py +++ b/package/MDAnalysis/selections/gromacs.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 diff --git a/package/MDAnalysis/selections/jmol.py b/package/MDAnalysis/selections/jmol.py index 72462b97d2e..ed293668845 100644 --- a/package/MDAnalysis/selections/jmol.py +++ b/package/MDAnalysis/selections/jmol.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 @@ -46,14 +46,14 @@ class SelectionWriter(base.SelectionWriterBase): format = ["Jmol", "spt"] ext = "spt" default_numterms = None - commentfmt = '#' + commentfmt = "#" def _translate(self, atoms, **kwargs): # Jmol indexing is 0 based when using atom bitsets def _index(atom): return str(atom.index) - return base.join(atoms, ' ', _index) + return base.join(atoms, " ", _index) def _write_head(self, out, **kwargs): out.write("@~{name!s} ({{".format(**kwargs)) diff --git a/package/MDAnalysis/selections/pymol.py b/package/MDAnalysis/selections/pymol.py index 080d83817f0..d528a1394b1 100644 --- a/package/MDAnalysis/selections/pymol.py +++ b/package/MDAnalysis/selections/pymol.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 @@ -46,7 +46,7 @@ class SelectionWriter(base.SelectionWriterBase): format = ["PyMol", "pml"] ext = "pml" - continuation = '\\' # quoted backslash! + continuation = "\\" # quoted backslash! commentfmt = "# %s" default_numterms = 6 @@ -55,8 +55,10 @@ def _translate(self, atoms, **kwargs): def _index(atom): return "index {0:d}".format((atom.index + 1)) - return base.join(atoms, ' |', _index) + return base.join(atoms, " |", _index) def _write_head(self, out, **kwargs): out.write(self.comment("MDAnalysis PyMol selection")) - out.write("select {name!s}, ".format(**kwargs) + self.continuation + '\n') + out.write( + "select {name!s}, ".format(**kwargs) + self.continuation + "\n" + ) diff --git a/package/MDAnalysis/selections/vmd.py b/package/MDAnalysis/selections/vmd.py index dc449167511..11081d6ebb1 100644 --- a/package/MDAnalysis/selections/vmd.py +++ b/package/MDAnalysis/selections/vmd.py @@ -53,7 +53,7 @@ class SelectionWriter(base.SelectionWriterBase): format = "VMD" ext = "vmd" - continuation = '\\' + continuation = "\\" commentfmt = "# %s" def _write_head(self, out, **kwargs): diff --git a/package/pyproject.toml b/package/pyproject.toml index 7699f7c3b47..74598c1b049 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -136,6 +136,7 @@ tables\.py | MDAnalysis/lib/.*\.py^ | MDAnalysis/transformations/.*\.py | MDAnalysis/converters/.*\.py +| MDAnalysis/selections/.*\.py ) ''' extend-exclude = ''' From a10e23e681023f383baac7582c39dce4180e2263 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 24 Dec 2024 12:27:40 +0100 Subject: [PATCH 43/58] [fmt] Guesser (#4851) --- package/MDAnalysis/guesser/base.py | 20 +- package/MDAnalysis/guesser/default_guesser.py | 153 +++++++----- package/pyproject.toml | 1 + .../MDAnalysisTests/guesser/test_base.py | 135 ++++++----- .../guesser/test_default_guesser.py | 227 ++++++++++-------- testsuite/pyproject.toml | 1 + 6 files changed, 304 insertions(+), 233 deletions(-) diff --git a/package/MDAnalysis/guesser/base.py b/package/MDAnalysis/guesser/base.py index 0fd7a7e18ea..c7bcc21e823 100644 --- a/package/MDAnalysis/guesser/base.py +++ b/package/MDAnalysis/guesser/base.py @@ -63,10 +63,11 @@ class FooGuesser(GuesserBase): .. versionadded:: 2.8.0 """ + def __init__(cls, name, bases, classdict): type.__init__(type, name, bases, classdict) - _GUESSERS[classdict['context'].upper()] = cls + _GUESSERS[classdict["context"].upper()] = cls class GuesserBase(metaclass=_GuesserMeta): @@ -87,7 +88,8 @@ class GuesserBase(metaclass=_GuesserMeta): .. versionadded:: 2.8.0 """ - context = 'base' + + context = "base" _guesser_methods: Dict = {} def __init__(self, universe=None, **kwargs): @@ -149,8 +151,10 @@ def guess_attr(self, attr_to_guess, force_guess=False): 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}') + 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 @@ -161,7 +165,8 @@ def guess_attr(self, attr_to_guess, force_guess=False): # 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)) + getattr(self._universe.atoms, attr_to_guess, None) + ) empty_values = top_attr.are_values_missing(attr_values) @@ -175,8 +180,9 @@ def guess_attr(self, attr_to_guess, force_guess=False): 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') + 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(guesser_method()) diff --git a/package/MDAnalysis/guesser/default_guesser.py b/package/MDAnalysis/guesser/default_guesser.py index ee4ede1d7d0..2ecc64c1e66 100644 --- a/package/MDAnalysis/guesser/default_guesser.py +++ b/package/MDAnalysis/guesser/default_guesser.py @@ -161,7 +161,8 @@ class DefaultGuesser(GuesserBase): .. versionadded:: 2.8.0 """ - context = 'default' + + context = "default" def __init__( self, @@ -170,7 +171,7 @@ def __init__( vdwradii=None, fudge_factor=0.55, lower_bound=0.1, - **kwargs + **kwargs, ): super().__init__( universe, @@ -178,17 +179,17 @@ def __init__( vdwradii=vdwradii, fudge_factor=fudge_factor, lower_bound=lower_bound, - **kwargs + **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, + "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): @@ -225,18 +226,21 @@ def guess_masses(self, atom_types=None, indices_to_guess=None): except NoDataError: try: atom_types = self.guess_types( - atom_types=self._universe.atoms.names) + atom_types=self._universe.atoms.names + ) except NoDataError: raise NoDataError( "there is no reference attributes" " (elements, types, or names)" - " in this universe to guess mass from") from None + " in this universe to guess mass from" + ) from None 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) + masses = np.array( + [self.get_atom_mass(atom) for atom in atom_types], dtype=np.float64 + ) return masses def get_atom_mass(self, element): @@ -256,7 +260,8 @@ def get_atom_mass(self, element): "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) + PendingDeprecationWarning, + ) return 0.0 def guess_atom_mass(self, atomname): @@ -295,13 +300,16 @@ def guess_types(self, atom_types=None, indices_to_guess=None): except NoDataError: raise NoDataError( "there is no reference attributes in this universe " - "to guess types from") from None + "to guess types from" + ) from None 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) + 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. @@ -315,7 +323,7 @@ def guess_atom_element(self, atomname): 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 @@ -331,16 +339,16 @@ def guess_atom_element(self, atomname): :func:`guess_atom_type` :mod:`MDAnalysis.guesser.tables` """ - NUMBERS = re.compile(r'[0-9]') # match numbers - SYMBOLS = re.compile(r'[*+-]') # match *, +, - - if atomname == '': - return '' + 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() + no_symbols = re.sub(SYMBOLS, "", atomname) + name = re.sub(NUMBERS, "", no_symbols).upper() # just in case if name in tables.atomelements: @@ -393,7 +401,7 @@ def guess_bonds(self, atoms=None, coords=None): Raises ------ - :exc:`ValueError` + :exc:`ValueError` If inputs are malformed or `vdwradii` data is missing. @@ -410,32 +418,37 @@ def guess_bonds(self, atoms=None, coords=None): if len(atoms) != len(coords): raise ValueError("'atoms' and 'coord' must be the same length") - fudge_factor = self._kwargs.get('fudge_factor', 0.55) + 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) + 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'): + 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'")) + 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) + lower_bound = self._kwargs.get("lower_bound", 0.1) - box = self._kwargs.get('box', None) + box = self._kwargs.get("box", None) if box is not None: box = np.asarray(box) @@ -447,14 +460,14 @@ def guess_bonds(self, atoms=None, coords=None): bonds = [] - pairs, dist = distances.self_capped_distance(coords, - max_cutoff=2.0 * max_vdw, - min_cutoff=lower_bound, - box=box) + 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): + d = ( + vdwradii[atomtypes[i]] + vdwradii[atomtypes[j]] + ) * fudge_factor + if dist[idx] < d: bonds.append((atoms[i].index, atoms[j].index)) return tuple(bonds) @@ -480,18 +493,21 @@ def guess_angles(self, bonds=None): -------- :meth:`guess_bonds` - """ + """ from ..core.universe import Universe angles_found = set() - + if bonds is None: - if hasattr(self._universe.atoms, 'bonds'): + 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)) + temp_u.add_bonds( + self.guess_bonds( + self._universe.atoms, self._universe.atoms.positions + ) + ) bonds = temp_u.atoms.bonds for b in bonds: @@ -501,7 +517,8 @@ def guess_angles(self, bonds=None): 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]) + [other_a.index, atom.index, third_a.index] + ) # first index always less than last if desc[0] > desc[-1]: desc = desc[::-1] @@ -530,15 +547,18 @@ def guess_dihedrals(self, angles=None): from ..core.universe import Universe if angles is None: - if hasattr(self._universe.atoms, 'angles'): + 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_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 @@ -549,8 +569,9 @@ def guess_dihedrals(self, angles=None): 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 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) @@ -580,14 +601,17 @@ def guess_improper_dihedrals(self, angles=None): from ..core.universe import Universe if angles is None: - if hasattr(self._universe.atoms, 'angles'): + 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_bonds( + self.guess_bonds( + self._universe.atoms, self._universe.atoms.positions + ) + ) temp_u.add_angles(self.guess_angles(temp_u.atoms.bonds)) @@ -652,7 +676,12 @@ def guess_gasteiger_charges(self, atomgroup): 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) + return np.array( + [ + atom.GetDoubleProp("_GasteigerCharge") + for atom in mol.GetAtoms() + ], + dtype=np.float32, + ) diff --git a/package/pyproject.toml b/package/pyproject.toml index 74598c1b049..876d2910dbd 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -135,6 +135,7 @@ tables\.py | MDAnalysis/visualization/.*\.py | MDAnalysis/lib/.*\.py^ | MDAnalysis/transformations/.*\.py +| MDAnalysis/guesser/.*\.py | MDAnalysis/converters/.*\.py | MDAnalysis/selections/.*\.py ) diff --git a/testsuite/MDAnalysisTests/guesser/test_base.py b/testsuite/MDAnalysisTests/guesser/test_base.py index c44fdc3c591..2c753c9af3e 100644 --- a/testsuite/MDAnalysisTests/guesser/test_base.py +++ b/testsuite/MDAnalysisTests/guesser/test_base.py @@ -20,89 +20,98 @@ # 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 +import numpy as np +import pytest +from MDAnalysis import _GUESSERS, _TOPOLOGY_ATTRS +from MDAnalysis.core.topology import Topology +from MDAnalysis.core.topologyattrs import Atomnames, Atomtypes, Masses from MDAnalysis.exceptions import NoDataError +from MDAnalysis.guesser.base import GuesserBase, get_guesser from numpy.testing import assert_allclose, assert_equal -from MDAnalysis import _TOPOLOGY_ATTRS, _GUESSERS - -class TestBaseGuesser(): +class TestBaseGuesser: def test_get_guesser(self): class TestGuesser1(GuesserBase): - context = 'test1' + context = "test1" class TestGuesser2(GuesserBase): - context = 'test2' + context = "test2" - assert get_guesser(TestGuesser1).context == 'test1' - assert get_guesser('test1').context == 'test1' - assert get_guesser(TestGuesser2()).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' + 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 + 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']) + 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)) + 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) + 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']) + 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)) + 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) + 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)) + 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) + 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, ['', '', '', '']) + 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, ["", "", "", ""]) def test_guess_topology_objects_existing_read(self): u = mda.Universe(datafiles.CONECT) @@ -166,7 +175,7 @@ def test_guess_topology_objects_out_of_order_init(self): u = mda.Universe( datafiles.PDB_small, to_guess=["dihedrals", "angles", "bonds"], - guess_bonds=False + guess_bonds=False, ) assert len(u.atoms.angles) == 6123 assert len(u.atoms.dihedrals) == 8921 @@ -177,8 +186,7 @@ def test_guess_topology_objects_out_of_order_guess(self): u.atoms.angles u.guess_TopologyAttrs( - "default", - to_guess=["dihedrals", "angles", "bonds"] + "default", to_guess=["dihedrals", "angles", "bonds"] ) assert len(u.atoms.angles) == 6123 assert len(u.atoms.dihedrals) == 8921 @@ -223,43 +231,50 @@ 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') + 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') - + 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') + masses = default_guesser.guess_attr("mass") def test_Universe_guess_bonds_deprecated(): with pytest.warns( - DeprecationWarning, - match='`guess_bonds` keyword is deprecated' + 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] + [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.] - ]) + a = np.array( + [ + [0.0, 0.0, 150.0], + [0.0, 0.0, 150.0], + [200.0, 0.0, 150.0], + [0.0, 0.0, 150.0], + [100.0, 100.0, 150.0], + [200.0, 100.0, 150.0], + [0.0, 200.0, 150.0], + [100.0, 200.0, 150.0], + [200.0, 200.0, 150.0], + ] + ) 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 fe8e012c8c4..92c24593c35 100644 --- a/testsuite/MDAnalysisTests/guesser/test_default_guesser.py +++ b/testsuite/MDAnalysisTests/guesser/test_default_guesser.py @@ -41,8 +41,9 @@ except ImportError: pass -requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), - reason="requires RDKit") +requires_rdkit = pytest.mark.skipif( + import_not_available("rdkit"), reason="requires RDKit" +) @pytest.fixture @@ -52,68 +53,76 @@ def default_guesser(): class TestGuessMasses(object): def test_guess_masses_from_universe(self): - topology = Topology(3, attrs=[Atomtypes(['C', 'C', 'H'])]) + 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) + 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'] + elements = ["H", "Ca", "Am"] values = np.array([1.008, 40.08000, 243.0]) - assert_allclose(default_guesser.guess_masses( - elements), values, atol=0) + assert_allclose(default_guesser.guess_masses(elements), values, atol=0) def test_guess_masses_warn(self): - topology = Topology(2, attrs=[Atomtypes(['X', 'Z'])]) + 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']) + 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),)) + @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) + 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(NoDataError, - match=('there is no reference attributes ')): - u.guess_TopologyAttrs('default', ['masses']) + with pytest.raises( + NoDataError, 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']) + 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)) + 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' + assert default_guesser.guess_atom_element("MG2+") == "MG" def test_guess_atom_element_empty(self, default_guesser): - assert default_guesser.guess_atom_element('') == '' + assert default_guesser.guess_atom_element("") == "" def test_guess_atom_element_singledigit(self, default_guesser): - assert default_guesser.guess_atom_element('1') == '1' + 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' + 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) + 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]) + atom_types=names, indices_to_guess=[True, False, True] + ) assert_equal(elements, guessed_elements) def test_guess_elements_from_no_data(self): @@ -123,36 +132,39 @@ def test_guess_elements_from_no_data(self): "universe to guess types from" ) with pytest.warns(UserWarning, 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'), - )) + 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) + 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(NoDataError, match=msg): - u.guess_TopologyAttrs(to_guess=['bonds']) + u.guess_TopologyAttrs(to_guess=["bonds"]) def test_guess_bond_vdw_error(): @@ -164,16 +176,16 @@ def test_guess_bond_vdw_error(): 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]]) + 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') + u.guess_TopologyAttrs(to_guess=["angles"]) + assert hasattr(u, "angles") + assert not hasattr(u, "bonds") def test_guess_impropers(default_guesser): @@ -188,41 +200,42 @@ def test_guess_impropers(default_guesser): def test_guess_dihedrals_with_no_angles(): - "Test guessing dihedrals for atoms 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') + 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 " + "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') + 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 " + "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') + 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: + for i, j in arr: if i > j: i, j = j, i out.append((i, j)) @@ -231,52 +244,52 @@ def bond_sort(arr): 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))) + 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))) @pytest.mark.parametrize( - "fudge_factor, n_bonds", - [(0, 0), (0.55, 4), (200, 6)] + "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") - ) + 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']) + 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)) + 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']) + 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)) + 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", -]) +@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) @@ -285,25 +298,30 @@ def test_guess_aromaticities(smi): u = mda.Universe(mol) guesser = DefaultGuesser(None) values = guesser.guess_aromaticities(u.atoms) - u.guess_TopologyAttrs(to_guess=['aromaticities']) + 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", -]) +@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) + 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) @@ -312,7 +330,8 @@ def test_guess_gasteiger_charges(smi): @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') + 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/pyproject.toml b/testsuite/pyproject.toml index 6be06957d79..1636dc406d0 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -162,6 +162,7 @@ setup\.py | MDAnalysisTests/auxiliary/.*\.py | MDAnalysisTests/lib/.*\.py | MDAnalysisTests/transformations/.*\.py +| MDAnalysisTests/guesser/.*\.py | MDAnalysisTests/converters/.*\.py ) ''' From 9312fa67f163ec055a66f5756a182206fbea3130 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 24 Dec 2024 12:37:18 +0100 Subject: [PATCH 44/58] fmt misc (#4859) --- benchmarks/benchmarks/GRO.py | 3 +- benchmarks/benchmarks/ag_methods.py | 128 ++++++++-------- benchmarks/benchmarks/analysis/distances.py | 34 ++--- benchmarks/benchmarks/analysis/leaflet.py | 35 +++-- benchmarks/benchmarks/analysis/psa.py | 156 ++++++++------------ benchmarks/benchmarks/analysis/rdf.py | 21 +-- benchmarks/benchmarks/analysis/rms.py | 61 +++----- benchmarks/benchmarks/import.py | 4 +- benchmarks/benchmarks/selections.py | 78 +++++----- benchmarks/benchmarks/topology.py | 26 ++-- benchmarks/benchmarks/traj_reader.py | 22 +-- maintainer/adapt_sitemap.py | 75 +++++++--- maintainer/norm_version.py | 2 +- maintainer/update_json_stubs_sitemap.py | 110 +++++++------- 14 files changed, 362 insertions(+), 393 deletions(-) diff --git a/benchmarks/benchmarks/GRO.py b/benchmarks/benchmarks/GRO.py index ad34b41915e..ba47f0943f8 100644 --- a/benchmarks/benchmarks/GRO.py +++ b/benchmarks/benchmarks/GRO.py @@ -1,8 +1,9 @@ +import MDAnalysis as mda import numpy as np from MDAnalysis.coordinates.GRO import GROReader from MDAnalysis.topology.GROParser import GROParser from MDAnalysisTests.datafiles import GRO -import MDAnalysis as mda + class GROReadBench(object): def time_read_GRO_coordinates(self): diff --git a/benchmarks/benchmarks/ag_methods.py b/benchmarks/benchmarks/ag_methods.py index 52616ba4511..e2c5cb8e434 100644 --- a/benchmarks/benchmarks/ag_methods.py +++ b/benchmarks/benchmarks/ag_methods.py @@ -2,35 +2,31 @@ import numpy as np try: - from MDAnalysisTests.datafiles import (GRO, TPR, XTC, - PSF, DCD, - TRZ_psf, TRZ) from MDAnalysis.exceptions import NoDataError + from MDAnalysisTests.datafiles import DCD, GRO, PSF, TPR, TRZ, XTC, TRZ_psf except: pass + class AtomGroupMethodsBench(object): """Benchmarks for the various MDAnalysis atomgroup methods. """ + # NOTE: the write() method has been # excluded as file writing is considered # a separate benchmarking category params = (10, 100, 1000, 10000) - param_names = ['num_atoms'] + param_names = ["num_atoms"] def setup(self, num_atoms): self.u = MDAnalysis.Universe(GRO) self.ag = self.u.atoms[:num_atoms] self.weights = np.ones(num_atoms) - self.vdwradii = {'H':1.0, - 'C':1.0, - 'N':1.0, - 'O':1.0, - 'DUMMY':1.0} - self.rot_matrix = np.ones((3,3)) - self.trans = np.ones((4,4)) + self.vdwradii = {"H": 1.0, "C": 1.0, "N": 1.0, "O": 1.0, "DUMMY": 1.0} + self.rot_matrix = np.ones((3, 3)) + self.trans = np.ones((4, 4)) def time_bbox_pbc(self, num_atoms): """Benchmark bounding box calculation @@ -60,15 +56,13 @@ def time_center_pbc(self, num_atoms): """Benchmark center calculation with pbc active. """ - self.ag.center(weights=self.weights, - pbc=True) + self.ag.center(weights=self.weights, pbc=True) def time_center_no_pbc(self, num_atoms): """Benchmark center calculation with pbc inactive. """ - self.ag.center(weights=self.weights, - pbc=False) + self.ag.center(weights=self.weights, pbc=False) def time_centroid_pbc(self, num_atoms): """Benchmark centroid calculation with @@ -83,8 +77,7 @@ def time_centroid_no_pbc(self, num_atoms): self.ag.centroid(pbc=False) def time_concatenate(self, num_atoms): - """Benchmark atomgroup concatenation. - """ + """Benchmark atomgroup concatenation.""" self.ag.concatenate(self.ag) def time_difference(self, num_atoms): @@ -97,7 +90,7 @@ def time_groupby(self, num_atoms): """Benchmark atomgroup groupby operation. """ - self.ag.groupby('resnames') + self.ag.groupby("resnames") def time_guess_bonds(self, num_atoms): """Benchmark atomgroup bond guessing @@ -106,18 +99,15 @@ def time_guess_bonds(self, num_atoms): self.ag.guess_bonds(self.vdwradii) def time_intersection(self, num_atoms): - """Benchmark ag intersection. - """ + """Benchmark ag intersection.""" self.ag.intersection(self.ag) def time_is_strict_subset(self, num_atoms): - """Benchmark ag strict subset operation. - """ + """Benchmark ag strict subset operation.""" self.ag.is_strict_subset(self.ag) def time_is_strict_superset(self, num_atoms): - """Benchmark ag strict superset operation. - """ + """Benchmark ag strict superset operation.""" self.ag.is_strict_superset(self.ag) def time_isdisjoint(self, num_atoms): @@ -155,19 +145,17 @@ def time_rotateby(self, num_atoms): """Benchmark rotation by an angle of the ag coordinates. """ - self.ag.rotateby(angle=45, - axis=[1,0,0]) + self.ag.rotateby(angle=45, axis=[1, 0, 0]) def time_split(self, num_atoms): """Benchmark ag splitting into multiple ags based on a simple criterion. """ - self.ag.split('residue') + self.ag.split("residue") def time_subtract(self, num_atoms): - """Benchmark ag subtraction. - """ + """Benchmark ag subtraction.""" self.ag.subtract(self.ag) def time_symmetric_difference(self, num_atoms): @@ -187,7 +175,7 @@ def time_translate(self, num_atoms): translation vector to the ag coordinates. """ - self.ag.translate([0,0.5,1]) + self.ag.translate([0, 0.5, 1]) def time_union(self, num_atoms): """Benchmark union operation @@ -202,14 +190,13 @@ def time_wrap(self, num_atoms): self.ag.wrap() - class AtomGroupAttrsBench(object): """Benchmarks for the various MDAnalysis atomgroup attributes. """ params = (10, 100, 1000, 10000) - param_names = ['num_atoms'] + param_names = ["num_atoms"] def setup(self, num_atoms): self.u = MDAnalysis.Universe(GRO) @@ -235,7 +222,7 @@ def time_dihedral(self, num_atoms): """ self.ag[:4].dihedral - #TODO: use universe / ag that + # TODO: use universe / ag that # is suitable for force calc def time_forces(self, num_atoms): """Benchmark atomgroup force @@ -246,7 +233,7 @@ def time_forces(self, num_atoms): except NoDataError: pass - #TODO: use universe / ag that + # TODO: use universe / ag that # is suitable for velocity extraction def time_velocity(self, num_atoms): """Benchmark atomgroup velocity @@ -265,8 +252,7 @@ def time_improper(self, num_atoms): self.ag[:4].improper def time_indices(self, num_atoms): - """Benchmark atom index calculation. - """ + """Benchmark atom index calculation.""" self.ag.ix def time_atomcount(self, num_atoms): @@ -326,15 +312,17 @@ def time_bond(self, num_atoms): class CompoundSplitting(object): """Test how fast can we split compounds into masks and apply them - + The benchmark used in Issue #3000. Parameterizes multiple compound number and size combinations. """ - - params = [(100, 10000, 1000000), # n_atoms - (1, 10, 100), # n_compounds - (True, False), # homogeneous - (True, False)] # contiguous + + params = [ + (100, 10000, 1000000), # n_atoms + (1, 10, 100), # n_compounds + (True, False), # homogeneous + (True, False), + ] # contiguous def setup(self, n_atoms, n_compounds, homogeneous, contiguous): rg = np.random.Generator(np.random.MT19937(3000)) @@ -345,7 +333,7 @@ def setup(self, n_atoms, n_compounds, homogeneous, contiguous): if n_compounds == 1 and not (homogeneous and contiguous): raise NotImplementedError - + if n_compounds == n_atoms: if not (homogeneous and contiguous): raise NotImplementedError @@ -354,51 +342,56 @@ def setup(self, n_atoms, n_compounds, homogeneous, contiguous): ats_per_compound, remainder = divmod(n_atoms, n_compounds) if remainder: raise NotImplementedError - compound_indices = np.tile(np.arange(n_compounds), - (ats_per_compound, 1)).T.ravel() + compound_indices = np.tile( + np.arange(n_compounds), (ats_per_compound, 1) + ).T.ravel() else: - compound_indices = np.sort(np.floor(rg.random(n_atoms) - * n_compounds).astype(np.int)) - + compound_indices = np.sort( + np.floor(rg.random(n_atoms) * n_compounds).astype(np.int) + ) + unique_indices = np.unique(compound_indices) if len(unique_indices) != n_compounds: raise RuntimeError - + if not contiguous: rg.shuffle(compound_indices) - - self.u = MDAnalysis.Universe.empty(n_atoms, - n_residues=n_compounds, - n_segments=1, - atom_resindex=compound_indices, - trajectory=True) - self.u.atoms.positions = rg.random((n_atoms, 3), - dtype=np.float32) * 100 + + self.u = MDAnalysis.Universe.empty( + n_atoms, + n_residues=n_compounds, + n_segments=1, + atom_resindex=compound_indices, + trajectory=True, + ) + self.u.atoms.positions = rg.random((n_atoms, 3), dtype=np.float32) * 100 self.u.dimensions = [50, 50, 50, 90, 90, 90] def time_center_compounds(self, *args): - self.u.atoms.center(None, compound='residues') + self.u.atoms.center(None, compound="residues") class FragmentFinding(object): """Test how quickly we find fragments (distinct molecules from bonds)""" + # if we try to parametrize over topology & # trajectory formats asv will use all # possible combinations, so instead handle # this in setup() - params = ('large_fragment_small_solvents', - 'large_fragment', - 'polymer_chains', # 20ish polymer chains - ) - param_names = ['universe_type'] + params = ( + "large_fragment_small_solvents", + "large_fragment", + "polymer_chains", # 20ish polymer chains + ) + param_names = ["universe_type"] def setup(self, universe_type): - if universe_type == 'large_fragment_small_solvents': - univ = (TPR, XTC) - elif universe_type == 'large_fragment': + if universe_type == "large_fragment_small_solvents": + univ = (TPR, XTC) + elif universe_type == "large_fragment": univ = (PSF, DCD) else: - univ = (TRZ_psf, TRZ) + univ = (TRZ_psf, TRZ) self.u = MDAnalysis.Universe(*univ) def time_find_fragments(self, universe_type): @@ -407,6 +400,7 @@ def time_find_fragments(self, universe_type): class FragmentCaching(FragmentFinding): """Test how quickly we find cached fragments""" + def setup(self, universe_type): super(FragmentCaching, self).setup(universe_type) frags = self.u.atoms.fragments # Priming the cache diff --git a/benchmarks/benchmarks/analysis/distances.py b/benchmarks/benchmarks/analysis/distances.py index 71af1e2c2b7..bbe60955734 100644 --- a/benchmarks/benchmarks/analysis/distances.py +++ b/benchmarks/benchmarks/analysis/distances.py @@ -24,32 +24,20 @@ class DistancesBench(object): def setup(self, num_atoms): np.random.seed(17809) - self.coords_1 = np.random.random_sample((num_atoms, 3)).astype( - np.float32 - ) + self.coords_1 = np.random.random_sample((num_atoms, 3)).astype(np.float32) np.random.seed(9008716) - self.coords_2 = np.random.random_sample((num_atoms, 3)).astype( - np.float32 - ) + self.coords_2 = np.random.random_sample((num_atoms, 3)).astype(np.float32) np.random.seed(15809) - self.coords_3 = np.random.random_sample((num_atoms, 3)).astype( - np.float32 - ) + self.coords_3 = np.random.random_sample((num_atoms, 3)).astype(np.float32) np.random.seed(871600) - self.coords_4 = np.random.random_sample((num_atoms, 3)).astype( - np.float32 - ) + self.coords_4 = np.random.random_sample((num_atoms, 3)).astype(np.float32) - self.allocated_array_2D = np.empty( - (num_atoms, num_atoms), dtype=np.float64 - ) + self.allocated_array_2D = np.empty((num_atoms, num_atoms), dtype=np.float64) self.array_shape_1D = int(num_atoms * (num_atoms - 1) / 2.0) - self.allocated_array_1D = np.empty( - self.array_shape_1D, dtype=np.float64 - ) + self.allocated_array_1D = np.empty(self.array_shape_1D, dtype=np.float64) self.u = mda.Universe(GRO) self.ag1 = self.u.atoms[:num_atoms] - self.ag2 = self.u.atoms[num_atoms: 2 * num_atoms] + self.ag2 = self.u.atoms[num_atoms : 2 * num_atoms] self.ag3 = self.u.atoms[-num_atoms:] def time_distance_array(self, num_atoms): @@ -139,9 +127,7 @@ def time_between(self, num_atoms): of atomgroup that is within a specific distance of two other atomgroups. """ - distances.between( - group=self.ag3, A=self.ag1, B=self.ag2, distance=15.0 - ) + distances.between(group=self.ag3, A=self.ag1, B=self.ag2, distance=15.0) def time_calc_bonds(self, num_atoms): """Benchmark calculation of bonds between @@ -153,9 +139,7 @@ def time_calc_angles(self, num_atoms): """Benchmark calculation of angles between atoms in three atomgroups. """ - mda.lib.distances.calc_angles( - self.coords_1, self.coords_2, self.coords_3 - ) + mda.lib.distances.calc_angles(self.coords_1, self.coords_2, self.coords_3) def time_calc_dihedrals(self, num_atoms): """Benchmark calculation of dihedrals between diff --git a/benchmarks/benchmarks/analysis/leaflet.py b/benchmarks/benchmarks/analysis/leaflet.py index 791ca7c8e68..5939d46efbd 100644 --- a/benchmarks/benchmarks/analysis/leaflet.py +++ b/benchmarks/benchmarks/analysis/leaflet.py @@ -12,29 +12,30 @@ except: pass + class LeafletBench(object): """Benchmarks for MDAnalysis.analysis.leaflet. LeafletFinder """ - params = ([7.0, 15.0, 23.0], - [None, True, False], - [True, False]) - param_names = ['cutoff', 'sparse', 'pbc'] + params = ([7.0, 15.0, 23.0], [None, True, False], [True, False]) + param_names = ["cutoff", "sparse", "pbc"] def setup(self, cutoff, sparse, pbc): self.u = MDAnalysis.Universe(Martini_membrane_gro) - self.headgroup_sel = 'name PO4' + self.headgroup_sel = "name PO4" def time_leafletfinder(self, cutoff, sparse, pbc): """Benchmark LeafletFinder for test lipid membrane system. """ - leaflet.LeafletFinder(universe=self.u, - select=self.headgroup_sel, - cutoff=cutoff, - pbc=pbc, - sparse=sparse) + leaflet.LeafletFinder( + universe=self.u, + select=self.headgroup_sel, + cutoff=cutoff, + pbc=pbc, + sparse=sparse, + ) class LeafletOptimizeBench(object): @@ -42,20 +43,18 @@ class LeafletOptimizeBench(object): optimize_cutoff """ - params = ([None, True, False], - [True, False]) - param_names = ['sparse', 'pbc'] + params = ([None, True, False], [True, False]) + param_names = ["sparse", "pbc"] def setup(self, sparse, pbc): self.u = MDAnalysis.Universe(Martini_membrane_gro) - self.headgroup_sel = 'name PO4' + self.headgroup_sel = "name PO4" def time_optimize_cutoff(self, sparse, pbc): """Benchmark optimize_cutoff for test lipid membrane system using default network distance range. """ - leaflet.optimize_cutoff(universe=self.u, - select=self.headgroup_sel, - pbc=pbc, - sparse=sparse) + leaflet.optimize_cutoff( + universe=self.u, select=self.headgroup_sel, pbc=pbc, sparse=sparse + ) diff --git a/benchmarks/benchmarks/analysis/psa.py b/benchmarks/benchmarks/analysis/psa.py index bc7b73ac835..14f0b27907d 100644 --- a/benchmarks/benchmarks/analysis/psa.py +++ b/benchmarks/benchmarks/analysis/psa.py @@ -6,97 +6,85 @@ except: pass + class PSA_sqnormBench(object): - """Benchmarks for MDAnalysis.analysis.psa. - sqnorm - """ + """Benchmarks for MDAnalysis.analysis.psa. + sqnorm + """ - params = ([2,3,4], - [100,1000,10000], - [None, 0, 1, -1]) + params = ([2, 3, 4], [100, 1000, 10000], [None, 0, 1, -1]) - # num_cols is equivalent to dimensions - # num_rows is equivalent to i.e., num atoms - param_names = ['num_cols', - 'num_rows', - 'axis'] + # num_cols is equivalent to dimensions + # num_rows is equivalent to i.e., num atoms + param_names = ["num_cols", "num_rows", "axis"] - def setup(self, num_cols, num_rows, axis): - np.random.seed(170089) - self.v = np.random.rand(num_rows, num_cols) + def setup(self, num_cols, num_rows, axis): + np.random.seed(170089) + self.v = np.random.rand(num_rows, num_cols) + + def time_sqnorm(self, num_cols, num_rows, axis): + """Benchmark sqnorm in psa module""" + psa.sqnorm(v=self.v, axis=axis) - def time_sqnorm(self, num_cols, num_rows, axis): - """Benchmark sqnorm in psa module - """ - psa.sqnorm(v=self.v, axis=axis) class PSA_get_msd_matrixBench(object): - """Benchmarks for MDAnalysis.analysis.psa. - get_msd_matrix - """ + """Benchmarks for MDAnalysis.analysis.psa. + get_msd_matrix + """ + + params = ([10, 100, 1000], [5, 25, 50]) + + # since the function is defined to work with + # 3N dimension data sets, we will restrict + # benchmarks to that dimensionality + param_names = ["time_steps", "n_atoms"] + + def setup(self, time_steps, n_atoms): + np.random.seed(170089) + self.P = np.random.rand(time_steps, n_atoms, 3) + np.random.seed(971132) + self.Q = np.random.rand(time_steps, n_atoms, 3) + + def time_get_msd_matrix(self, time_steps, n_atoms): + """Benchmark for get_msd_matrix in psa module""" + # only default argument for axis is benchmarked + psa.get_msd_matrix(P=self.P, Q=self.Q, axis=None) - params = ([10,100,1000], - [5,25,50]) - - # since the function is defined to work with - # 3N dimension data sets, we will restrict - # benchmarks to that dimensionality - param_names = ['time_steps', - 'n_atoms'] - - def setup(self, time_steps, n_atoms): - np.random.seed(170089) - self.P = np.random.rand(time_steps, - n_atoms, - 3) - np.random.seed(971132) - self.Q = np.random.rand(time_steps, - n_atoms, - 3) - - def time_get_msd_matrix(self, time_steps, n_atoms): - """Benchmark for get_msd_matrix in psa module - """ - # only default argument for axis is benchmarked - psa.get_msd_matrix(P=self.P, - Q=self.Q, - axis=None) class PSA_get_coord_axesBench(object): """Benchmarks for MDAnalysis.analysis.psa. get_coord_axes """ - params = ([10,100,1000], - [5, 25, 50]) + params = ([10, 100, 1000], [5, 25, 50]) - param_names = ['time_steps', - 'n_atoms'] + param_names = ["time_steps", "n_atoms"] def setup(self, time_steps, n_atoms): np.random.seed(170089) # only using condensed path input # data structure for now - self.path = np.random.rand(time_steps, - n_atoms * 3) + self.path = np.random.rand(time_steps, n_atoms * 3) def time_get_coord_axes(self, time_steps, n_atoms): - """Benchmark get_coord_axes in psa module - """ + """Benchmark get_coord_axes in psa module""" psa.get_coord_axes(path=self.path) + class PSA_get_path_metric_funcBench(object): """Benchmark for MDAnalysis.analysis.psa. get_path_metric_func """ - params = (['hausdorff', - 'weighted_average_hausdorff', - 'average_hausdorff', - 'hausdorff_neighbors', - 'discrete_frechet']) + params = [ + "hausdorff", + "weighted_average_hausdorff", + "average_hausdorff", + "hausdorff_neighbors", + "discrete_frechet", + ] - param_names = ['path_metric'] + param_names = ["path_metric"] def time_get_path_metric_func(self, path_metric): """Benchmark for get_path_metric_func in psa @@ -104,54 +92,38 @@ def time_get_path_metric_func(self, path_metric): """ psa.get_path_metric_func(name=path_metric) + class PSA_metricBench(object): """Benchmarks for the various path metric calculations in the psa module. """ - params = ([10,100,200], - [5,25,50]) + params = ([10, 100, 200], [5, 25, 50]) - param_names = ['time_steps', - 'n_atoms'] + param_names = ["time_steps", "n_atoms"] def setup(self, time_steps, n_atoms): np.random.seed(170089) - self.P = np.random.rand(time_steps, - n_atoms, - 3) + self.P = np.random.rand(time_steps, n_atoms, 3) np.random.seed(971132) - self.Q = np.random.rand(time_steps, - n_atoms, - 3) + self.Q = np.random.rand(time_steps, n_atoms, 3) def time_hausdorff(self, time_steps, n_atoms): - """Benchmark for hausdorff() in psa module. - """ - psa.hausdorff(P=self.P, - Q=self.Q) + """Benchmark for hausdorff() in psa module.""" + psa.hausdorff(P=self.P, Q=self.Q) def time_hausdorff_wavg(self, time_steps, n_atoms): - """Benchmark for hausdorff_wavg() in psa module. - """ - psa.hausdorff_wavg(P=self.P, - Q=self.Q) + """Benchmark for hausdorff_wavg() in psa module.""" + psa.hausdorff_wavg(P=self.P, Q=self.Q) def time_hausdorff_avg(self, time_steps, n_atoms): - """Benchmark for hausdorff_avg() in psa module. - """ - psa.hausdorff_avg(P=self.P, - Q=self.Q) - + """Benchmark for hausdorff_avg() in psa module.""" + psa.hausdorff_avg(P=self.P, Q=self.Q) def time_hausdorff_neighbors(self, time_steps, n_atoms): - """Benchmark for hausdorff_neighbors() in psa module. - """ - psa.hausdorff_neighbors(P=self.P, - Q=self.Q) + """Benchmark for hausdorff_neighbors() in psa module.""" + psa.hausdorff_neighbors(P=self.P, Q=self.Q) def time_discrete_frechet(self, time_steps, n_atoms): - """Benchmark for discrete_frechet() in psa module. - """ - psa.discrete_frechet(P=self.P, - Q=self.Q) + """Benchmark for discrete_frechet() in psa module.""" + psa.discrete_frechet(P=self.P, Q=self.Q) diff --git a/benchmarks/benchmarks/analysis/rdf.py b/benchmarks/benchmarks/analysis/rdf.py index 507b112df89..7a1a4fa41f9 100644 --- a/benchmarks/benchmarks/analysis/rdf.py +++ b/benchmarks/benchmarks/analysis/rdf.py @@ -10,21 +10,17 @@ except: pass + class SimpleRdfBench(object): - """Benchmarks for MDAnalysis.analysis.rdf - """ + """Benchmarks for MDAnalysis.analysis.rdf""" - params = ([20,75,200], - [[0,5], [0,15], [0,20]], - [1, 100, 1000, 10000]) + params = ([20, 75, 200], [[0, 5], [0, 15], [0, 20]], [1, 100, 1000, 10000]) - param_names = ['nbins', - 'range_val', - 'natoms'] + param_names = ["nbins", "range_val", "natoms"] def setup(self, nbins, range_val, natoms): - - self.sel_str = 'name OW' + + self.sel_str = "name OW" self.u = MDAnalysis.Universe(TPR, XTC) @@ -36,10 +32,7 @@ def setup(self, nbins, range_val, natoms): # do not include initialization of the # InterRDF object in the benchmark itself - self.rdf = InterRDF(g1=self.sel, - g2=self.sel, - nbins=nbins, - range=range_val) + self.rdf = InterRDF(g1=self.sel, g2=self.sel, nbins=nbins, range=range_val) def time_interrdf(self, nbins, range_val, natoms): """Benchmark a full trajectory parse diff --git a/benchmarks/benchmarks/analysis/rms.py b/benchmarks/benchmarks/analysis/rms.py index bd1040cee04..dc99c337c22 100644 --- a/benchmarks/benchmarks/analysis/rms.py +++ b/benchmarks/benchmarks/analysis/rms.py @@ -1,7 +1,7 @@ import MDAnalysis try: - from MDAnalysisTests.datafiles import PSF, DCD + from MDAnalysisTests.datafiles import DCD, PSF except: pass @@ -10,18 +10,12 @@ except: pass + class SimpleRmsBench(object): - """Benchmarks for MDAnalysis.analysis.rms.rmsd - """ - - params = ([100, 500, 2000], - [None, [1.0, 0.5]], - [False, True], - [False, True]) - param_names = ['num_atoms', - 'weights', - 'center', - 'superposition'] + """Benchmarks for MDAnalysis.analysis.rms.rmsd""" + + params = ([100, 500, 2000], [None, [1.0, 0.5]], [False, True], [False, True]) + param_names = ["num_atoms", "weights", "center", "superposition"] def setup(self, num_atoms, weights, center, superposition): # mimic rmsd docstring example code @@ -43,33 +37,32 @@ def time_rmsd(self, num_atoms, weights, center, superposition): its docstring example code along with several possible permutations of parameters. """ - rms.rmsd(a=self.A, - b=self.B, - weights=weights, - center=center, - superposition=superposition) + rms.rmsd( + a=self.A, + b=self.B, + weights=weights, + center=center, + superposition=superposition, + ) + class RmsdTrajBench(object): - """Benchmarks for MDAnalysis.analysis.rms.RMSD - """ + """Benchmarks for MDAnalysis.analysis.rms.RMSD""" # TODO: RMSD has many parameters / options, # some of which are apparently still considered # experimental -- we'll eventually want to # benchmark more of these - params = (['all', 'backbone'], - [None, 'mass']) + params = (["all", "backbone"], [None, "mass"]) - param_names = ['select', - 'weights'] + param_names = ["select", "weights"] def setup(self, select, weights): self.u = MDAnalysis.Universe(PSF, DCD) - self.RMSD_inst = rms.RMSD(atomgroup=self.u, - reference=None, - select=select, - weights=weights) + self.RMSD_inst = rms.RMSD( + atomgroup=self.u, reference=None, select=select, weights=weights + ) def time_RMSD(self, select, weights): """Benchmark RMSD.run() method, which parses @@ -79,22 +72,16 @@ def time_RMSD(self, select, weights): class RmsfTrajBench(object): - """Benchmarks for MDAnalysis.analysis.rms.RMSF - """ + """Benchmarks for MDAnalysis.analysis.rms.RMSF""" - params = ([100,500,2000], - [None, 3], - [None,'mass']) + params = ([100, 500, 2000], [None, 3], [None, "mass"]) - param_names = ['n_atoms', - 'step', - 'weights'] + param_names = ["n_atoms", "step", "weights"] def setup(self, n_atoms, step, weights): self.u = MDAnalysis.Universe(PSF, DCD) self.ag = self.u.atoms[:n_atoms] - self.RMSF_inst = rms.RMSF(atomgroup=self.ag, - weights=weights) + self.RMSF_inst = rms.RMSF(atomgroup=self.ag, weights=weights) def time_RMSF(self, n_atoms, step, weights): """Benchmark RMSF.run() method, which parses diff --git a/benchmarks/benchmarks/import.py b/benchmarks/benchmarks/import.py index d1ca3cb43bd..387010e21dc 100644 --- a/benchmarks/benchmarks/import.py +++ b/benchmarks/benchmarks/import.py @@ -1,6 +1,6 @@ class ImportBench(object): def time_import(self): - """Benchmark time needed to import MDAnalysis - """ + """Benchmark time needed to import MDAnalysis""" import MDAnalysis as mda + pass diff --git a/benchmarks/benchmarks/selections.py b/benchmarks/benchmarks/selections.py index e1524ad378d..18d204f6bf6 100644 --- a/benchmarks/benchmarks/selections.py +++ b/benchmarks/benchmarks/selections.py @@ -5,21 +5,25 @@ except: pass + class SimpleSelectionBench(object): """Benchmarks for the various MDAnalysis simple selection strings. """ - params = ('protein', - 'backbone', - 'nucleic', - 'nucleicbackbone', - 'resid 1:10', - 'resnum 1:10', - 'resname LYS', - 'name CA', - 'bynum 0:10') - param_names = ['selection_string'] + params = ( + "protein", + "backbone", + "nucleic", + "nucleicbackbone", + "resid 1:10", + "resnum 1:10", + "resname LYS", + "name CA", + "bynum 0:10", + ) + + param_names = ["selection_string"] def setup(self, selection_string): self.u = MDAnalysis.Universe(GRO) @@ -28,11 +32,12 @@ def time_simple_selections(self, selection_string): """Benchmark simple selections on the protein-based standard test GRO file. """ - if hasattr(MDAnalysis.Universe, 'select_atoms'): + if hasattr(MDAnalysis.Universe, "select_atoms"): self.u.select_atoms(selection_string) else: self.u.selectAtoms(selection_string) + class GeoSelectionBench(object): """Benchmarks for the various MDAnalysis geometric selection strings. @@ -41,40 +46,37 @@ class GeoSelectionBench(object): # all selection strings verified # to produce non-zero atom groups # with GRO test file - params = (['around 5.0 resid 1', - 'sphlayer 2.4 6.0 (protein)', - 'sphzone 6.0 (protein)', - 'cylayer 5 10 10 -8 protein', - 'cyzone 15 4 -8 protein', - 'point 5.0 5.0 5.0 3.5', - 'prop z >= 5.0', - 'prop abs z <= 5.0'], - [True, False], # updating flags - [[False, True], [True, False]]) # periodic flags - + params = ( + [ + "around 5.0 resid 1", + "sphlayer 2.4 6.0 (protein)", + "sphzone 6.0 (protein)", + "cylayer 5 10 10 -8 protein", + "cyzone 15 4 -8 protein", + "point 5.0 5.0 5.0 3.5", + "prop z >= 5.0", + "prop abs z <= 5.0", + ], + [True, False], # updating flags + [[False, True], [True, False]], + ) # periodic flags # benchmarks should include static & # dynamic selections & periodic # vs non-periodic - param_names = ['selection_string', - 'dynamic_selection', - 'periodic_selection'] - + param_names = ["selection_string", "dynamic_selection", "periodic_selection"] - def setup(self, - selection_string, - dynamic_selection, - periodic_selection): + def setup(self, selection_string, dynamic_selection, periodic_selection): self.u = MDAnalysis.Universe(GRO) - def time_geometric_selections(self, - selection_string, - dynamic_selection, - periodic_selection): + def time_geometric_selections( + self, selection_string, dynamic_selection, periodic_selection + ): # TODO: Do we need a kwarg similar to old `use_KDTree_routines` # flag? We used to benchmark that. - self.u.select_atoms(selection_string, - updating=dynamic_selection, - periodic=periodic_selection[0], - ) + self.u.select_atoms( + selection_string, + updating=dynamic_selection, + periodic=periodic_selection[0], + ) diff --git a/benchmarks/benchmarks/topology.py b/benchmarks/benchmarks/topology.py index 45c3fcf95bf..661eeab9585 100644 --- a/benchmarks/benchmarks/topology.py +++ b/benchmarks/benchmarks/topology.py @@ -3,32 +3,30 @@ from MDAnalysis.guesser import DefaultGuesser try: - from MDAnalysisTests.datafiles import GRO from MDAnalysis.exceptions import NoDataError + from MDAnalysisTests.datafiles import GRO except: pass + class TopologyGuessBench(object): """Benchmarks for individual topology functions """ + params = (10, 100, 1000, 10000) - param_names = ['num_atoms'] - + param_names = ["num_atoms"] + def setup(self, num_atoms): self.u = MDAnalysis.Universe(GRO) self.ag = self.u.atoms[:num_atoms] - self.vdwradii = {'H':1.0, - 'C':1.0, - 'N':1.0, - 'O':1.0, - 'DUMMY':1.0} + self.vdwradii = {"H": 1.0, "C": 1.0, "N": 1.0, "O": 1.0, "DUMMY": 1.0} def time_guessbonds(self, num_atoms): """Benchmark for guessing bonds""" - DefaultGuesser(None).guess_bonds(self.ag, self.ag.positions, - box=self.ag.dimensions, - vdwradii=self.vdwradii) + DefaultGuesser(None).guess_bonds( + self.ag, self.ag.positions, box=self.ag.dimensions, vdwradii=self.vdwradii + ) class BondsBench(object): @@ -37,11 +35,11 @@ class BondsBench(object): """ params = (1000, 10000, 100000, 1000000) - param_names = ['num_bonds'] + param_names = ["num_bonds"] def setup(self, num_bonds): - self.u = MDAnalysis.Universe.empty(2*num_bonds) - bonds = np.arange(2*num_bonds).reshape(num_bonds, 2) + self.u = MDAnalysis.Universe.empty(2 * num_bonds) + bonds = np.arange(2 * num_bonds).reshape(num_bonds, 2) self.u.add_bonds(bonds) def time_bonds(self, num_bonds): diff --git a/benchmarks/benchmarks/traj_reader.py b/benchmarks/benchmarks/traj_reader.py index b2fce682c60..f30e4156f2b 100644 --- a/benchmarks/benchmarks/traj_reader.py +++ b/benchmarks/benchmarks/traj_reader.py @@ -1,4 +1,3 @@ - try: from MDAnalysis.coordinates.DCD import DCDReader from MDAnalysisTests.datafiles import DCD @@ -23,15 +22,19 @@ except ImportError: pass -traj_dict = {'XTC': [XTC, XTCReader], - 'TRR': [TRR, TRRReader], - 'DCD': [DCD, DCDReader], - 'NCDF': [NCDF, NCDFReader]} +traj_dict = { + "XTC": [XTC, XTCReader], + "TRR": [TRR, TRRReader], + "DCD": [DCD, DCDReader], + "NCDF": [NCDF, NCDFReader], +} + class TrajReaderCreation(object): """Benchmarks for trajectory file format reading.""" - params = (['XTC', 'TRR', 'DCD', 'NCDF']) - param_names = ['traj_format'] + + params = ["XTC", "TRR", "DCD", "NCDF"] + param_names = ["traj_format"] def setup(self, traj_format): self.traj_dict = traj_dict @@ -46,8 +49,9 @@ def time_reads(self, traj_format): class TrajReaderIteration(object): """Benchmarks for trajectory file format striding.""" - params = (['XTC', 'TRR', 'DCD', 'NCDF']) - param_names = ['traj_format'] + + params = ["XTC", "TRR", "DCD", "NCDF"] + param_names = ["traj_format"] def setup(self, traj_format): self.traj_dict = traj_dict diff --git a/maintainer/adapt_sitemap.py b/maintainer/adapt_sitemap.py index a731bf20872..16c45a96704 100755 --- a/maintainer/adapt_sitemap.py +++ b/maintainer/adapt_sitemap.py @@ -3,8 +3,8 @@ # # Adjust path in sitemap.xml -from xml.etree import ElementTree import argparse +from xml.etree import ElementTree # defaults for MDAnalysis, see https://github.com/MDAnalysis/mdanalysis/pull/1890 # and https://github.com/MDAnalysis/MDAnalysis.github.io/issues/78 @@ -15,39 +15,60 @@ # change if sitemaps.org updates their schema NAMESPACE = {"sitemaps": "http://www.sitemaps.org/schemas/sitemap/0.9"} + def replace_loc(tree, search, replace, namespace=NAMESPACE): root = tree.getroot() urls = root.findall("sitemaps:url", namespace) if len(urls) == 0: - raise ValueError("No sitemaps:url element found: check if the namespace in the XML file " - "is still xmlns='{0[sitemaps]}'".format(namespace)) + raise ValueError( + "No sitemaps:url element found: check if the namespace in the XML file " + "is still xmlns='{0[sitemaps]}'".format(namespace) + ) for url in urls: loc = url.find("sitemaps:loc", namespace) try: loc.text = loc.text.replace(search, replace) except AttributError: - raise ValueError("No sitemaps:loc element found: check if the namespace in the XML file " - "is still xmlns='{0[sitemaps]}'".format(namespace)) + raise ValueError( + "No sitemaps:loc element found: check if the namespace in the XML file " + "is still xmlns='{0[sitemaps]}'".format(namespace) + ) return tree if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Change top level loc in sitemap.', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - parser.add_argument('sitemap', metavar="FILE", - help="path to sitemap.xml file, will be changed in place") - parser.add_argument('--output', '-o', metavar="FILE", - default="sitemap_release.xml", - help="write altered XML to output FILE") - parser.add_argument("--search", "-s", metavar="URL", - default=DEVELOP_URL, - help="search this URL in the loc elements") - parser.add_argument("--replace", "-r", metavar="URL", - default=RELEASE_URL, - help="replace the searched URL with this URL in the loc elements") - args = parser.parse_args() + parser = argparse.ArgumentParser( + description="Change top level loc in sitemap.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "sitemap", + metavar="FILE", + help="path to sitemap.xml file, will be changed in place", + ) + parser.add_argument( + "--output", + "-o", + metavar="FILE", + default="sitemap_release.xml", + help="write altered XML to output FILE", + ) + parser.add_argument( + "--search", + "-s", + metavar="URL", + default=DEVELOP_URL, + help="search this URL in the loc elements", + ) + parser.add_argument( + "--replace", + "-r", + metavar="URL", + default=RELEASE_URL, + help="replace the searched URL with this URL in the loc elements", + ) + args = parser.parse_args() with open(args.sitemap) as xmlfile: tree = ElementTree.parse(xmlfile) @@ -55,8 +76,16 @@ def replace_loc(tree, search, replace, namespace=NAMESPACE): tree = replace_loc(tree, args.search, args.replace) with open(args.output, "wb") as xmlfile: - tree.write(xmlfile, encoding="utf-8", xml_declaration=True, - default_namespace=NAMESPACE['sitemaps']) + tree.write( + xmlfile, + encoding="utf-8", + xml_declaration=True, + default_namespace=NAMESPACE["sitemaps"], + ) - print("adapt_sitemap.py: Created output file {} with change in loc:".format(args.output)) + print( + "adapt_sitemap.py: Created output file {} with change in loc:".format( + args.output + ) + ) print("adapt_sitemap.py: {0} --> {1}".format(args.search, args.replace)) diff --git a/maintainer/norm_version.py b/maintainer/norm_version.py index 6bc8748ea04..32e02cd9512 100644 --- a/maintainer/norm_version.py +++ b/maintainer/norm_version.py @@ -23,7 +23,7 @@ def norm_version(version_str: str): import argparse parser = argparse.ArgumentParser() - parser.add_argument('--file', type=str, help="file with version to parse") + parser.add_argument("--file", type=str, help="file with version to parse") args = parser.parse_args() with open(args.file) as filed: diff --git a/maintainer/update_json_stubs_sitemap.py b/maintainer/update_json_stubs_sitemap.py index f6b1436abc4..9ba36745141 100644 --- a/maintainer/update_json_stubs_sitemap.py +++ b/maintainer/update_json_stubs_sitemap.py @@ -9,43 +9,44 @@ # 3. Write a sitemap.xml file for the root directory # +import errno +import glob import json import os import shutil -import xml.etree.ElementTree as ET -import errno -import glob import textwrap -import shutil +import xml.etree.ElementTree as ET try: from urllib.request import Request, urlopen except ImportError: from urllib2 import Request, urlopen -URL = os.environ['URL'] -VERSION = os.environ['VERSION'] +URL = os.environ["URL"] +VERSION = os.environ["VERSION"] if "http" not in URL: - raise ValueError("URL should have the transfer protocol (HTTP/S). " - f"Given: $URL={URL}") + raise ValueError( + "URL should have the transfer protocol (HTTP/S). " f"Given: $URL={URL}" + ) try: int(VERSION[0]) except ValueError: - raise ValueError("$VERSION should start with a number. " - f"Given: $VERSION={VERSION}") from None + raise ValueError( + "$VERSION should start with a number. " f"Given: $VERSION={VERSION}" + ) from None def get_web_file(filename, callback, default): url = os.path.join(URL, filename) try: - page = Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + page = Request(url, headers={"User-Agent": "Mozilla/5.0"}) data = urlopen(page).read().decode() except Exception as e: print(e) try: - with open(filename, 'r') as f: + with open(filename, "r") as f: return callback(f) except IOError as e: print(e) @@ -54,58 +55,62 @@ def get_web_file(filename, callback, default): return callback(data) -def write_redirect(file, version='', outfile=None): +def write_redirect(file, version="", outfile=None): if outfile is None: outfile = file url = os.path.join(URL, version, file) - REDIRECT = textwrap.dedent(f""" + REDIRECT = textwrap.dedent( + f""" Redirecting to {url} - """) - with open(outfile, 'w') as f: + """ + ) + with open(outfile, "w") as f: f.write(REDIRECT) print(f"Wrote redirect from {url} to {outfile}") # ========= WRITE JSON ========= # Update $root/versions.json with links to the right version -versions = get_web_file('versions.json', json.loads, []) -existing = [item['version'] for item in versions] +versions = get_web_file("versions.json", json.loads, []) +existing = [item["version"] for item in versions] already_exists = VERSION in existing -latest = 'dev' not in VERSION +latest = "dev" not in VERSION if not already_exists: if latest: for ver in versions: - ver['latest'] = False + ver["latest"] = False - versions.append({ - 'version': VERSION, - 'display': VERSION, - 'url': os.path.join(URL, VERSION), - 'latest': latest - }) + versions.append( + { + "version": VERSION, + "display": VERSION, + "url": os.path.join(URL, VERSION), + "latest": latest, + } + ) for ver in versions[::-1]: - if ver['latest']: - latest_version = ver['version'] + if ver["latest"]: + latest_version = ver["version"] break else: try: - latest_version = versions[-1]['version'] + latest_version = versions[-1]["version"] except IndexError: latest_version = None for ver in versions[::-1]: - if '-dev' in ver['version']: - dev_version = ver['version'] + if "-dev" in ver["version"]: + dev_version = ver["version"] break else: try: - dev_version = versions[-1]['version'] + dev_version = versions[-1]["version"] except IndexError: dev_version = None @@ -147,12 +152,14 @@ def add_or_update_version(version): ver["url"] = os.path.join(URL, version) break else: - versions.append({ - "version": version, - "display": version, - "url": os.path.join(URL, version), - "latest": False - }) + versions.append( + { + "version": version, + "display": version, + "url": os.path.join(URL, version), + "latest": False, + } + ) def copy_version(old_version, new_version): @@ -166,7 +173,7 @@ def copy_version(old_version, new_version): # Copy stable/ docs and write redirects from root level docs if latest: copy_version(VERSION, "stable") - html_files = glob.glob(f'stable/**/*.html', recursive=True) + html_files = glob.glob(f"stable/**/*.html", recursive=True) for file in html_files: # below should be true because we only globbed stable/* paths assert file.startswith("stable/") @@ -179,26 +186,26 @@ def copy_version(old_version, new_version): if exc.errno != errno.EEXIST: raise - write_redirect(file, '', outfile) + write_redirect(file, "", outfile) # Separate just in case we update versions.json or muck around manually # with docs if latest_version: - write_redirect('index.html', "stable") - write_redirect('index.html', latest_version, 'latest/index.html') + write_redirect("index.html", "stable") + write_redirect("index.html", latest_version, "latest/index.html") # Copy dev/ docs if dev_version and dev_version == VERSION: copy_version(VERSION, "dev") # update versions.json online -with open("versions.json", 'w') as f: +with open("versions.json", "w") as f: json.dump(versions, f, indent=2) # ========= WRITE SUPER SITEMAP.XML ========= # make one big sitemap.xml -ET.register_namespace('xhtml', "http://www.w3.org/1999/xhtml") +ET.register_namespace("xhtml", "http://www.w3.org/1999/xhtml") # so we could make 1 big sitemap as commented # below, but they must be max 50 MB / 50k URL. @@ -220,11 +227,10 @@ def copy_version(old_version, new_version): bigroot = ET.Element("sitemapindex") bigroot.set("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9") for ver in versions: - path = os.path.join(URL, '{}/sitemap.xml'.format(ver['version'])) - sitemap = ET.SubElement(bigroot, 'sitemap') - ET.SubElement(sitemap, 'loc').text = path - -ET.ElementTree(bigroot).write('sitemap_index.xml', - xml_declaration=True, - encoding='utf-8', - method="xml") + path = os.path.join(URL, "{}/sitemap.xml".format(ver["version"])) + sitemap = ET.SubElement(bigroot, "sitemap") + ET.SubElement(sitemap, "loc").text = path + +ET.ElementTree(bigroot).write( + "sitemap_index.xml", xml_declaration=True, encoding="utf-8", method="xml" +) From 29deccc9b43a09d2ec3b456bb9c70aae4c34c2cd Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 24 Dec 2024 13:54:19 +0100 Subject: [PATCH 45/58] fmt tests (#4857) --- package/MDAnalysis/tests/datafiles.py | 2 +- package/pyproject.toml | 1 + .../data/coordinates/create_data.py | 17 +- .../data/coordinates/create_h5md_data.py | 32 +- .../MDAnalysisTests/formats/test_libdcd.py | 345 +++++++++++------- .../MDAnalysisTests/formats/test_libmdaxdr.py | 219 +++++++---- .../parallelism/test_multiprocessing.py | 157 ++++---- .../parallelism/test_pickle_transformation.py | 11 +- testsuite/pyproject.toml | 4 + 9 files changed, 485 insertions(+), 303 deletions(-) diff --git a/package/MDAnalysis/tests/datafiles.py b/package/MDAnalysis/tests/datafiles.py index 30d3af12534..897b321ce7a 100644 --- a/package/MDAnalysis/tests/datafiles.py +++ b/package/MDAnalysis/tests/datafiles.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 diff --git a/package/pyproject.toml b/package/pyproject.toml index 876d2910dbd..413e4b68585 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -137,6 +137,7 @@ tables\.py | MDAnalysis/transformations/.*\.py | MDAnalysis/guesser/.*\.py | MDAnalysis/converters/.*\.py +| MDAnalysis/tests/.*\.py | MDAnalysis/selections/.*\.py ) ''' diff --git a/testsuite/MDAnalysisTests/data/coordinates/create_data.py b/testsuite/MDAnalysisTests/data/coordinates/create_data.py index 94919f6d77b..651c2569900 100644 --- a/testsuite/MDAnalysisTests/data/coordinates/create_data.py +++ b/testsuite/MDAnalysisTests/data/coordinates/create_data.py @@ -12,7 +12,7 @@ def create_test_trj(uni, fname): print(uni.trajectory.ts.__class__) with mda.Writer(fname, n_atoms) as w: for i in range(5): - uni.atoms.positions = 2 ** i * pos + uni.atoms.positions = 2**i * pos uni.trajectory.ts.time = i uni.trajectory.ts.velocities = uni.atoms.positions / 10 uni.trajectory.ts.forces = uni.atoms.positions / 100 @@ -24,14 +24,15 @@ def create_test_trj(uni, fname): def main(): - pdb = 'test_topology.pdb' + pdb = "test_topology.pdb" u = mda.Universe(pdb) - create_test_trj(u, 'test.xyz') - create_test_trj(u, 'test.xtc') - create_test_trj(u, 'test.trr') - create_test_trj(u, 'test.gro') - create_test_trj(u, 'test.dcd') + create_test_trj(u, "test.xyz") + create_test_trj(u, "test.xtc") + create_test_trj(u, "test.trr") + create_test_trj(u, "test.gro") + create_test_trj(u, "test.dcd") -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/testsuite/MDAnalysisTests/data/coordinates/create_h5md_data.py b/testsuite/MDAnalysisTests/data/coordinates/create_h5md_data.py index 090ac163717..8c73dc01adb 100644 --- a/testsuite/MDAnalysisTests/data/coordinates/create_h5md_data.py +++ b/testsuite/MDAnalysisTests/data/coordinates/create_h5md_data.py @@ -8,19 +8,24 @@ def create_test_trj(uni, fname): uni.trajectory.ts.dt = 1 orig_box = np.array([81.1, 82.2, 83.3, 75, 80, 85], dtype=np.float32) uni.trajectory.ts.dimensions = orig_box - uni.trajectory.units = {'time': 'ps', - 'length': 'Angstrom', - 'velocity': 'Angstrom/ps', - 'force': 'kJ/(mol*Angstrom)'} + uni.trajectory.units = { + "time": "ps", + "length": "Angstrom", + "velocity": "Angstrom/ps", + "force": "kJ/(mol*Angstrom)", + } print(uni.trajectory) print(uni.trajectory.ts.__class__) - with mda.Writer(fname, n_atoms, - positions=True, - velocities=True, - forces=True, - convert_units=False) as w: + with mda.Writer( + fname, + n_atoms, + positions=True, + velocities=True, + forces=True, + convert_units=False, + ) as w: for i in range(5): - uni.atoms.positions = 2 ** i * pos + uni.atoms.positions = 2**i * pos uni.trajectory.ts.time = i uni.trajectory.ts.velocities = uni.atoms.positions / 10 uni.trajectory.ts.forces = uni.atoms.positions / 100 @@ -32,10 +37,11 @@ def create_test_trj(uni, fname): def main(): - pdb = 'test_topology.pdb' + pdb = "test_topology.pdb" u = mda.Universe(pdb) - create_test_trj(u, 'test.h5md') + create_test_trj(u, "test.h5md") -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/testsuite/MDAnalysisTests/formats/test_libdcd.py b/testsuite/MDAnalysisTests/formats/test_libdcd.py index f44ae5ba1ae..8f2dd310940 100644 --- a/testsuite/MDAnalysisTests/formats/test_libdcd.py +++ b/testsuite/MDAnalysisTests/formats/test_libdcd.py @@ -27,29 +27,44 @@ import hypothesis import numpy as np -from numpy.testing import (assert_array_almost_equal, assert_equal, - assert_array_equal, assert_almost_equal) - -from MDAnalysis.lib.formats.libdcd import DCDFile, DCD_IS_CHARMM, DCD_HAS_EXTRA_BLOCK +from numpy.testing import ( + assert_array_almost_equal, + assert_equal, + assert_array_equal, + assert_almost_equal, +) + +from MDAnalysis.lib.formats.libdcd import ( + DCDFile, + DCD_IS_CHARMM, + DCD_HAS_EXTRA_BLOCK, +) from MDAnalysisTests.datafiles import ( - DCD, DCD_NAMD_TRICLINIC, legacy_DCD_ADK_coords, legacy_DCD_NAMD_coords, - legacy_DCD_c36_coords, DCD_TRICLINIC) + DCD, + DCD_NAMD_TRICLINIC, + legacy_DCD_ADK_coords, + legacy_DCD_NAMD_coords, + legacy_DCD_c36_coords, + DCD_TRICLINIC, +) import pytest -@pytest.mark.parametrize("dcdfile, is_periodic", - [(DCD, False), (DCD_NAMD_TRICLINIC, True), - (DCD_TRICLINIC, True)]) +@pytest.mark.parametrize( + "dcdfile, is_periodic", + [(DCD, False), (DCD_NAMD_TRICLINIC, True), (DCD_TRICLINIC, True)], +) def test_is_periodic(dcdfile, is_periodic): with DCDFile(dcdfile) as f: assert f.is_periodic == is_periodic -@pytest.mark.parametrize("dcdfile, natoms", [(DCD, 3341), (DCD_NAMD_TRICLINIC, - 5545), - (DCD_TRICLINIC, 375)]) +@pytest.mark.parametrize( + "dcdfile, natoms", + [(DCD, 3341), (DCD_NAMD_TRICLINIC, 5545), (DCD_TRICLINIC, 375)], +) def test_read_coordsshape(dcdfile, natoms): # confirm shape of coordinate data against result from previous # MDAnalysis implementation of DCD file handling @@ -61,10 +76,18 @@ def test_read_coordsshape(dcdfile, natoms): @pytest.mark.parametrize( "dcdfile, unit_cell", - [(DCD, [0., 90., 0., 90., 90., 0.]), - (DCD_NAMD_TRICLINIC, [38.42659378, 0.499563, 38.393102, 0., 0., 44.7598]), - (DCD_TRICLINIC, - [30.841836, 14.578635, 31.780088, 9.626323, -2.60815, 32.67009])]) + [ + (DCD, [0.0, 90.0, 0.0, 90.0, 90.0, 0.0]), + ( + DCD_NAMD_TRICLINIC, + [38.42659378, 0.499563, 38.393102, 0.0, 0.0, 44.7598], + ), + ( + DCD_TRICLINIC, + [30.841836, 14.578635, 31.780088, 9.626323, -2.60815, 32.67009], + ), + ], +) def test_read_unit_cell(dcdfile, unit_cell): # confirm unit cell read against result from previous # MDAnalysis implementation of DCD file handling @@ -161,24 +184,25 @@ def test_iteration(dcd): def test_open_wrong_mode(): with pytest.raises(IOError): - DCDFile(DCD, 'e') + DCDFile(DCD, "e") def test_raise_not_existing(): with pytest.raises(IOError): - DCDFile('foo') + DCDFile("foo") def test_zero_based_frames_counting(dcd): assert dcd.tell() == 0 -@pytest.mark.parametrize("dcdfile, natoms", [(DCD, 3341), (DCD_NAMD_TRICLINIC, - 5545), - (DCD_TRICLINIC, 375)]) +@pytest.mark.parametrize( + "dcdfile, natoms", + [(DCD, 3341), (DCD_NAMD_TRICLINIC, 5545), (DCD_TRICLINIC, 375)], +) def test_natoms(dcdfile, natoms): with DCDFile(dcdfile) as dcd: - assert dcd.header['natoms'] == natoms + assert dcd.header["natoms"] == natoms def test_read_closed(dcd): @@ -187,17 +211,18 @@ def test_read_closed(dcd): dcd.read() -@pytest.mark.parametrize("dcdfile, nframes", [(DCD, 98), (DCD_NAMD_TRICLINIC, - 1), (DCD_TRICLINIC, - 10)]) +@pytest.mark.parametrize( + "dcdfile, nframes", + [(DCD, 98), (DCD_NAMD_TRICLINIC, 1), (DCD_TRICLINIC, 10)], +) def test_length_traj(dcdfile, nframes): with DCDFile(dcdfile) as dcd: assert len(dcd) == nframes def test_read_write_mode_file(tmpdir): - fname = str(tmpdir.join('foo')) - with DCDFile(fname, 'w') as f: + fname = str(tmpdir.join("foo")) + with DCDFile(fname, "w") as f: with pytest.raises(IOError): f.read() @@ -211,25 +236,35 @@ def test_iterating_twice(dcd): assert_equal(i + 1, f.tell()) -DCD_HEADER = '''* DIMS ADK SEQUENCE FOR PORE PROGRAM * WRITTEN BY LIZ DENNING (6.2008) * DATE: 6/ 6/ 8 17:23:56 CREATED BY USER: denniej0 ''' -DCD_NAMD_TRICLINIC_HEADER = 'Created by DCD pluginREMARKS Created 06 July, 2014 at 17:29Y5~CORD,' -DCD_TRICLINIC_HEADER = '* CHARMM TRICLINIC BOX TESTING * (OLIVER BECKSTEIN 2014) * BASED ON NPTDYN.INP : SCOTT FELLER, NIH, 7/15/95 * TEST EXTENDED SYSTEM CONSTANT PRESSURE AND TEMPERATURE * DYNAMICS WITH WATER BOX. * DATE: 7/ 7/14 13:59:46 CREATED BY USER: oliver ' +DCD_HEADER = """* DIMS ADK SEQUENCE FOR PORE PROGRAM * WRITTEN BY LIZ DENNING (6.2008) * DATE: 6/ 6/ 8 17:23:56 CREATED BY USER: denniej0 """ +DCD_NAMD_TRICLINIC_HEADER = ( + "Created by DCD pluginREMARKS Created 06 July, 2014 at 17:29Y5~CORD," +) +DCD_TRICLINIC_HEADER = "* CHARMM TRICLINIC BOX TESTING * (OLIVER BECKSTEIN 2014) * BASED ON NPTDYN.INP : SCOTT FELLER, NIH, 7/15/95 * TEST EXTENDED SYSTEM CONSTANT PRESSURE AND TEMPERATURE * DYNAMICS WITH WATER BOX. * DATE: 7/ 7/14 13:59:46 CREATED BY USER: oliver " -@pytest.mark.parametrize("dcdfile, remarks", - ((DCD, DCD_HEADER), (DCD_NAMD_TRICLINIC, - DCD_NAMD_TRICLINIC_HEADER), - (DCD_TRICLINIC, DCD_TRICLINIC_HEADER))) +@pytest.mark.parametrize( + "dcdfile, remarks", + ( + (DCD, DCD_HEADER), + (DCD_NAMD_TRICLINIC, DCD_NAMD_TRICLINIC_HEADER), + (DCD_TRICLINIC, DCD_TRICLINIC_HEADER), + ), +) def test_header_remarks(dcdfile, remarks): # confirm correct header remarks section reading with DCDFile(dcdfile) as f: - assert len(f.header['remarks']) == len(remarks) + assert len(f.header["remarks"]) == len(remarks) -@pytest.mark.parametrize("dcdfile, legacy_data, frames", - ((DCD, legacy_DCD_ADK_coords, [5, 29]), - (DCD_NAMD_TRICLINIC, legacy_DCD_NAMD_coords, [0]), - (DCD_TRICLINIC, legacy_DCD_c36_coords, [1, 4]))) +@pytest.mark.parametrize( + "dcdfile, legacy_data, frames", + ( + (DCD, legacy_DCD_ADK_coords, [5, 29]), + (DCD_NAMD_TRICLINIC, legacy_DCD_NAMD_coords, [0]), + (DCD_TRICLINIC, legacy_DCD_c36_coords, [1, 4]), + ), +) def test_read_coord_values(dcdfile, legacy_data, frames): # test the actual values of coordinates read in versus # stored values read in by the legacy DCD handling framework @@ -244,10 +279,14 @@ def test_read_coord_values(dcdfile, legacy_data, frames): assert_array_equal(actual_coords, desired_coords) -@pytest.mark.parametrize("dcdfile, legacy_data, frame_idx", - ((DCD, legacy_DCD_ADK_coords, [5, 29]), - (DCD_NAMD_TRICLINIC, legacy_DCD_NAMD_coords, [0]), - (DCD_TRICLINIC, legacy_DCD_c36_coords, [1, 4]))) +@pytest.mark.parametrize( + "dcdfile, legacy_data, frame_idx", + ( + (DCD, legacy_DCD_ADK_coords, [5, 29]), + (DCD_NAMD_TRICLINIC, legacy_DCD_NAMD_coords, [0]), + (DCD_TRICLINIC, legacy_DCD_c36_coords, [1, 4]), + ), +) def test_readframes(dcdfile, legacy_data, frame_idx): legacy = np.load(legacy_data) with DCDFile(dcdfile) as dcd: @@ -261,45 +300,46 @@ def test_readframes(dcdfile, legacy_data, frame_idx): def test_write_header(tmpdir): # test that _write_header() can produce a very crude # header for a new / empty file - testfile = str(tmpdir.join('test.dcd')) - with DCDFile(testfile, 'w') as dcd: + testfile = str(tmpdir.join("test.dcd")) + with DCDFile(testfile, "w") as dcd: dcd.write_header( - remarks='Crazy!', + remarks="Crazy!", natoms=22, istart=12, nsavc=10, delta=0.02, - is_periodic=1) + is_periodic=1, + ) with DCDFile(testfile) as dcd: header = dcd.header - assert header['remarks'] == 'Crazy!' - assert header['natoms'] == 22 - assert header['istart'] == 12 - assert header['is_periodic'] == 1 - assert header['nsavc'] == 10 - assert np.allclose(header['delta'], .02) + assert header["remarks"] == "Crazy!" + assert header["natoms"] == 22 + assert header["istart"] == 12 + assert header["is_periodic"] == 1 + assert header["nsavc"] == 10 + assert np.allclose(header["delta"], 0.02) # we also check the bytes written directly. - with open(testfile, 'rb') as fh: + with open(testfile, "rb") as fh: header_bytes = fh.read() # check for magic number - assert struct.unpack('i', header_bytes[:4])[0] == 84 + assert struct.unpack("i", header_bytes[:4])[0] == 84 # magic number should be written again before remark section - assert struct.unpack('i', header_bytes[88:92])[0] == 84 + assert struct.unpack("i", header_bytes[88:92])[0] == 84 # length of remark section. We hard code this to 244 right now - assert struct.unpack('i', header_bytes[92:96])[0] == 244 + assert struct.unpack("i", header_bytes[92:96])[0] == 244 # say we have 3 block of length 80 - assert struct.unpack('i', header_bytes[96:100])[0] == 3 + assert struct.unpack("i", header_bytes[96:100])[0] == 3 # after the remark section the length should be reported again - assert struct.unpack('i', header_bytes[340:344])[0] == 244 + assert struct.unpack("i", header_bytes[340:344])[0] == 244 # this is a magic number as far as I see - assert struct.unpack('i', header_bytes[344:348])[0] == 4 + assert struct.unpack("i", header_bytes[344:348])[0] == 4 def test_write_no_header(tmpdir): - fname = str(tmpdir.join('test.dcd')) - with DCDFile(fname, 'w') as dcd: + fname = str(tmpdir.join("test.dcd")) + with DCDFile(fname, "w") as dcd: with pytest.raises(IOError): dcd.write(np.ones(3), np.ones(6)) @@ -309,16 +349,16 @@ def test_write_header_twice(tmpdir): # header writing is attempted header = { - "remarks": 'Crazy!', + "remarks": "Crazy!", "natoms": 22, "istart": 12, "nsavc": 10, "delta": 0.02, - "is_periodic": 1 + "is_periodic": 1, } - fname = str(tmpdir.join('test.dcd')) - with DCDFile(fname, 'w') as dcd: + fname = str(tmpdir.join("test.dcd")) + with DCDFile(fname, "w") as dcd: dcd.write_header(**header) with pytest.raises(IOError): dcd.write_header(**header) @@ -329,12 +369,13 @@ def test_write_header_wrong_mode(dcd): # write_header with a DCDFile object in 'r' mode with pytest.raises(IOError): dcd.write_header( - remarks='Crazy!', + remarks="Crazy!", natoms=22, istart=12, nsavc=10, delta=0.02, - is_periodic=1) + is_periodic=1, + ) def test_write_mode(dcd): @@ -344,8 +385,8 @@ def test_write_mode(dcd): dcd.write(xyz=np.zeros((3, 3)), box=np.zeros(6, dtype=np.float64)) -def write_dcd(in_name, out_name, remarks='testing', header=None): - with DCDFile(in_name) as f_in, DCDFile(out_name, 'w') as f_out: +def write_dcd(in_name, out_name, remarks="testing", header=None): + with DCDFile(in_name) as f_in, DCDFile(out_name, "w") as f_out: if header is None: header = f_in.header f_out.write_header(**header) @@ -353,40 +394,43 @@ def write_dcd(in_name, out_name, remarks='testing', header=None): f_out.write(xyz=frame.xyz, box=frame.unitcell) -@pytest.mark.xfail((os.name == 'nt' - and sys.maxsize <= 2**32) or - platform.machine() == 'aarch64', - reason="occasional fail on 32-bit windows and ARM") +@pytest.mark.xfail( + (os.name == "nt" and sys.maxsize <= 2**32) + or platform.machine() == "aarch64", + reason="occasional fail on 32-bit windows and ARM", +) # occasionally fails due to unreliable test timings @hypothesis.settings(deadline=None) # see Issue 3096 -@given(remarks=strategies.text( - alphabet=string.printable, min_size=0, - max_size=239)) # handle the printable ASCII strings -@example(remarks='') +@given( + remarks=strategies.text( + alphabet=string.printable, min_size=0, max_size=239 + ) +) # handle the printable ASCII strings +@example(remarks="") def test_written_remarks_property(remarks, tmpdir_factory): # property based testing for writing of a wide range of string # values to REMARKS field dcd = DCDFile(DCD) dirname = str(id(remarks)) + "_" - testfile = str(tmpdir_factory.mktemp(dirname).join('test.dcd')) + testfile = str(tmpdir_factory.mktemp(dirname).join("test.dcd")) header = dcd.header - header['remarks'] = remarks + header["remarks"] = remarks write_dcd(DCD, testfile, header=header) expected_remarks = remarks with DCDFile(testfile) as f: - assert f.header['remarks'] == expected_remarks + assert f.header["remarks"] == expected_remarks -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def written_dcd(tmpdir_factory): with DCDFile(DCD) as dcd: header = dcd.header - testfile = tmpdir_factory.mktemp('dcd').join('test.dcd') + testfile = tmpdir_factory.mktemp("dcd").join("test.dcd") testfile = str(testfile) write_dcd(DCD, testfile) Result = namedtuple("Result", "testfile, header, orgfile") # throw away last char we didn't save due to null termination - header['remarks'] = header['remarks'][:-1] + header["remarks"] = header["remarks"][:-1] return Result(testfile, header, DCD) @@ -398,14 +442,18 @@ def test_written_header(written_dcd): def test_written_num_frames(written_dcd): - with DCDFile(written_dcd.testfile) as dcd, DCDFile( - written_dcd.orgfile) as other: + with ( + DCDFile(written_dcd.testfile) as dcd, + DCDFile(written_dcd.orgfile) as other, + ): assert len(dcd) == len(other) def test_written_dcd_coordinate_data_shape(written_dcd): - with DCDFile(written_dcd.testfile) as dcd, DCDFile( - written_dcd.orgfile) as other: + with ( + DCDFile(written_dcd.testfile) as dcd, + DCDFile(written_dcd.orgfile) as other, + ): for frame, other_frame in zip(dcd, other): assert frame.xyz.shape == other_frame.xyz.shape @@ -418,84 +466,93 @@ def test_written_seek(written_dcd): def test_written_coord_match(written_dcd): - with DCDFile(written_dcd.testfile) as test, DCDFile( - written_dcd.orgfile) as ref: + with ( + DCDFile(written_dcd.testfile) as test, + DCDFile(written_dcd.orgfile) as ref, + ): for frame, o_frame in zip(test, ref): assert_array_almost_equal(frame.xyz, o_frame.xyz) def test_written_unit_cell(written_dcd): - with DCDFile(written_dcd.testfile) as test, DCDFile( - written_dcd.orgfile) as ref: + with ( + DCDFile(written_dcd.testfile) as test, + DCDFile(written_dcd.orgfile) as ref, + ): for frame, o_frame in zip(test, ref): assert_array_almost_equal(frame.unitcell, o_frame.unitcell) -@pytest.mark.parametrize("dtype", (np.int32, np.int64, np.float32, np.float64, - int, float)) +@pytest.mark.parametrize( + "dtype", (np.int32, np.int64, np.float32, np.float64, int, float) +) def test_write_all_dtypes(tmpdir, dtype): - fname = str(tmpdir.join('foo.dcd')) - with DCDFile(fname, 'w') as out: + fname = str(tmpdir.join("foo.dcd")) + with DCDFile(fname, "w") as out: natoms = 10 xyz = np.ones((natoms, 3), dtype=dtype) box = np.ones(6, dtype=dtype) out.write_header( - remarks='test', + remarks="test", natoms=natoms, is_periodic=1, delta=1, nsavc=1, - istart=1) + istart=1, + ) out.write(xyz=xyz, box=box) @pytest.mark.parametrize("array_like", (np.array, list)) def test_write_array_like(tmpdir, array_like): - fname = str(tmpdir.join('foo.dcd')) - with DCDFile(fname, 'w') as out: + fname = str(tmpdir.join("foo.dcd")) + with DCDFile(fname, "w") as out: natoms = 10 xyz = array_like([[1, 1, 1] for i in range(natoms)]) box = array_like([i for i in range(6)]) out.write_header( - remarks='test', + remarks="test", natoms=natoms, is_periodic=1, delta=1, nsavc=1, - istart=1) + istart=1, + ) out.write(xyz=xyz, box=box) def test_write_wrong_shape_xyz(tmpdir): - fname = str(tmpdir.join('foo.dcd')) - with DCDFile(fname, 'w') as out: + fname = str(tmpdir.join("foo.dcd")) + with DCDFile(fname, "w") as out: natoms = 10 xyz = np.ones((natoms + 1, 3)) box = np.ones(6) out.write_header( - remarks='test', + remarks="test", natoms=natoms, is_periodic=1, delta=1, nsavc=1, - istart=1) + istart=1, + ) with pytest.raises(ValueError): out.write(xyz=xyz, box=box) def test_write_wrong_shape_box(tmpdir): - fname = str(tmpdir.join('foo.dcd')) - with DCDFile(fname, 'w') as out: + fname = str(tmpdir.join("foo.dcd")) + with DCDFile(fname, "w") as out: natoms = 10 xyz = np.ones((natoms, 3)) box = np.ones(7) out.write_header( - remarks='test', + remarks="test", natoms=natoms, is_periodic=1, delta=1, nsavc=1, - istart=1) + istart=1, + ) with pytest.raises(ValueError): out.write(xyz=xyz, box=box) @@ -520,8 +577,11 @@ def test_file_size_breakdown(dcdfile): expected = os.path.getsize(dcdfile) with DCDFile(dcdfile) as dcd: - actual = dcd._header_size + dcd._firstframesize + ( - (dcd.n_frames - 1) * dcd._framesize) + actual = ( + dcd._header_size + + dcd._firstframesize + + ((dcd.n_frames - 1) * dcd._framesize) + ) assert actual == expected @@ -538,12 +598,28 @@ def test_nframessize_int(dcdfile): @pytest.mark.parametrize( "slice, length", - [([None, None, None], 98), ([0, None, None], 98), ([None, 98, None], 98), - ([None, None, 1], 98), ([None, None, -1], 98), ([2, 6, 2], 2), - ([0, 10, None], 10), ([2, 10, None], 8), ([0, 1, 1], 1), ([1, 1, 1], 0), - ([1, 2, 1], 1), ([1, 2, 2], 1), ([1, 4, 2], 2), ([1, 4, 4], 1), ([ - 0, 5, 5 - ], 1), ([3, 5, 1], 2), ([4, 0, -1], 4), ([5, 0, -2], 3), ([5, 0, -4], 2)]) + [ + ([None, None, None], 98), + ([0, None, None], 98), + ([None, 98, None], 98), + ([None, None, 1], 98), + ([None, None, -1], 98), + ([2, 6, 2], 2), + ([0, 10, None], 10), + ([2, 10, None], 8), + ([0, 1, 1], 1), + ([1, 1, 1], 0), + ([1, 2, 1], 1), + ([1, 2, 2], 1), + ([1, 4, 2], 2), + ([1, 4, 4], 1), + ([0, 5, 5], 1), + ([3, 5, 1], 2), + ([4, 0, -1], 4), + ([5, 0, -2], 3), + ([5, 0, -4], 2), + ], +) def test_readframes_slices(slice, length, dcd): start, stop, step = slice allframes = dcd.readframes().xyz @@ -553,36 +629,41 @@ def test_readframes_slices(slice, length, dcd): assert_array_almost_equal(xyz, allframes[start:stop:step]) -@pytest.mark.parametrize("order, shape", ( - ('fac', (98, 3341, 3)), - ('fca', (98, 3, 3341)), - ('afc', (3341, 98, 3)), - ('acf', (3341, 3, 98)), - ('caf', (3, 3341, 98)), - ('cfa', (3, 98, 3341)), )) +@pytest.mark.parametrize( + "order, shape", + ( + ("fac", (98, 3341, 3)), + ("fca", (98, 3, 3341)), + ("afc", (3341, 98, 3)), + ("acf", (3341, 3, 98)), + ("caf", (3, 3341, 98)), + ("cfa", (3, 98, 3341)), + ), +) def test_readframes_order(order, shape, dcd): x = dcd.readframes(order=order).xyz assert x.shape == shape -@pytest.mark.parametrize("indices", [[1, 2, 3, 4], [5, 10, 15, 19], - [9, 4, 2, 0, 50]]) +@pytest.mark.parametrize( + "indices", [[1, 2, 3, 4], [5, 10, 15, 19], [9, 4, 2, 0, 50]] +) def test_readframes_atomindices(indices, dcd): - allframes = dcd.readframes(order='afc').xyz - frames = dcd.readframes(indices=indices, order='afc') + allframes = dcd.readframes(order="afc").xyz + frames = dcd.readframes(indices=indices, order="afc") xyz = frames.xyz assert len(xyz) == len(indices) assert_array_almost_equal(xyz, allframes[indices]) def test_write_random_unitcell(tmpdir): - testname = str(tmpdir.join('test.dcd')) + testname = str(tmpdir.join("test.dcd")) rstate = np.random.RandomState(1178083) random_unitcells = rstate.uniform(high=80, size=(98, 6)).astype(np.float64) - with DCDFile(DCD) as f_in, DCDFile(testname, 'w') as f_out: + with DCDFile(DCD) as f_in, DCDFile(testname, "w") as f_out: header = f_in.header - header['is_periodic'] = True + header["is_periodic"] = True f_out.write_header(**header) for index, frame in enumerate(f_in): f_out.write(xyz=frame.xyz, box=random_unitcells[index]) diff --git a/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py b/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py index cd6a73a28e7..64748e3088e 100644 --- a/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py +++ b/testsuite/MDAnalysisTests/formats/test_libmdaxdr.py @@ -24,8 +24,12 @@ import numpy as np -from numpy.testing import (assert_almost_equal, assert_array_almost_equal, - assert_array_equal, assert_equal) +from numpy.testing import ( + assert_almost_equal, + assert_array_almost_equal, + assert_array_equal, + assert_equal, +) from MDAnalysis.lib.formats.libmdaxdr import TRRFile, XTCFile @@ -49,9 +53,11 @@ def trr(): yield f -@pytest.mark.parametrize('fname, xdr', ((XTC_multi_frame, XTCFile), - (TRR_multi_frame, TRRFile)), - indirect=True) +@pytest.mark.parametrize( + "fname, xdr", + ((XTC_multi_frame, XTCFile), (TRR_multi_frame, TRRFile)), + indirect=True, +) class TestCommonAPI(object): @staticmethod @pytest.fixture @@ -115,15 +121,15 @@ def test_len(self, reader): def test_raise_not_existing(self, xdr, fname): with pytest.raises(IOError): - xdr('foo') + xdr("foo") def test_open_wrong_mode(self, xdr, fname): with pytest.raises(IOError): - xdr('foo', 'e') + xdr("foo", "e") def test_read_write_mode_file(self, xdr, tmpdir, fname): - fname = str(tmpdir.join('foo')) - with xdr(fname, 'w') as f: + fname = str(tmpdir.join("foo")) + with xdr(fname, "w") as f: with pytest.raises(IOError): f.read() @@ -174,11 +180,14 @@ def test_pickle_immediately(self, reader): assert reader.tell() == new_reader.tell() - -@pytest.mark.parametrize("xdrfile, fname, offsets", - ((XTCFile, XTC_multi_frame, XTC_OFFSETS), - (TRRFile, TRR_multi_frame, TRR_OFFSETS)), - indirect=True) +@pytest.mark.parametrize( + "xdrfile, fname, offsets", + ( + (XTCFile, XTC_multi_frame, XTC_OFFSETS), + (TRRFile, TRR_multi_frame, TRR_OFFSETS), + ), + indirect=True, +) class TestOffsets(object): @staticmethod @pytest.fixture @@ -239,20 +248,22 @@ def test_seek_tell_largefile(self, reader, offsets): assert reader._bytes_tell() == big_offset -@pytest.mark.parametrize("xdrfile, fname", ((XTCFile, XTC_multi_frame), - (TRRFile, TRR_multi_frame))) +@pytest.mark.parametrize( + "xdrfile, fname", ((XTCFile, XTC_multi_frame), (TRRFile, TRR_multi_frame)) +) def test_steps(xdrfile, fname): with xdrfile(fname) as f: for i, frame in enumerate(f): assert frame.step == i -@pytest.mark.parametrize("xdrfile, fname", ((XTCFile, XTC_multi_frame), - (TRRFile, TRR_multi_frame))) +@pytest.mark.parametrize( + "xdrfile, fname", ((XTCFile, XTC_multi_frame), (TRRFile, TRR_multi_frame)) +) def test_time(xdrfile, fname): with xdrfile(fname) as f: for i, frame in enumerate(f): - assert frame.time == i * .5 + assert frame.time == i * 0.5 def test_box_xtc(xtc): @@ -293,13 +304,13 @@ def test_forces_trr(trr): def test_lmbda_trr(trr): for i, frame in enumerate(trr): - assert_almost_equal(frame.lmbda, .01 * i) + assert_almost_equal(frame.lmbda, 0.01 * i) @pytest.fixture def written_xtc(tmpdir, xtc): fname = str(tmpdir.join("foo.xtc")) - with XTCFile(fname, 'w') as f: + with XTCFile(fname, "w") as f: for frame in xtc: f.write(*frame) with XTCFile(fname) as f: @@ -313,7 +324,7 @@ def test_written_step_xtc(written_xtc): def test_written_time_xtc(written_xtc): for i, frame in enumerate(written_xtc): - assert frame.time == i * .5 + assert frame.time == i * 0.5 def test_written_prec_xtc(written_xtc): @@ -334,10 +345,11 @@ def test_written_xyx_xtc(written_xtc): @pytest.mark.parametrize( - 'dtype', (np.int32, np.int64, np.float32, np.float64, int, float)) + "dtype", (np.int32, np.int64, np.float32, np.float64, int, float) +) def test_write_xtc_dtype(tmpdir, dtype, xtc): fname = str(tmpdir.join("foo.xtc")) - with XTCFile(fname, 'w') as f: + with XTCFile(fname, "w") as f: for frame in xtc: x = frame.x.astype(dtype) box = frame.box.astype(dtype) @@ -346,13 +358,14 @@ def test_write_xtc_dtype(tmpdir, dtype, xtc): box=box, step=frame.step, time=frame.time, - precision=frame.prec) + precision=frame.prec, + ) -@pytest.mark.parametrize('array_like', (np.array, list)) +@pytest.mark.parametrize("array_like", (np.array, list)) def test_write_xtc_array_like(tmpdir, array_like, xtc): fname = str(tmpdir.join("foo.xtc")) - with XTCFile(fname, 'w') as f: + with XTCFile(fname, "w") as f: for frame in xtc: x = array_like(frame.x) box = array_like(frame.box) @@ -361,12 +374,13 @@ def test_write_xtc_array_like(tmpdir, array_like, xtc): box=box, step=frame.step, time=frame.time, - precision=frame.prec) + precision=frame.prec, + ) def test_write_prec(tmpdir, xtc): - outname = str(tmpdir.join('foo.xtc')) - with XTCFile(outname, 'w') as f_out: + outname = str(tmpdir.join("foo.xtc")) + with XTCFile(outname, "w") as f_out: assert f_out.n_atoms == 0 frame = xtc.read() f_out.write(frame.x, frame.box, frame.step, frame.time, 100.0) @@ -377,11 +391,10 @@ def test_write_prec(tmpdir, xtc): def test_different_box_xtc(tmpdir, xtc): - """test if we can write different box-sizes for different frames. - """ + """test if we can write different box-sizes for different frames.""" orig_box = None - fname = str(tmpdir.join('foo.xtc')) - with XTCFile(fname, 'w') as f_out: + fname = str(tmpdir.join("foo.xtc")) + with XTCFile(fname, "w") as f_out: assert f_out.n_atoms == 0 frame = xtc.read() f_out.write(frame.x, frame.box, frame.step, frame.time, frame.prec) @@ -398,8 +411,8 @@ def test_different_box_xtc(tmpdir, xtc): def test_write_different_x_xtc(tmpdir, xtc): - outname = str(tmpdir.join('foo.xtc')) - with XTCFile(outname, 'w') as f_out: + outname = str(tmpdir.join("foo.xtc")) + with XTCFile(outname, "w") as f_out: assert f_out.n_atoms == 0 frame = xtc.read() f_out.write(frame.x, frame.box, frame.step, frame.time, frame.prec) @@ -409,8 +422,8 @@ def test_write_different_x_xtc(tmpdir, xtc): def test_write_different_prec(tmpdir, xtc): - outname = str(tmpdir.join('foo.xtc')) - with XTCFile(outname, 'w') as f_out: + outname = str(tmpdir.join("foo.xtc")) + with XTCFile(outname, "w") as f_out: assert f_out.n_atoms == 0 frame = xtc.read() f_out.write(frame.x, frame.box, frame.step, frame.time, frame.prec) @@ -421,11 +434,19 @@ def test_write_different_prec(tmpdir, xtc): @pytest.fixture def written_trr(tmpdir, trr): fname = str(tmpdir.join("foo.trr")) - with TRRFile(fname, 'w') as f: + with TRRFile(fname, "w") as f: for frame in trr: natoms = frame.x.shape[0] - f.write(frame.x, frame.v, frame.f, frame.box, frame.step, - frame.time, frame.lmbda, natoms) + f.write( + frame.x, + frame.v, + frame.f, + frame.box, + frame.step, + frame.time, + frame.lmbda, + natoms, + ) with TRRFile(fname) as f: yield f @@ -437,7 +458,7 @@ def test_written_step_trr(written_trr): def test_written_time_trr(written_trr): for i, frame in enumerate(written_trr): - assert frame.time == i * .5 + assert frame.time == i * 0.5 def test_written_box_trr(written_trr): @@ -465,49 +486,67 @@ def test_written_forces_trr(written_trr): @pytest.mark.parametrize( - 'dtype', (np.int32, np.int64, np.float32, np.float64, int, float)) + "dtype", (np.int32, np.int64, np.float32, np.float64, int, float) +) def test_write_trr_dtype(tmpdir, dtype, trr): fname = str(tmpdir.join("foo.trr")) - with TRRFile(fname, 'w') as fout: + with TRRFile(fname, "w") as fout: for frame in trr: natoms = frame.x.shape[0] x = frame.x.astype(dtype) v = frame.v.astype(dtype) f = frame.f.astype(dtype) box = frame.box.astype(dtype) - fout.write(x, v, f, box, frame.step, frame.time, frame.lmbda, - natoms) + fout.write( + x, v, f, box, frame.step, frame.time, frame.lmbda, natoms + ) -@pytest.mark.parametrize('array_like', (np.array, list)) +@pytest.mark.parametrize("array_like", (np.array, list)) def test_write_trr_array_like(tmpdir, array_like, trr): fname = str(tmpdir.join("foo.trr")) - with TRRFile(fname, 'w') as fout: + with TRRFile(fname, "w") as fout: for frame in trr: natoms = frame.x.shape[0] x = array_like(frame.x) v = array_like(frame.v) f = array_like(frame.f) box = array_like(frame.box) - fout.write(x, v, f, box, frame.step, frame.time, frame.lmbda, - natoms) + fout.write( + x, v, f, box, frame.step, frame.time, frame.lmbda, natoms + ) def test_write_different_box_trr(tmpdir, trr): - """test if we can write different box-sizes for different frames. - """ + """test if we can write different box-sizes for different frames.""" orig_box = None - fname = str(tmpdir.join('foo.trr')) - with TRRFile(fname, 'w') as f_out: + fname = str(tmpdir.join("foo.trr")) + with TRRFile(fname, "w") as f_out: assert f_out.n_atoms == 0 frame = trr.read() natoms = frame.x.shape[0] - f_out.write(frame.x, frame.v, frame.f, frame.box, frame.step, - frame.time, frame.lmbda, natoms) + f_out.write( + frame.x, + frame.v, + frame.f, + frame.box, + frame.step, + frame.time, + frame.lmbda, + natoms, + ) orig_box = frame.box.copy() box = frame.box.copy() + 1 - f_out.write(frame.x, frame.v, frame.f, box, frame.step, frame.time, - frame.lmbda, natoms) + f_out.write( + frame.x, + frame.v, + frame.f, + box, + frame.step, + frame.time, + frame.lmbda, + natoms, + ) with TRRFile(fname) as trr: assert len(trr) == 2 @@ -519,13 +558,21 @@ def test_write_different_box_trr(tmpdir, trr): @pytest.fixture def trr_writer(tmpdir, trr): - outname = str(tmpdir.join('foo.trr')) - with TRRFile(outname, 'w') as f_out: + outname = str(tmpdir.join("foo.trr")) + with TRRFile(outname, "w") as f_out: assert f_out.n_atoms == 0 frame = trr.read() natoms = frame.x.shape[0] - f_out.write(frame.x, frame.v, frame.f, frame.box, frame.step, - frame.time, frame.lmbda, natoms) + f_out.write( + frame.x, + frame.v, + frame.f, + frame.box, + frame.step, + frame.time, + frame.lmbda, + natoms, + ) yield frame, f_out @@ -533,8 +580,16 @@ def test_write_different_natoms(trr_writer): frame, writer = trr_writer natoms = frame.x.shape[0] with pytest.raises(IOError): - writer.write(frame.x, frame.v, frame.f, frame.box, frame.step, - frame.time, frame.lmbda, natoms - 1) + writer.write( + frame.x, + frame.v, + frame.f, + frame.box, + frame.step, + frame.time, + frame.lmbda, + natoms - 1, + ) def test_write_different_x_trr(trr_writer): @@ -542,8 +597,16 @@ def test_write_different_x_trr(trr_writer): natoms = frame.x.shape[0] x = np.ones((natoms - 1, 3)) with pytest.raises(IOError): - writer.write(x, frame.v, frame.f, frame.box, frame.step, frame.time, - frame.lmbda, natoms) + writer.write( + x, + frame.v, + frame.f, + frame.box, + frame.step, + frame.time, + frame.lmbda, + natoms, + ) def test_write_different_v(trr_writer): @@ -551,8 +614,16 @@ def test_write_different_v(trr_writer): natoms = frame.x.shape[0] v = np.ones((natoms - 1, 3)) with pytest.raises(IOError): - writer.write(frame.x, v, frame.f, frame.box, frame.step, frame.time, - frame.lmbda, natoms) + writer.write( + frame.x, + v, + frame.f, + frame.box, + frame.step, + frame.time, + frame.lmbda, + natoms, + ) def test_write_different_f(trr_writer): @@ -560,5 +631,13 @@ def test_write_different_f(trr_writer): natoms = frame.x.shape[0] f = np.ones((natoms - 1, 3)) with pytest.raises(IOError): - writer.write(frame.x, frame.v, f, frame.box, frame.step, frame.time, - frame.lmbda, natoms) + writer.write( + frame.x, + frame.v, + f, + frame.box, + frame.step, + frame.time, + frame.lmbda, + natoms, + ) diff --git a/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py b/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py index 6cfc6816696..b4648d16bc7 100644 --- a/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py +++ b/testsuite/MDAnalysisTests/parallelism/test_multiprocessing.py @@ -37,7 +37,8 @@ from MDAnalysisTests.datafiles import ( CRD, - PSF, DCD, + PSF, + DCD, DMS, DLP_CONFIG, DLP_HISTORY, @@ -53,40 +54,48 @@ mol2_molecules, MMTF, NCDF, - PDB, PDB_small, PDB_multiframe, + PDB, + PDB_small, + PDB_multiframe, PDBQT_input, PQR, - TRC_PDB_VAC, TRC_TRAJ1_VAC, TRC_TRAJ2_VAC, + TRC_PDB_VAC, + TRC_TRAJ1_VAC, + TRC_TRAJ2_VAC, TRR, TRJ, TRZ, TXYZ, XTC, XPDB_small, - XYZ_mini, XYZ, XYZ_bz2, + XYZ_mini, + XYZ, + XYZ_bz2, ) -@pytest.fixture(params=[ - (PSF, DCD), - (GRO, XTC), - (PDB_multiframe,), - (XYZ,), - (XYZ_bz2,), # .bz2 - (GMS_SYMOPT,), # .gms - (GMS_ASYMOPT,), # .gz - pytest.param( - (GSD_long,), - marks=pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') - ), - (NCDF,), - (np.arange(150).reshape(5, 10, 3).astype(np.float64),), - (GRO, [GRO, GRO, GRO, GRO, GRO]), - (PDB, [PDB, PDB, PDB, PDB, PDB]), - (GRO, [XTC, XTC]), - (TRC_PDB_VAC, TRC_TRAJ1_VAC), - (TRC_PDB_VAC, [TRC_TRAJ1_VAC, TRC_TRAJ2_VAC]), -]) +@pytest.fixture( + params=[ + (PSF, DCD), + (GRO, XTC), + (PDB_multiframe,), + (XYZ,), + (XYZ_bz2,), # .bz2 + (GMS_SYMOPT,), # .gms + (GMS_ASYMOPT,), # .gz + pytest.param( + (GSD_long,), + marks=pytest.mark.skipif(not HAS_GSD, reason="gsd not installed"), + ), + (NCDF,), + (np.arange(150).reshape(5, 10, 3).astype(np.float64),), + (GRO, [GRO, GRO, GRO, GRO, GRO]), + (PDB, [PDB, PDB, PDB, PDB, PDB]), + (GRO, [XTC, XTC]), + (TRC_PDB_VAC, TRC_TRAJ1_VAC), + (TRC_PDB_VAC, [TRC_TRAJ1_VAC, TRC_TRAJ2_VAC]), + ] +) def u(request): if len(request.param) == 1: f = request.param[0] @@ -95,6 +104,7 @@ def u(request): top, trj = request.param return mda.Universe(top, trj) + @pytest.fixture(scope="function") def temp_xtc(tmp_path): fresh_xtc = tmp_path / "testing.xtc" @@ -120,12 +130,10 @@ def cog(u, ag, frame_id): def test_multiprocess_COG(u): ag = u.atoms[2:5] - ref = np.array([cog(u, ag, i) - for i in range(3)]) + ref = np.array([cog(u, ag, i) for i in range(3)]) p = multiprocessing.Pool(2) - res = np.array([p.apply(cog, args=(u, ag, i)) - for i in range(3)]) + res = np.array([p.apply(cog, args=(u, ag, i)) for i in range(3)]) p.close() assert_equal(ref, res) @@ -137,12 +145,10 @@ def getnames(u, ix): def test_universe_unpickle_in_new_process(): u = mda.Universe(GRO, XTC) - ref = [getnames(u, i) - for i in range(3)] + ref = [getnames(u, i) for i in range(3)] p = multiprocessing.Pool(2) - res = [p.apply(getnames, args=(u, i)) - for i in range(3)] + res = [p.apply(getnames, args=(u, i)) for i in range(3)] p.close() assert_equal(ref, res) @@ -160,48 +166,51 @@ def test_creating_multiple_universe_without_offset(temp_xtc, ncopies=3): universes = [p.apply_async(mda.Universe, args) for i in range(ncopies)] universes = [universe.get() for universe in universes] - - assert_equal(universes[0].trajectory._xdr.offsets, - universes[1].trajectory._xdr.offsets) - - -@pytest.fixture(params=[ - # formatname, filename - ('CRD', CRD, dict()), - ('DATA', LAMMPSdata_mini, dict(n_atoms=1)), - ('DCD', DCD, dict()), - ('DMS', DMS, dict()), - ('CONFIG', DLP_CONFIG, dict()), - ('FHIAIMS', FHIAIMS, dict()), - ('HISTORY', DLP_HISTORY, dict()), - ('INPCRD', INPCRD, dict()), - ('LAMMPSDUMP', LAMMPSDUMP, dict()), - ('GMS', GMS_ASYMOPT, dict()), - ('GRO', GRO, dict()), - pytest.param( - ('GSD', GSD, dict()), - marks=pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') - ), - ('MMTF', MMTF, dict()), - ('MOL2', mol2_molecules, dict()), - ('PDB', PDB_small, dict()), - ('PQR', PQR, dict()), - ('PDBQT', PDBQT_input, dict()), - ('TRR', TRR, dict()), - ('TRZ', TRZ, dict(n_atoms=8184)), - ('TRJ', TRJ, dict(n_atoms=252)), - ('XTC', XTC, dict()), - ('XPDB', XPDB_small, dict()), - ('XYZ', XYZ_mini, dict()), - ('NCDF', NCDF, dict()), - ('TXYZ', TXYZ, dict()), - ('memory', np.arange(60).reshape(2, 10, 3).astype(np.float64), dict()), - ('TRC', TRC_TRAJ1_VAC, dict()), - ('CHAIN', [TRC_TRAJ1_VAC, TRC_TRAJ2_VAC], dict()), - ('CHAIN', [GRO, GRO, GRO], dict()), - ('CHAIN', [PDB, PDB, PDB], dict()), - ('CHAIN', [XTC, XTC, XTC], dict()), -]) + assert_equal( + universes[0].trajectory._xdr.offsets, + universes[1].trajectory._xdr.offsets, + ) + + +@pytest.fixture( + params=[ + # formatname, filename + ("CRD", CRD, dict()), + ("DATA", LAMMPSdata_mini, dict(n_atoms=1)), + ("DCD", DCD, dict()), + ("DMS", DMS, dict()), + ("CONFIG", DLP_CONFIG, dict()), + ("FHIAIMS", FHIAIMS, dict()), + ("HISTORY", DLP_HISTORY, dict()), + ("INPCRD", INPCRD, dict()), + ("LAMMPSDUMP", LAMMPSDUMP, dict()), + ("GMS", GMS_ASYMOPT, dict()), + ("GRO", GRO, dict()), + pytest.param( + ("GSD", GSD, dict()), + marks=pytest.mark.skipif(not HAS_GSD, reason="gsd not installed"), + ), + ("MMTF", MMTF, dict()), + ("MOL2", mol2_molecules, dict()), + ("PDB", PDB_small, dict()), + ("PQR", PQR, dict()), + ("PDBQT", PDBQT_input, dict()), + ("TRR", TRR, dict()), + ("TRZ", TRZ, dict(n_atoms=8184)), + ("TRJ", TRJ, dict(n_atoms=252)), + ("XTC", XTC, dict()), + ("XPDB", XPDB_small, dict()), + ("XYZ", XYZ_mini, dict()), + ("NCDF", NCDF, dict()), + ("TXYZ", TXYZ, dict()), + ("memory", np.arange(60).reshape(2, 10, 3).astype(np.float64), dict()), + ("TRC", TRC_TRAJ1_VAC, dict()), + ("CHAIN", [TRC_TRAJ1_VAC, TRC_TRAJ2_VAC], dict()), + ("CHAIN", [GRO, GRO, GRO], dict()), + ("CHAIN", [PDB, PDB, PDB], dict()), + ("CHAIN", [XTC, XTC, XTC], dict()), + ] +) def ref_reader(request): fmt_name, filename, extras = request.param r = get_reader_for(filename, format=fmt_name)(filename, **extras) diff --git a/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py b/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py index 14911079b34..4838e35ed34 100644 --- a/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py +++ b/testsuite/MDAnalysisTests/parallelism/test_pickle_transformation.py @@ -35,9 +35,11 @@ from MDAnalysisTests.datafiles import PSF_TRICLINIC, DCD_TRICLINIC -@pytest.fixture(params=[ - (PSF_TRICLINIC, DCD_TRICLINIC), -]) +@pytest.fixture( + params=[ + (PSF_TRICLINIC, DCD_TRICLINIC), + ] +) def u(request): top, traj = request.param return mda.Universe(top, traj) @@ -96,8 +98,7 @@ def test_add_fit_translation_pickle(fit_translation_transformation, u): assert_almost_equal(u_ts.positions, u_p_ts.positions) -def test_add_fit_rot_trans_pickle(fit_rot_trans_transformation, - u): +def test_add_fit_rot_trans_pickle(fit_rot_trans_transformation, u): u.trajectory.add_transformations(fit_rot_trans_transformation) u_p = pickle.loads(pickle.dumps(u)) u.trajectory[0] diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 1636dc406d0..8e422352e11 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -164,6 +164,10 @@ setup\.py | MDAnalysisTests/transformations/.*\.py | MDAnalysisTests/guesser/.*\.py | MDAnalysisTests/converters/.*\.py +| MDAnalysisTests/data/.*\.py +| MDAnalysisTests/formats/.*\.py +| MDAnalysisTests/parallelism/.*\.py +| MDAnalysisTests/scripts/.*\.py ) ''' extend-exclude = ''' From c08cb797fd1a2cb45bc0b9e4522cabb15d1f36bd Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 24 Dec 2024 21:11:03 +0100 Subject: [PATCH 46/58] [fmt] Coordinates (#4856) --- package/MDAnalysis/coordinates/CRD.py | 137 ++- package/MDAnalysis/coordinates/DMS.py | 34 +- package/MDAnalysis/coordinates/FHIAIMS.py | 99 +- package/MDAnalysis/coordinates/GRO.py | 184 +-- package/MDAnalysis/coordinates/GSD.py | 50 +- package/MDAnalysis/coordinates/INPCRD.py | 25 +- package/MDAnalysis/coordinates/LAMMPS.py | 392 +++--- package/MDAnalysis/coordinates/MMTF.py | 21 +- package/MDAnalysis/coordinates/NAMDBIN.py | 42 +- package/MDAnalysis/coordinates/PDBQT.py | 161 ++- package/MDAnalysis/coordinates/PQR.py | 128 +- package/MDAnalysis/coordinates/ParmEd.py | 10 +- package/MDAnalysis/coordinates/TNG.py | 42 +- package/MDAnalysis/coordinates/TRC.py | 32 +- package/MDAnalysis/coordinates/core.py | 12 +- package/MDAnalysis/coordinates/null.py | 5 +- package/pyproject.toml | 2 + testsuite/MDAnalysisTests/coordinates/base.py | 697 +++++++---- .../MDAnalysisTests/coordinates/reference.py | 133 ++- .../coordinates/test_amber_inpcrd.py | 21 +- .../coordinates/test_chainreader.py | 376 +++--- .../coordinates/test_chemfiles.py | 24 +- .../coordinates/test_copying.py | 182 +-- .../MDAnalysisTests/coordinates/test_crd.py | 79 +- .../MDAnalysisTests/coordinates/test_dcd.py | 349 ++++-- .../coordinates/test_dlpoly.py | 114 +- .../MDAnalysisTests/coordinates/test_dms.py | 39 +- .../coordinates/test_fhiaims.py | 218 ++-- .../MDAnalysisTests/coordinates/test_gms.py | 38 +- .../MDAnalysisTests/coordinates/test_gro.py | 289 +++-- .../MDAnalysisTests/coordinates/test_gsd.py | 32 +- .../MDAnalysisTests/coordinates/test_h5md.py | 877 ++++++++------ .../coordinates/test_lammps.py | 723 ++++++----- .../coordinates/test_memory.py | 161 +-- .../MDAnalysisTests/coordinates/test_mmtf.py | 29 +- .../MDAnalysisTests/coordinates/test_mol2.py | 58 +- .../coordinates/test_namdbin.py | 21 +- .../coordinates/test_netcdf.py | 1053 ++++++++++------- .../MDAnalysisTests/coordinates/test_null.py | 2 +- .../MDAnalysisTests/coordinates/test_pdb.py | 855 ++++++++----- .../MDAnalysisTests/coordinates/test_pdbqt.py | 63 +- .../MDAnalysisTests/coordinates/test_pqr.py | 154 ++- .../coordinates/test_reader_api.py | 258 ++-- .../coordinates/test_timestep_api.py | 295 +++-- .../MDAnalysisTests/coordinates/test_tng.py | 48 +- .../MDAnalysisTests/coordinates/test_trc.py | 46 +- .../MDAnalysisTests/coordinates/test_trj.py | 83 +- .../MDAnalysisTests/coordinates/test_trz.py | 182 +-- .../MDAnalysisTests/coordinates/test_txyz.py | 24 +- .../coordinates/test_windows.py | 52 +- .../coordinates/test_writer_api.py | 51 +- .../coordinates/test_writer_registration.py | 14 +- .../MDAnalysisTests/coordinates/test_xdr.py | 412 ++++--- .../MDAnalysisTests/coordinates/test_xyz.py | 60 +- testsuite/pyproject.toml | 1 + 55 files changed, 5819 insertions(+), 3670 deletions(-) diff --git a/package/MDAnalysis/coordinates/CRD.py b/package/MDAnalysis/coordinates/CRD.py index c57d9dea0da..e762c83767b 100644 --- a/package/MDAnalysis/coordinates/CRD.py +++ b/package/MDAnalysis/coordinates/CRD.py @@ -45,8 +45,9 @@ class CRDReader(base.SingleFrameReaderBase): Now returns a ValueError instead of FormatError. Frames now 0-based instead of 1-based. """ - format = 'CRD' - units = {'time': None, 'length': 'Angstrom'} + + format = "CRD" + units = {"time": None, "length": "Angstrom"} def _read_first_frame(self): # EXT: @@ -62,37 +63,47 @@ def _read_first_frame(self): extended = False natoms = 0 for linenum, line in enumerate(crdfile): - if line.strip().startswith('*') or line.strip() == "": + if line.strip().startswith("*") or line.strip() == "": continue # ignore TITLE and empty lines fields = line.split() if len(fields) <= 2: # should be the natoms line natoms = int(fields[0]) - extended = (fields[-1] == 'EXT') + extended = fields[-1] == "EXT" continue # process coordinates try: if extended: - coords_list.append(np.array(line[45:100].split()[0:3], dtype=float)) + coords_list.append( + np.array(line[45:100].split()[0:3], dtype=float) + ) else: - coords_list.append(np.array(line[20:50].split()[0:3], dtype=float)) + coords_list.append( + np.array(line[20:50].split()[0:3], dtype=float) + ) except Exception: - errmsg = (f"Check CRD format at line {linenum}: " - f"{line.rstrip()}") + errmsg = ( + f"Check CRD format at line {linenum}: " + f"{line.rstrip()}" + ) raise ValueError(errmsg) from None self.n_atoms = len(coords_list) - self.ts = self._Timestep.from_coordinates(np.array(coords_list), - **self._ts_kwargs) + self.ts = self._Timestep.from_coordinates( + np.array(coords_list), **self._ts_kwargs + ) self.ts.frame = 0 # 0-based frame number # if self.convert_units: # self.convert_pos_from_native(self.ts._pos) # in-place ! # sanity check if self.n_atoms != natoms: - raise ValueError("Found %d coordinates in %r but the header claims that there " - "should be %d coordinates." % (self.n_atoms, self.filename, natoms)) + raise ValueError( + "Found %d coordinates in %r but the header claims that there " + "should be %d coordinates." + % (self.n_atoms, self.filename, natoms) + ) def Writer(self, filename, **kwargs): """Returns a CRDWriter for *filename*. @@ -132,21 +143,26 @@ class CRDWriter(base.WriterBase): Files are now written in `wt` mode, and keep extensions, allowing for files to be written under compressed formats """ - format = 'CRD' - units = {'time': None, 'length': 'Angstrom'} + + format = "CRD" + units = {"time": None, "length": "Angstrom"} fmt = { - #crdtype = 'extended' - #fortran_format = '(2I10,2X,A8,2X,A8,3F20.10,2X,A8,2X,A8,F20.10)' - "ATOM_EXT": ("{serial:10d}{totRes:10d} {resname:<8.8s} {name:<8.8s}" - "{pos[0]:20.10f}{pos[1]:20.10f}{pos[2]:20.10f} " - "{chainID:<8.8s} {resSeq:<8d}{tempfactor:20.10f}\n"), + # crdtype = 'extended' + # fortran_format = '(2I10,2X,A8,2X,A8,3F20.10,2X,A8,2X,A8,F20.10)' + "ATOM_EXT": ( + "{serial:10d}{totRes:10d} {resname:<8.8s} {name:<8.8s}" + "{pos[0]:20.10f}{pos[1]:20.10f}{pos[2]:20.10f} " + "{chainID:<8.8s} {resSeq:<8d}{tempfactor:20.10f}\n" + ), "NUMATOMS_EXT": "{0:10d} EXT\n", - #crdtype = 'standard' - #fortran_format = '(2I5,1X,A4,1X,A4,3F10.5,1X,A4,1X,A4,F10.5)' - "ATOM": ("{serial:5d}{totRes:5d} {resname:<4.4s} {name:<4.4s}" - "{pos[0]:10.5f}{pos[1]:10.5f}{pos[2]:10.5f} " - "{chainID:<4.4s} {resSeq:<4d}{tempfactor:10.5f}\n"), + # crdtype = 'standard' + # fortran_format = '(2I5,1X,A4,1X,A4,3F10.5,1X,A4,1X,A4,F10.5)' + "ATOM": ( + "{serial:5d}{totRes:5d} {resname:<4.4s} {name:<4.4s}" + "{pos[0]:10.5f}{pos[1]:10.5f}{pos[2]:10.5f} " + "{chainID:<4.4s} {resSeq:<4d}{tempfactor:10.5f}\n" + ), "TITLE": "* FRAME {frame} FROM {where}\n", "NUMATOMS": "{0:5d}\n", } @@ -168,7 +184,7 @@ def __init__(self, filename, **kwargs): .. versionadded:: 2.2.0 """ - self.filename = util.filename(filename, ext='crd', keep=True) + self.filename = util.filename(filename, ext="crd", keep=True) self.crd = None # account for explicit crd format, if requested @@ -200,21 +216,22 @@ def write(self, selection, frame=None): except AttributeError: frame = 0 # should catch cases when we are analyzing a single PDB (?) - atoms = selection.atoms # make sure to use atoms (Issue 46) - coor = atoms.positions # can write from selection == Universe (Issue 49) + coor = ( + atoms.positions + ) # can write from selection == Universe (Issue 49) n_atoms = len(atoms) # Detect which format string we're using to output (EXT or not) # *len refers to how to truncate various things, # depending on output format! if self.extended or n_atoms > 99999: - at_fmt = self.fmt['ATOM_EXT'] + at_fmt = self.fmt["ATOM_EXT"] serial_len = 10 resid_len = 8 totres_len = 10 else: - at_fmt = self.fmt['ATOM'] + at_fmt = self.fmt["ATOM"] serial_len = 5 resid_len = 4 totres_len = 5 @@ -223,11 +240,11 @@ def write(self, selection, frame=None): attrs = {} missing_topology = [] for attr, default in ( - ('resnames', itertools.cycle(('UNK',))), - # Resids *must* be an array because we index it later - ('resids', np.ones(n_atoms, dtype=int)), - ('names', itertools.cycle(('X',))), - ('tempfactors', itertools.cycle((0.0,))), + ("resnames", itertools.cycle(("UNK",))), + # Resids *must* be an array because we index it later + ("resids", np.ones(n_atoms, dtype=int)), + ("names", itertools.cycle(("X",))), + ("tempfactors", itertools.cycle((0.0,))), ): try: attrs[attr] = getattr(atoms, attr) @@ -236,40 +253,50 @@ def write(self, selection, frame=None): missing_topology.append(attr) # ChainIDs - Try ChainIDs first, fall back to Segids try: - attrs['chainIDs'] = atoms.chainIDs + attrs["chainIDs"] = atoms.chainIDs except (NoDataError, AttributeError): # try looking for segids instead try: - attrs['chainIDs'] = atoms.segids + attrs["chainIDs"] = atoms.segids except (NoDataError, AttributeError): - attrs['chainIDs'] = itertools.cycle(('',)) + attrs["chainIDs"] = itertools.cycle(("",)) missing_topology.append(attr) if missing_topology: warnings.warn( "Supplied AtomGroup was missing the following attributes: " "{miss}. These will be written with default values. " - "".format(miss=', '.join(missing_topology))) + "".format(miss=", ".join(missing_topology)) + ) - with util.openany(self.filename, 'wt') as crd: + with util.openany(self.filename, "wt") as crd: # Write Title - crd.write(self.fmt['TITLE'].format( - frame=frame, where=u.trajectory.filename)) + crd.write( + self.fmt["TITLE"].format( + frame=frame, where=u.trajectory.filename + ) + ) crd.write("*\n") # Write NUMATOMS if self.extended or n_atoms > 99999: - crd.write(self.fmt['NUMATOMS_EXT'].format(n_atoms)) + crd.write(self.fmt["NUMATOMS_EXT"].format(n_atoms)) else: - crd.write(self.fmt['NUMATOMS'].format(n_atoms)) + crd.write(self.fmt["NUMATOMS"].format(n_atoms)) # Write all atoms current_resid = 1 - resids = attrs['resids'] + resids = attrs["resids"] for i, pos, resname, name, chainID, resid, tempfactor in zip( - range(n_atoms), coor, attrs['resnames'], attrs['names'], - attrs['chainIDs'], attrs['resids'], attrs['tempfactors']): - if not i == 0 and resids[i] != resids[i-1]: + range(n_atoms), + coor, + attrs["resnames"], + attrs["names"], + attrs["chainIDs"], + attrs["resids"], + attrs["tempfactors"], + ): + if not i == 0 and resids[i] != resids[i - 1]: current_resid += 1 # Truncate numbers @@ -277,7 +304,15 @@ def write(self, selection, frame=None): resid = util.ltruncate_int(resid, resid_len) current_resid = util.ltruncate_int(current_resid, totres_len) - crd.write(at_fmt.format( - serial=serial, totRes=current_resid, resname=resname, - name=name, pos=pos, chainID=chainID, - resSeq=resid, tempfactor=tempfactor)) + crd.write( + at_fmt.format( + serial=serial, + totRes=current_resid, + resname=resname, + name=name, + pos=pos, + chainID=chainID, + resSeq=resid, + tempfactor=tempfactor, + ) + ) diff --git a/package/MDAnalysis/coordinates/DMS.py b/package/MDAnalysis/coordinates/DMS.py index 1d207ca2bd9..04367f438d3 100644 --- a/package/MDAnalysis/coordinates/DMS.py +++ b/package/MDAnalysis/coordinates/DMS.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 @@ -46,29 +46,30 @@ class DMSReader(base.SingleFrameReaderBase): .. versionchanged:: 0.11.0 Frames now 0-based instead of 1-based """ - format = 'DMS' - units = {'time': None, 'length': 'A', 'velocity': 'A/ps'} + + format = "DMS" + units = {"time": None, "length": "A", "velocity": "A/ps"} def get_coordinates(self, cur): - cur.execute('SELECT * FROM particle') + cur.execute("SELECT * FROM particle") particles = cur.fetchall() - return [(p['x'], p['y'], p['z']) for p in particles] + return [(p["x"], p["y"], p["z"]) for p in particles] def get_particle_by_columns(self, cur, columns=None): if columns is None: - columns = ['x', 'y', 'z'] - cur.execute('SELECT * FROM particle') + columns = ["x", "y", "z"] + cur.execute("SELECT * FROM particle") particles = cur.fetchall() return [tuple([p[c] for c in columns]) for p in particles] def get_global_cell(self, cur): - cur.execute('SELECT * FROM global_cell') + cur.execute("SELECT * FROM global_cell") rows = cur.fetchall() assert len(rows) == 3 x = [row["x"] for row in rows] y = [row["y"] for row in rows] z = [row["z"] for row in rows] - return {'x': x, 'y': y, 'z': z} + return {"x": x, "y": y, "z": z} def _read_first_frame(self): coords_list = None @@ -85,7 +86,9 @@ def dict_factory(cursor, row): con.row_factory = dict_factory cur = con.cursor() coords_list = self.get_coordinates(cur) - velocities_list = self.get_particle_by_columns(cur, columns=['vx', 'vy', 'vz']) + velocities_list = self.get_particle_by_columns( + cur, columns=["vx", "vy", "vz"] + ) unitcell = self.get_global_cell(cur) if not coords_list: @@ -99,15 +102,20 @@ def dict_factory(cursor, row): self.ts = self._Timestep.from_coordinates( np.array(coords_list, dtype=np.float32), velocities=velocities, - **self._ts_kwargs) + **self._ts_kwargs, + ) self.ts.frame = 0 # 0-based frame number - self.ts.dimensions = triclinic_box(unitcell['x'], unitcell['y'], unitcell['z']) + self.ts.dimensions = triclinic_box( + unitcell["x"], unitcell["y"], unitcell["z"] + ) if self.convert_units: self.convert_pos_from_native(self.ts._pos) # in-place ! if self.ts.dimensions is not None: - self.convert_pos_from_native(self.ts.dimensions[:3]) # in-place ! + self.convert_pos_from_native( + self.ts.dimensions[:3] + ) # in-place ! if self.ts.has_velocities: # converts nm/ps to A/ps units self.convert_velocities_from_native(self.ts._velocities) diff --git a/package/MDAnalysis/coordinates/FHIAIMS.py b/package/MDAnalysis/coordinates/FHIAIMS.py index 193d570560e..3aff6d97f52 100644 --- a/package/MDAnalysis/coordinates/FHIAIMS.py +++ b/package/MDAnalysis/coordinates/FHIAIMS.py @@ -119,31 +119,35 @@ class FHIAIMSReader(base.SingleFrameReaderBase): """Reader for the FHIAIMS geometry format. - Single frame reader for the `FHI-AIMS`_ input file format. Reads - geometry (3D and molecules only), positions (absolut or fractional), - velocities if given, all according to the `FHI-AIMS format`_ - specifications + Single frame reader for the `FHI-AIMS`_ input file format. Reads + geometry (3D and molecules only), positions (absolut or fractional), + velocities if given, all according to the `FHI-AIMS format`_ + specifications """ - format = ['IN', 'FHIAIMS'] - units = {'time': 'ps', 'length': 'Angstrom', 'velocity': 'Angstrom/ps'} + + format = ["IN", "FHIAIMS"] + units = {"time": "ps", "length": "Angstrom", "velocity": "Angstrom/ps"} def _read_first_frame(self): - with util.openany(self.filename, 'rt') as fhiaimsfile: + with util.openany(self.filename, "rt") as fhiaimsfile: relative, positions, velocities, lattice_vectors = [], [], [], [] skip_tags = ["#", "initial_moment"] - oldline = '' + oldline = "" for line in fhiaimsfile: line = line.strip() if line.startswith("atom"): positions.append(line.split()[1:-1]) - relative.append('atom_frac' in line) + relative.append("atom_frac" in line) oldline = line continue if line.startswith("velocity"): - if not 'atom' in oldline: + if not "atom" in oldline: raise ValueError( - 'Non-conforming line (velocity must follow atom): ({0})in FHI-AIMS input file {0}'.format(line, self.filename)) + "Non-conforming line (velocity must follow atom): ({0})in FHI-AIMS input file {0}".format( + line, self.filename + ) + ) velocities.append(line.split()[1:]) oldline = line continue @@ -155,7 +159,10 @@ def _read_first_frame(self): oldline = line continue raise ValueError( - 'Non-conforming line: ({0})in FHI-AIMS input file {0}'.format(line, self.filename)) + "Non-conforming line: ({0})in FHI-AIMS input file {0}".format( + line, self.filename + ) + ) # positions and velocities are lists of lists of strings; they will be # cast to np.arrays(..., dtype=float32) during assignment to ts.positions/ts.velocities @@ -163,14 +170,18 @@ def _read_first_frame(self): if len(velocities) not in (0, len(positions)): raise ValueError( - 'Found incorrect number of velocity tags ({0}) in the FHI-AIMS file, should be {1}.'.format( - len(velocities), len(positions))) + "Found incorrect number of velocity tags ({0}) in the FHI-AIMS file, should be {1}.".format( + len(velocities), len(positions) + ) + ) if len(lattice_vectors) not in (0, 3): raise ValueError( - 'Found partial periodicity in FHI-AIMS file. This cannot be handled by MDAnalysis.') + "Found partial periodicity in FHI-AIMS file. This cannot be handled by MDAnalysis." + ) if len(lattice_vectors) == 0 and any(relative): raise ValueError( - 'Found relative coordinates in FHI-AIMS file without lattice info.') + "Found relative coordinates in FHI-AIMS file without lattice info." + ) # create Timestep @@ -180,7 +191,9 @@ def _read_first_frame(self): if len(lattice_vectors) > 0: ts.dimensions = triclinic_box(*lattice_vectors) - ts.positions[relative] = np.matmul(ts.positions[relative], lattice_vectors) + ts.positions[relative] = np.matmul( + ts.positions[relative], lattice_vectors + ) if len(velocities) > 0: ts.velocities = velocities @@ -208,24 +221,24 @@ def Writer(self, filename, n_atoms=None, **kwargs): class FHIAIMSWriter(base.WriterBase): """FHI-AIMS Writer. - Single frame writer for the `FHI-AIMS`_ format. Writes geometry (3D and - molecules only), positions (absolut only), velocities if given, all - according to the `FHI-AIMS format`_ specifications. + Single frame writer for the `FHI-AIMS`_ format. Writes geometry (3D and + molecules only), positions (absolut only), velocities if given, all + according to the `FHI-AIMS format`_ specifications. - If no atom names are given, it will set each atom name to "X". + If no atom names are given, it will set each atom name to "X". """ - format = ['IN', 'FHIAIMS'] - units = {'time': None, 'length': 'Angstrom', 'velocity': 'Angstrom/ps'} + format = ["IN", "FHIAIMS"] + units = {"time": None, "length": "Angstrom", "velocity": "Angstrom/ps"} #: format strings for the FHI-AIMS file (all include newline) fmt = { # coordinates output format, see https://doi.org/10.6084/m9.figshare.12413477.v1 - 'xyz': "atom {pos[0]:12.8f} {pos[1]:12.8f} {pos[2]:12.8f} {name:<3s}\n", - 'vel': "velocity {vel[0]:12.8f} {vel[1]:12.8f} {vel[2]:12.8f}\n", + "xyz": "atom {pos[0]:12.8f} {pos[1]:12.8f} {pos[2]:12.8f} {name:<3s}\n", + "vel": "velocity {vel[0]:12.8f} {vel[1]:12.8f} {vel[2]:12.8f}\n", # unitcell - 'box_triclinic': "lattice_vector {box[0]:12.8f} {box[1]:12.8f} {box[2]:12.8f}\nlattice_vector {box[3]:12.8f} {box[4]:12.8f} {box[5]:12.8f}\nlattice_vector {box[6]:12.8f} {box[7]:12.8f} {box[8]:12.8f}\n" + "box_triclinic": "lattice_vector {box[0]:12.8f} {box[1]:12.8f} {box[2]:12.8f}\nlattice_vector {box[3]:12.8f} {box[4]:12.8f} {box[5]:12.8f}\nlattice_vector {box[6]:12.8f} {box[7]:12.8f} {box[8]:12.8f}\n", } def __init__(self, filename, convert_units=True, n_atoms=None, **kwargs): @@ -239,7 +252,7 @@ def __init__(self, filename, convert_units=True, n_atoms=None, **kwargs): number of atoms """ - self.filename = util.filename(filename, ext='.in', keep=True) + self.filename = util.filename(filename, ext=".in", keep=True) self.n_atoms = n_atoms def _write_next_frame(self, obj): @@ -268,20 +281,21 @@ def _write_next_frame(self, obj): try: names = ag.names except (AttributeError, NoDataError): - names = itertools.cycle(('X',)) - missing_topology.append('names') + names = itertools.cycle(("X",)) + missing_topology.append("names") try: atom_indices = ag.ids except (AttributeError, NoDataError): - atom_indices = range(1, ag.n_atoms+1) - missing_topology.append('ids') + atom_indices = range(1, ag.n_atoms + 1) + missing_topology.append("ids") if missing_topology: warnings.warn( "Supplied AtomGroup was missing the following attributes: " "{miss}. These will be written with default values. " - "".format(miss=', '.join(missing_topology))) + "".format(miss=", ".join(missing_topology)) + ) positions = ag.positions try: @@ -290,7 +304,7 @@ def _write_next_frame(self, obj): except (AttributeError, NoDataError): has_velocities = False - with util.openany(self.filename, 'wt') as output_fhiaims: + with util.openany(self.filename, "wt") as output_fhiaims: # Lattice try: # for AtomGroup/Universe tri_dims = obj.universe.trajectory.ts.triclinic_dimensions @@ -299,16 +313,19 @@ def _write_next_frame(self, obj): # full output if tri_dims is not None: output_fhiaims.write( - self.fmt['box_triclinic'].format(box=tri_dims.flatten())) + self.fmt["box_triclinic"].format(box=tri_dims.flatten()) + ) # Atom descriptions and coords # Dont use enumerate here, # all attributes could be infinite cycles! - for atom_index, name in zip( - range(ag.n_atoms), names): - output_fhiaims.write(self.fmt['xyz'].format( - pos=positions[atom_index], - name=name)) + for atom_index, name in zip(range(ag.n_atoms), names): + output_fhiaims.write( + self.fmt["xyz"].format( + pos=positions[atom_index], name=name + ) + ) if has_velocities: - output_fhiaims.write(self.fmt['vel'].format( - vel=velocities[atom_index])) + output_fhiaims.write( + self.fmt["vel"].format(vel=velocities[atom_index]) + ) diff --git a/package/MDAnalysis/coordinates/GRO.py b/package/MDAnalysis/coordinates/GRO.py index 721fbe096f9..4c17caaf5cd 100644 --- a/package/MDAnalysis/coordinates/GRO.py +++ b/package/MDAnalysis/coordinates/GRO.py @@ -137,6 +137,7 @@ _TS_ORDER_Y = [5, 1, 6] _TS_ORDER_Z = [7, 8, 2] + def _gmx_to_dimensions(box): # convert gromacs ordered box to [lx, ly, lz, alpha, beta, gamma] form x = box[_TS_ORDER_X] @@ -165,12 +166,13 @@ class GROReader(base.SingleFrameReaderBase): being without dimension information (i.e. will the timestep dimension to ``None``). """ - format = 'GRO' - units = {'time': None, 'length': 'nm', 'velocity': 'nm/ps'} + + format = "GRO" + units = {"time": None, "length": "nm", "velocity": "nm/ps"} _Timestep = Timestep def _read_first_frame(self): - with util.openany(self.filename, 'rt') as grofile: + with util.openany(self.filename, "rt") as grofile: # Read first two lines to get number of atoms grofile.readline() self.n_atoms = n_atoms = int(grofile.readline()) @@ -182,12 +184,16 @@ def _read_first_frame(self): # and the third line to get the spacing between coords (cs) # (dependent upon the GRO file precision) first_atomline = grofile.readline() - cs = first_atomline[25:].find('.') + 1 - ts._pos[0] = [first_atomline[20 + cs * i:20 + cs * (i + 1)] - for i in range(3)] + cs = first_atomline[25:].find(".") + 1 + ts._pos[0] = [ + first_atomline[20 + cs * i : 20 + cs * (i + 1)] + for i in range(3) + ] try: - velocities[0] = [first_atomline[20 + cs * i:20 + cs * (i + 1)] - for i in range(3, 6)] + velocities[0] = [ + first_atomline[20 + cs * i : 20 + cs * (i + 1)] + for i in range(3, 6) + ] except ValueError: # Remember that we got this error missed_vel = True @@ -200,12 +206,19 @@ def _read_first_frame(self): unitcell = np.float32(line.split()) except ValueError: # Try to parse floats with 5 digits if no spaces between values... - unitcell = np.float32(re.findall(r"(\d+\.\d{5})", line)) + unitcell = np.float32( + re.findall(r"(\d+\.\d{5})", line) + ) break - ts._pos[pos] = [line[20 + cs * i:20 + cs * (i + 1)] for i in range(3)] + ts._pos[pos] = [ + line[20 + cs * i : 20 + cs * (i + 1)] for i in range(3) + ] try: - velocities[pos] = [line[20 + cs * i:20 + cs * (i + 1)] for i in range(3, 6)] + velocities[pos] = [ + line[20 + cs * i : 20 + cs * (i + 1)] + for i in range(3, 6) + ] except ValueError: # Remember that we got this error missed_vel = True @@ -213,8 +226,10 @@ def _read_first_frame(self): if np.any(velocities): ts.velocities = velocities if missed_vel: - warnings.warn("Not all velocities were present. " - "Unset velocities set to zero.") + warnings.warn( + "Not all velocities were present. " + "Unset velocities set to zero." + ) self.ts.frame = 0 # 0-based frame number @@ -222,13 +237,15 @@ def _read_first_frame(self): # special case: a b c --> (a 0 0) (b 0 0) (c 0 0) # see docstring above for format (!) # Treat empty 3 entry boxes as not having a unit cell - if np.allclose(unitcell, [0., 0., 0.]): - wmsg = ("Empty box [0., 0., 0.] found - treating as missing " - "unit cell. Dimensions set to `None`.") + if np.allclose(unitcell, [0.0, 0.0, 0.0]): + wmsg = ( + "Empty box [0., 0., 0.] found - treating as missing " + "unit cell. Dimensions set to `None`." + ) warnings.warn(wmsg) self.ts.dimensions = None else: - self.ts.dimensions = np.r_[unitcell, [90., 90., 90.]] + self.ts.dimensions = np.r_[unitcell, [90.0, 90.0, 90.0]] elif len(unitcell) == 9: self.ts.dimensions = _gmx_to_dimensions(unitcell) else: # raise an error for wrong format @@ -238,7 +255,9 @@ def _read_first_frame(self): if self.convert_units: self.convert_pos_from_native(self.ts._pos) # in-place ! if self.ts.dimensions is not None: - self.convert_pos_from_native(self.ts.dimensions[:3]) # in-place! + self.convert_pos_from_native( + self.ts.dimensions[:3] + ) # in-place! if self.ts.has_velocities: # converts nm/ps to A/ps units self.convert_velocities_from_native(self.ts._velocities) @@ -295,21 +314,23 @@ class GROWriter(base.WriterBase): information (i.e. set to ``None``). """ - format = 'GRO' - units = {'time': None, 'length': 'nm', 'velocity': 'nm/ps'} - gro_coor_limits = {'min': -999.9995, 'max': 9999.9995} + format = "GRO" + units = {"time": None, "length": "nm", "velocity": "nm/ps"} + gro_coor_limits = {"min": -999.9995, "max": 9999.9995} #: format strings for the GRO file (all include newline); precision #: of 3 decimal places is hard-coded here. fmt = { - 'n_atoms': "{0:5d}\n", # number of atoms + "n_atoms": "{0:5d}\n", # number of atoms # coordinates output format, see http://chembytes.wikidot.com/g-grofile - 'xyz': "{resid:>5d}{resname:<5.5s}{name:>5.5s}{index:>5d}{pos[0]:8.3f}{pos[1]:8.3f}{pos[2]:8.3f}\n", + "xyz": "{resid:>5d}{resname:<5.5s}{name:>5.5s}{index:>5d}{pos[0]:8.3f}{pos[1]:8.3f}{pos[2]:8.3f}\n", # unitcell - 'box_orthorhombic': "{box[0]:10.5f} {box[1]:9.5f} {box[2]:9.5f}\n", - 'box_triclinic': "{box[0]:10.5f} {box[4]:9.5f} {box[8]:9.5f} {box[1]:9.5f} {box[2]:9.5f} {box[3]:9.5f} {box[5]:9.5f} {box[6]:9.5f} {box[7]:9.5f}\n" + "box_orthorhombic": "{box[0]:10.5f} {box[1]:9.5f} {box[2]:9.5f}\n", + "box_triclinic": "{box[0]:10.5f} {box[4]:9.5f} {box[8]:9.5f} {box[1]:9.5f} {box[2]:9.5f} {box[3]:9.5f} {box[5]:9.5f} {box[6]:9.5f} {box[7]:9.5f}\n", } - fmt['xyz_v'] = fmt['xyz'][:-1] + "{vel[0]:8.4f}{vel[1]:8.4f}{vel[2]:8.4f}\n" + fmt["xyz_v"] = ( + fmt["xyz"][:-1] + "{vel[0]:8.4f}{vel[1]:8.4f}{vel[2]:8.4f}\n" + ) def __init__(self, filename, convert_units=True, n_atoms=None, **kwargs): """Set up a GROWriter with a precision of 3 decimal places. @@ -344,11 +365,13 @@ def __init__(self, filename, convert_units=True, n_atoms=None, **kwargs): w.write(u.atoms) """ - self.filename = util.filename(filename, ext='gro', keep=True) + self.filename = util.filename(filename, ext="gro", keep=True) self.n_atoms = n_atoms - self.reindex = kwargs.pop('reindex', True) + self.reindex = kwargs.pop("reindex", True) - self.convert_units = convert_units # convert length and time to base units + self.convert_units = ( + convert_units # convert length and time to base units + ) def write(self, obj): """Write selection at current trajectory frame to file. @@ -396,25 +419,25 @@ def write(self, obj): try: names = ag.names except (AttributeError, NoDataError): - names = itertools.cycle(('X',)) - missing_topology.append('names') + names = itertools.cycle(("X",)) + missing_topology.append("names") try: resnames = ag.resnames except (AttributeError, NoDataError): - resnames = itertools.cycle(('UNK',)) - missing_topology.append('resnames') + resnames = itertools.cycle(("UNK",)) + missing_topology.append("resnames") try: resids = ag.resids except (AttributeError, NoDataError): resids = itertools.cycle((1,)) - missing_topology.append('resids') + missing_topology.append("resids") if not self.reindex: try: atom_indices = ag.ids except (AttributeError, NoDataError): - atom_indices = range(1, ag.n_atoms+1) - missing_topology.append('ids') + atom_indices = range(1, ag.n_atoms + 1) + missing_topology.append("ids") else: atom_indices = range(1, ag.n_atoms + 1) if missing_topology: @@ -422,7 +445,8 @@ def write(self, obj): "Supplied AtomGroup was missing the following attributes: " "{miss}. These will be written with default values. " "Alternatively these can be supplied as keyword arguments." - "".format(miss=', '.join(missing_topology))) + "".format(miss=", ".join(missing_topology)) + ) positions = ag.positions @@ -431,65 +455,81 @@ def write(self, obj): # Not inplace because AtomGroup is not a copy positions = self.convert_pos_to_native(positions, inplace=False) if has_velocities: - velocities = self.convert_velocities_to_native(velocities, inplace=False) + velocities = self.convert_velocities_to_native( + velocities, inplace=False + ) # check if any coordinates are illegal # (checks the coordinates in native nm!) if not self.has_valid_coordinates(self.gro_coor_limits, positions): - raise ValueError("GRO files must have coordinate values between " - "{0:.3f} and {1:.3f} nm: No file was written." - "".format(self.gro_coor_limits["min"], - self.gro_coor_limits["max"])) + raise ValueError( + "GRO files must have coordinate values between " + "{0:.3f} and {1:.3f} nm: No file was written." + "".format( + self.gro_coor_limits["min"], self.gro_coor_limits["max"] + ) + ) - with util.openany(self.filename, 'wt') as output_gro: + with util.openany(self.filename, "wt") as output_gro: # Header - output_gro.write('Written by MDAnalysis\n') - output_gro.write(self.fmt['n_atoms'].format(ag.n_atoms)) + output_gro.write("Written by MDAnalysis\n") + output_gro.write(self.fmt["n_atoms"].format(ag.n_atoms)) # Atom descriptions and coords # Dont use enumerate here, # all attributes could be infinite cycles! for atom_index, resid, resname, name in zip( - range(ag.n_atoms), resids, resnames, names): - truncated_atom_index = util.ltruncate_int(atom_indices[atom_index], 5) + range(ag.n_atoms), resids, resnames, names + ): + truncated_atom_index = util.ltruncate_int( + atom_indices[atom_index], 5 + ) truncated_resid = util.ltruncate_int(resid, 5) if has_velocities: - output_gro.write(self.fmt['xyz_v'].format( - resid=truncated_resid, - resname=resname, - index=truncated_atom_index, - name=name, - pos=positions[atom_index], - vel=velocities[atom_index], - )) + output_gro.write( + self.fmt["xyz_v"].format( + resid=truncated_resid, + resname=resname, + index=truncated_atom_index, + name=name, + pos=positions[atom_index], + vel=velocities[atom_index], + ) + ) else: - output_gro.write(self.fmt['xyz'].format( - resid=truncated_resid, - resname=resname, - index=truncated_atom_index, - name=name, - pos=positions[atom_index] - )) + output_gro.write( + self.fmt["xyz"].format( + resid=truncated_resid, + resname=resname, + index=truncated_atom_index, + name=name, + pos=positions[atom_index], + ) + ) # Footer: box dimensions - if (ag.dimensions is None or - np.allclose(ag.dimensions[3:], [90., 90., 90.])): + if ag.dimensions is None or np.allclose( + ag.dimensions[3:], [90.0, 90.0, 90.0] + ): if ag.dimensions is None: - wmsg = ("missing dimension - setting unit cell to zeroed " - "box [0., 0., 0.]") + wmsg = ( + "missing dimension - setting unit cell to zeroed " + "box [0., 0., 0.]" + ) warnings.warn(wmsg) box = np.zeros(3) else: box = self.convert_pos_to_native( - ag.dimensions[:3], inplace=False) + ag.dimensions[:3], inplace=False + ) # orthorhombic cell, only lengths along axes needed in gro - output_gro.write(self.fmt['box_orthorhombic'].format( - box=box) - ) + output_gro.write(self.fmt["box_orthorhombic"].format(box=box)) else: try: # for AtomGroup/Universe tri_dims = obj.universe.coord.triclinic_dimensions except AttributeError: # for Timestep tri_dims = obj.triclinic_dimensions # full output - box = self.convert_pos_to_native(tri_dims.flatten(), inplace=False) - output_gro.write(self.fmt['box_triclinic'].format(box=box)) + box = self.convert_pos_to_native( + tri_dims.flatten(), inplace=False + ) + output_gro.write(self.fmt["box_triclinic"].format(box=box)) diff --git a/package/MDAnalysis/coordinates/GSD.py b/package/MDAnalysis/coordinates/GSD.py index f08a3872213..bacd0441c33 100644 --- a/package/MDAnalysis/coordinates/GSD.py +++ b/package/MDAnalysis/coordinates/GSD.py @@ -51,6 +51,7 @@ """ import numpy as np + try: import gsd import gsd.fl @@ -72,11 +73,10 @@ class MockHOOMDTrajectory: class GSDReader(base.ReaderBase): - """Reader for the GSD format. + """Reader for the GSD format.""" - """ - format = 'GSD' - units = {'time': None, 'length': None} + format = "GSD" + units = {"time": None, "length": None} @store_init_arguments def __init__(self, filename, **kwargs): @@ -111,7 +111,7 @@ def __init__(self, filename, **kwargs): def open_trajectory(self): """opens the trajectory file using gsd.hoomd module""" self._frame = -1 - self._file = gsd_pickle_open(self.filename, mode='r') + self._file = gsd_pickle_open(self.filename, mode="r") def close(self): """close reader""" @@ -138,7 +138,7 @@ def _read_frame(self, frame): # sets the Timestep object self.ts.frame = frame - self.ts.data['step'] = myframe.configuration.step + self.ts.data["step"] = myframe.configuration.step # set frame box dimensions self.ts.dimensions = myframe.configuration.box @@ -148,9 +148,11 @@ def _read_frame(self, frame): frame_positions = myframe.particles.position n_atoms_now = frame_positions.shape[0] if n_atoms_now != self.n_atoms: - raise ValueError("Frame %d has %d atoms but the initial frame has %d" + raise ValueError( + "Frame %d has %d atoms but the initial frame has %d" " atoms. MDAnalysis in unable to deal with variable" - " topology!"%(frame, n_atoms_now, self.n_atoms)) + " topology!" % (frame, n_atoms_now, self.n_atoms) + ) else: self.ts.positions = frame_positions return self.ts @@ -206,21 +208,24 @@ class GSDPicklable(gsd.hoomd.HOOMDTrajectory): .. versionadded:: 2.0.0 """ + def __getstate__(self): return self.file.name, self.file.mode def __setstate__(self, args): gsd_version = gsd.version.version schema_version = [1, 4] - gsdfileobj = gsd.fl.open(name=args[0], - mode=args[1], - application='gsd.hoomd ' + gsd_version, - schema='hoomd', - schema_version=schema_version) + gsdfileobj = gsd.fl.open( + name=args[0], + mode=args[1], + application="gsd.hoomd " + gsd_version, + schema="hoomd", + schema_version=schema_version, + ) self.__init__(gsdfileobj) -def gsd_pickle_open(name: str, mode: str='r'): +def gsd_pickle_open(name: str, mode: str = "r"): """Open hoomd schema GSD file with pickle function implemented. This function returns a GSDPicklable object. It can be used as a @@ -278,12 +283,13 @@ def gsd_pickle_open(name: str, mode: str='r'): """ gsd_version = gsd.version.version schema_version = [1, 4] - if mode != 'r': - raise ValueError("Only read mode 'r' " - "files can be pickled.") - gsdfileobj = gsd.fl.open(name=name, - mode=mode, - application='gsd.hoomd ' + gsd_version, - schema='hoomd', - schema_version=schema_version) + if mode != "r": + raise ValueError("Only read mode 'r' " "files can be pickled.") + gsdfileobj = gsd.fl.open( + name=name, + mode=mode, + application="gsd.hoomd " + gsd_version, + schema="hoomd", + schema_version=schema_version, + ) return GSDPicklable(gsdfileobj) diff --git a/package/MDAnalysis/coordinates/INPCRD.py b/package/MDAnalysis/coordinates/INPCRD.py index 9b90f6301e1..de464da0895 100644 --- a/package/MDAnalysis/coordinates/INPCRD.py +++ b/package/MDAnalysis/coordinates/INPCRD.py @@ -40,15 +40,16 @@ from . import base + class INPReader(base.SingleFrameReaderBase): """Reader for Amber restart files.""" - format = ['INPCRD', 'RESTRT'] - units = {'length': 'Angstrom'} + format = ["INPCRD", "RESTRT"] + units = {"length": "Angstrom"} def _read_first_frame(self): # Read header - with open(self.filename, 'r') as inf: + with open(self.filename, "r") as inf: self.title = inf.readline().strip() line = inf.readline().split() self.n_atoms = int(line[0]) @@ -65,18 +66,26 @@ def _read_first_frame(self): for p in range(self.n_atoms // 2): line = inf.readline() # each float is f12.7, 6 floats a line - for i, dest in enumerate([(2*p, 0), (2*p, 1), (2*p, 2), - (2*p + 1, 0), (2*p + 1, 1), (2*p + 1, 2)]): - self.ts._pos[dest] = float(line[i*12:(i+1)*12]) + for i, dest in enumerate( + [ + (2 * p, 0), + (2 * p, 1), + (2 * p, 2), + (2 * p + 1, 0), + (2 * p + 1, 1), + (2 * p + 1, 2), + ] + ): + self.ts._pos[dest] = float(line[i * 12 : (i + 1) * 12]) # Read last coordinate if necessary if self.n_atoms % 2: line = inf.readline() for i in range(3): - self.ts._pos[-1, i] = float(line[i*12:(i+1)*12]) + self.ts._pos[-1, i] = float(line[i * 12 : (i + 1) * 12]) @staticmethod def parse_n_atoms(filename, **kwargs): - with open(filename, 'r') as f: + with open(filename, "r") as f: f.readline() n_atoms = int(f.readline().split()[0]) return n_atoms diff --git a/package/MDAnalysis/coordinates/LAMMPS.py b/package/MDAnalysis/coordinates/LAMMPS.py index 2a91c44e331..c45345c6079 100644 --- a/package/MDAnalysis/coordinates/LAMMPS.py +++ b/package/MDAnalysis/coordinates/LAMMPS.py @@ -148,8 +148,13 @@ from . import base import warnings -btype_sections = {'bond':'Bonds', 'angle':'Angles', - 'dihedral':'Dihedrals', 'improper':'Impropers'} +btype_sections = { + "bond": "Bonds", + "angle": "Angles", + "dihedral": "Dihedrals", + "improper": "Impropers", +} + class DCDWriter(DCD.DCDWriter): """Write a LAMMPS_ DCD trajectory. @@ -159,18 +164,26 @@ class DCDWriter(DCD.DCDWriter): "Angstrom". See :mod:`MDAnalysis.units` for other recognized values. """ - format = 'LAMMPS' + + format = "LAMMPS" multiframe = True - flavor = 'LAMMPS' + flavor = "LAMMPS" def __init__(self, *args, **kwargs): - self.units = {'time': 'fs', 'length': 'Angstrom'} # must be instance level - self.units['time'] = kwargs.pop('timeunit', self.units['time']) - self.units['length'] = kwargs.pop('lengthunit', self.units['length']) + self.units = { + "time": "fs", + "length": "Angstrom", + } # must be instance level + self.units["time"] = kwargs.pop("timeunit", self.units["time"]) + self.units["length"] = kwargs.pop("lengthunit", self.units["length"]) for unit_type, unit in self.units.items(): try: if units.unit_types[unit] != unit_type: - raise TypeError("LAMMPS DCDWriter: wrong unit {0!r} for unit type {1!r}".format(unit, unit_type)) + raise TypeError( + "LAMMPS DCDWriter: wrong unit {0!r} for unit type {1!r}".format( + unit, unit_type + ) + ) except KeyError: errmsg = f"LAMMPS DCDWriter: unknown unit {unit}" raise ValueError(errmsg) from None @@ -187,20 +200,30 @@ class DCDReader(DCD.DCDReader): .. _units style: http://lammps.sandia.gov/doc/units.html """ - format = 'LAMMPS' - flavor = 'LAMMPS' + + format = "LAMMPS" + flavor = "LAMMPS" @store_init_arguments def __init__(self, dcdfilename, **kwargs): - self.units = {'time': 'fs', 'length': 'Angstrom'} # must be instance level - self.units['time'] = kwargs.pop('timeunit', self.units['time']) - self.units['length'] = kwargs.pop('lengthunit', self.units['length']) + self.units = { + "time": "fs", + "length": "Angstrom", + } # must be instance level + self.units["time"] = kwargs.pop("timeunit", self.units["time"]) + self.units["length"] = kwargs.pop("lengthunit", self.units["length"]) for unit_type, unit in self.units.items(): try: if units.unit_types[unit] != unit_type: - raise TypeError("LAMMPS DCDReader: wrong unit {0!r} for unit type {1!r}".format(unit, unit_type)) + raise TypeError( + "LAMMPS DCDReader: wrong unit {0!r} for unit type {1!r}".format( + unit, unit_type + ) + ) except KeyError: - raise ValueError("LAMMPS DCDReader: unknown unit {0!r}".format(unit)) + raise ValueError( + "LAMMPS DCDReader: unknown unit {0!r}".format(unit) + ) super(DCDReader, self).__init__(dcdfilename, **kwargs) @@ -211,30 +234,35 @@ class DATAReader(base.SingleFrameReaderBase): .. versionchanged:: 0.11.0 Frames now 0-based instead of 1-based """ - format = 'DATA' - units = {'time': None, 'length': 'Angstrom', 'velocity': 'Angstrom/fs'} + + format = "DATA" + units = {"time": None, "length": "Angstrom", "velocity": "Angstrom/fs"} @store_init_arguments def __init__(self, filename, **kwargs): - self.n_atoms = kwargs.pop('n_atoms', None) + self.n_atoms = kwargs.pop("n_atoms", None) if self.n_atoms is None: # this should be done by parsing DATA first raise ValueError("DATAReader requires n_atoms keyword") - self.atom_style = kwargs.pop('atom_style', None) + self.atom_style = kwargs.pop("atom_style", None) super(DATAReader, self).__init__(filename, **kwargs) def _read_first_frame(self): with DATAParser(self.filename) as p: - self.ts = p.read_DATA_timestep(self.n_atoms, self._Timestep, - self._ts_kwargs, self.atom_style) + self.ts = p.read_DATA_timestep( + self.n_atoms, self._Timestep, self._ts_kwargs, self.atom_style + ) self.ts.frame = 0 if self.convert_units: self.convert_pos_from_native(self.ts._pos) # in-place ! try: - self.convert_velocities_from_native(self.ts._velocities) # in-place ! + self.convert_velocities_from_native( + self.ts._velocities + ) # in-place ! except AttributeError: pass + class DATAWriter(base.WriterBase): """Write out the current time step as a LAMMPS DATA file. @@ -263,7 +291,8 @@ class DATAWriter(base.WriterBase): an integer >= 1. """ - format = 'DATA' + + format = "DATA" def __init__(self, filename, convert_units=True, **kwargs): """Set up a DATAWriter @@ -275,20 +304,21 @@ def __init__(self, filename, convert_units=True, **kwargs): convert_units : bool, optional units are converted to the MDAnalysis base format; [``True``] """ - self.filename = util.filename(filename, ext='data', keep=True) + self.filename = util.filename(filename, ext="data", keep=True) self.convert_units = convert_units - self.units = {'time': 'fs', 'length': 'Angstrom'} - self.units['length'] = kwargs.pop('lengthunit', self.units['length']) - self.units['time'] = kwargs.pop('timeunit', self.units['time']) - self.units['velocity'] = kwargs.pop('velocityunit', - self.units['length']+'/'+self.units['time']) + self.units = {"time": "fs", "length": "Angstrom"} + self.units["length"] = kwargs.pop("lengthunit", self.units["length"]) + self.units["time"] = kwargs.pop("timeunit", self.units["time"]) + self.units["velocity"] = kwargs.pop( + "velocityunit", self.units["length"] + "/" + self.units["time"] + ) def _write_atoms(self, atoms, data): - self.f.write('\n') - self.f.write('Atoms\n') - self.f.write('\n') + self.f.write("\n") + self.f.write("Atoms\n") + self.f.write("\n") try: charges = atoms.charges @@ -303,63 +333,84 @@ def _write_atoms(self, atoms, data): moltags = data.get("molecule_tag", np.zeros(len(atoms), dtype=int)) if self.convert_units: - coordinates = self.convert_pos_to_native(atoms.positions, inplace=False) + coordinates = self.convert_pos_to_native( + atoms.positions, inplace=False + ) if has_charges: - for index, moltag, atype, charge, coords in zip(indices, moltags, - types, charges, coordinates): + for index, moltag, atype, charge, coords in zip( + indices, moltags, types, charges, coordinates + ): x, y, z = coords - self.f.write(f"{index:d} {moltag:d} {atype:d} {charge:f}" - f" {x:f} {y:f} {z:f}\n") + self.f.write( + f"{index:d} {moltag:d} {atype:d} {charge:f}" + f" {x:f} {y:f} {z:f}\n" + ) else: - for index, moltag, atype, coords in zip(indices, moltags, types, - coordinates): + for index, moltag, atype, coords in zip( + indices, moltags, types, coordinates + ): x, y, z = coords - self.f.write(f"{index:d} {moltag:d} {atype:d}" - f" {x:f} {y:f} {z:f}\n") + self.f.write( + f"{index:d} {moltag:d} {atype:d}" f" {x:f} {y:f} {z:f}\n" + ) def _write_velocities(self, atoms): - self.f.write('\n') - self.f.write('Velocities\n') - self.f.write('\n') + self.f.write("\n") + self.f.write("Velocities\n") + self.f.write("\n") indices = atoms.indices + 1 - velocities = self.convert_velocities_to_native(atoms.velocities, - inplace=False) + velocities = self.convert_velocities_to_native( + atoms.velocities, inplace=False + ) for index, vel in zip(indices, velocities): - self.f.write('{i:d} {x:f} {y:f} {z:f}\n'.format(i=index, x=vel[0], - y=vel[1], z=vel[2])) + self.f.write( + "{i:d} {x:f} {y:f} {z:f}\n".format( + i=index, x=vel[0], y=vel[1], z=vel[2] + ) + ) def _write_masses(self, atoms): - self.f.write('\n') - self.f.write('Masses\n') - self.f.write('\n') + self.f.write("\n") + self.f.write("Masses\n") + self.f.write("\n") mass_dict = {} max_type = max(atoms.types.astype(np.int32)) - for atype in range(1, max_type+1): + for atype in range(1, max_type + 1): # search entire universe for mass info, not just writing selection - masses = set(atoms.universe.atoms.select_atoms( - 'type {:d}'.format(atype)).masses) + masses = set( + atoms.universe.atoms.select_atoms( + "type {:d}".format(atype) + ).masses + ) if len(masses) == 0: mass_dict[atype] = 1.0 else: mass_dict[atype] = masses.pop() if masses: - raise ValueError('LAMMPS DATAWriter: to write data file, '+ - 'atoms with same type must have same mass') + raise ValueError( + "LAMMPS DATAWriter: to write data file, " + + "atoms with same type must have same mass" + ) for atype, mass in mass_dict.items(): - self.f.write('{:d} {:f}\n'.format(atype, mass)) + self.f.write("{:d} {:f}\n".format(atype, mass)) def _write_bonds(self, bonds): - self.f.write('\n') - self.f.write('{}\n'.format(btype_sections[bonds.btype])) - self.f.write('\n') - for bond, i in zip(bonds, range(1, len(bonds)+1)): + self.f.write("\n") + self.f.write("{}\n".format(btype_sections[bonds.btype])) + self.f.write("\n") + for bond, i in zip(bonds, range(1, len(bonds) + 1)): try: - self.f.write('{:d} {:d} '.format(i, int(bond.type))+\ - ' '.join((bond.atoms.indices + 1).astype(str))+'\n') + self.f.write( + "{:d} {:d} ".format(i, int(bond.type)) + + " ".join((bond.atoms.indices + 1).astype(str)) + + "\n" + ) except TypeError: - errmsg = (f"LAMMPS DATAWriter: Trying to write bond, but bond " - f"type {bond.type} is not numerical.") + errmsg = ( + f"LAMMPS DATAWriter: Trying to write bond, but bond " + f"type {bond.type} is not numerical." + ) raise TypeError(errmsg) from None def _write_dimensions(self, dimensions): @@ -367,18 +418,22 @@ def _write_dimensions(self, dimensions): units and then write the dimensions section """ if self.convert_units: - triv = self.convert_pos_to_native(mdamath.triclinic_vectors( - dimensions),inplace=False) - self.f.write('\n') - self.f.write('{:f} {:f} xlo xhi\n'.format(0., triv[0][0])) - self.f.write('{:f} {:f} ylo yhi\n'.format(0., triv[1][1])) - self.f.write('{:f} {:f} zlo zhi\n'.format(0., triv[2][2])) + triv = self.convert_pos_to_native( + mdamath.triclinic_vectors(dimensions), inplace=False + ) + self.f.write("\n") + self.f.write("{:f} {:f} xlo xhi\n".format(0.0, triv[0][0])) + self.f.write("{:f} {:f} ylo yhi\n".format(0.0, triv[1][1])) + self.f.write("{:f} {:f} zlo zhi\n".format(0.0, triv[2][2])) if any([triv[1][0], triv[2][0], triv[2][1]]): - self.f.write('{xy:f} {xz:f} {yz:f} xy xz yz\n'.format( - xy=triv[1][0], xz=triv[2][0], yz=triv[2][1])) - self.f.write('\n') - - @requires('types', 'masses') + self.f.write( + "{xy:f} {xz:f} {yz:f} xy xz yz\n".format( + xy=triv[1][0], xz=triv[2][0], yz=triv[2][1] + ) + ) + self.f.write("\n") + + @requires("types", "masses") def write(self, selection, frame=None): """Write selection at current trajectory frame to file. @@ -419,8 +474,10 @@ def write(self, selection, frame=None): try: atoms.types.astype(np.int32) except ValueError: - errmsg = ("LAMMPS.DATAWriter: atom types must be convertible to " - "integers") + errmsg = ( + "LAMMPS.DATAWriter: atom types must be convertible to " + "integers" + ) raise ValueError(errmsg) from None try: @@ -431,27 +488,38 @@ def write(self, selection, frame=None): has_velocities = True features = {} - with util.openany(self.filename, 'wt') as self.f: - self.f.write('LAMMPS data file via MDAnalysis\n') - self.f.write('\n') - self.f.write('{:>12d} atoms\n'.format(len(atoms))) - - attrs = [('bond', 'bonds'), ('angle', 'angles'), - ('dihedral', 'dihedrals'), ('improper', 'impropers')] + with util.openany(self.filename, "wt") as self.f: + self.f.write("LAMMPS data file via MDAnalysis\n") + self.f.write("\n") + self.f.write("{:>12d} atoms\n".format(len(atoms))) + + attrs = [ + ("bond", "bonds"), + ("angle", "angles"), + ("dihedral", "dihedrals"), + ("improper", "impropers"), + ] for btype, attr_name in attrs: features[btype] = atoms.__getattribute__(attr_name) - self.f.write('{:>12d} {}\n'.format(len(features[btype]), - attr_name)) + self.f.write( + "{:>12d} {}\n".format(len(features[btype]), attr_name) + ) features[btype] = features[btype].atomgroup_intersection( - atoms, strict=True) + atoms, strict=True + ) - self.f.write('\n') - self.f.write('{:>12d} atom types\n'.format(max(atoms.types.astype(np.int32)))) + self.f.write("\n") + self.f.write( + "{:>12d} atom types\n".format( + max(atoms.types.astype(np.int32)) + ) + ) for btype, attr in features.items(): - self.f.write('{:>12d} {} types\n'.format(len(attr.types()), - btype)) + self.f.write( + "{:>12d} {} types\n".format(len(attr.types()), btype) + ) self._write_dimensions(atoms.dimensions) @@ -467,7 +535,7 @@ def write(self, selection, frame=None): class DumpReader(base.ReaderBase): - """Reads the default `LAMMPS dump format + """Reads the default `LAMMPS dump format `__ Supports coordinates in the LAMMPS "unscaled" (x,y,z), "scaled" (xs,ys,zs), @@ -523,10 +591,10 @@ class DumpReader(base.ReaderBase): Convention used in coordinates, can be one of the following according to the `LAMMPS documentation `__: - - "auto" - Detect coordinate type from file column header. If auto + - "auto" - Detect coordinate type from file column header. If auto detection is used, the guessing checks whether the coordinates - fit each convention in the order "unscaled", "scaled", "unwrapped", - "scaled_unwrapped" and whichever set of coordinates is detected + fit each convention in the order "unscaled", "scaled", "unwrapped", + "scaled_unwrapped" and whichever set of coordinates is detected first will be used. - "scaled" - Coordinates wrapped in box and scaled by box length (see note below), i.e., xs, ys, zs @@ -536,13 +604,13 @@ class DumpReader(base.ReaderBase): - "unwrapped" - Coordinates unwrapped, i.e., xu, yu, zu If coordinates are given in the scaled coordinate convention (xs,ys,zs) - or scaled unwrapped coordinate convention (xsu,ysu,zsu) they will + or scaled unwrapped coordinate convention (xsu,ysu,zsu) they will automatically be converted from their scaled/fractional representation to their real values. unwrap_images : bool (optional) default=False - If `True` and the dump file contains image flags, the coordinates - will be unwrapped. See `read_data - `__ in the lammps + If `True` and the dump file contains image flags, the coordinates + will be unwrapped. See `read_data + `__ in the lammps documentation for more information. **kwargs Other keyword arguments used in :class:`~MDAnalysis.coordinates.base.ReaderBase` @@ -561,15 +629,21 @@ class DumpReader(base.ReaderBase): Now parses coordinates in multiple lammps conventions (x,xs,xu,xsu) .. versionadded:: 0.19.0 """ - format = 'LAMMPSDUMP' - _conventions = ["auto", "unscaled", "scaled", "unwrapped", - "scaled_unwrapped"] + + format = "LAMMPSDUMP" + _conventions = [ + "auto", + "unscaled", + "scaled", + "unwrapped", + "scaled_unwrapped", + ] _coordtype_column_names = { "unscaled": ["x", "y", "z"], "scaled": ["xs", "ys", "zs"], "unwrapped": ["xu", "yu", "zu"], - "scaled_unwrapped": ["xsu", "ysu", "zsu"] + "scaled_unwrapped": ["xsu", "ysu", "zsu"], } _parsable_columns = ["id", "vx", "vy", "vz", "fx", "fy", "fz"] @@ -577,10 +651,14 @@ class DumpReader(base.ReaderBase): _parsable_columns += _coordtype_column_names[key] @store_init_arguments - def __init__(self, filename, - lammps_coordinate_convention="auto", - unwrap_images=False, - additional_columns=None, **kwargs): + def __init__( + self, + filename, + lammps_coordinate_convention="auto", + unwrap_images=False, + additional_columns=None, + **kwargs, + ): super(DumpReader, self).__init__(filename, **kwargs) root, ext = os.path.splitext(self.filename) @@ -588,22 +666,28 @@ def __init__(self, filename, self.lammps_coordinate_convention = lammps_coordinate_convention else: option_string = "'" + "', '".join(self._conventions) + "'" - raise ValueError("lammps_coordinate_convention=" - f"'{lammps_coordinate_convention}'" - " is not a valid option. " - f"Please choose one of {option_string}") + raise ValueError( + "lammps_coordinate_convention=" + f"'{lammps_coordinate_convention}'" + " is not a valid option. " + f"Please choose one of {option_string}" + ) self._unwrap = unwrap_images - if (util.iterable(additional_columns) - or additional_columns is None - or additional_columns is True): + if ( + util.iterable(additional_columns) + or additional_columns is None + or additional_columns is True + ): self._additional_columns = additional_columns else: - raise ValueError(f"additional_columns={additional_columns} " - "is not a valid option. Please provide an " - "iterable containing the additional" - "column headers.") + raise ValueError( + f"additional_columns={additional_columns} " + "is not a valid option. Please provide an " + "iterable containing the additional" + "column headers." + ) self._cache = {} @@ -618,7 +702,7 @@ def _reopen(self): self.ts.frame = -1 @property - @cached('n_atoms') + @cached("n_atoms") def n_atoms(self): with util.anyopen(self.filename) as f: f.readline() @@ -628,7 +712,7 @@ def n_atoms(self): return n_atoms @property - @cached('n_frames') + @cached("n_frames") def n_frames(self): # 2(timestep) + 2(natoms info) + 4(box info) + 1(atom header) + n_atoms lines_per_frame = self.n_atoms + 9 @@ -645,7 +729,7 @@ def n_frames(self): return len(self._offsets) def close(self): - if hasattr(self, '_file'): + if hasattr(self, "_file"): self._file.close() def _read_frame(self, frame): @@ -663,14 +747,16 @@ def _read_next_timestep(self): f.readline() # ITEM TIMESTEP step_num = int(f.readline()) - ts.data['step'] = step_num - ts.data['time'] = step_num * ts.dt + ts.data["step"] = step_num + ts.data["time"] = step_num * ts.dt f.readline() # ITEM NUMBER OF ATOMS n_atoms = int(f.readline()) if n_atoms != self.n_atoms: - raise ValueError("Number of atoms in trajectory changed " - "this is not supported in MDAnalysis") + raise ValueError( + "Number of atoms in trajectory changed " + "this is not supported in MDAnalysis" + ) triclinic = len(f.readline().split()) == 9 # ITEM BOX BOUNDS if triclinic: @@ -698,7 +784,7 @@ def _read_next_timestep(self): xlen = xhi - xlo ylen = yhi - ylo zlen = zhi - zlo - alpha = beta = gamma = 90. + alpha = beta = gamma = 90.0 ts.dimensions = xlen, ylen, zlen, alpha, beta, gamma indices = np.zeros(self.n_atoms, dtype=int) @@ -709,8 +795,9 @@ def _read_next_timestep(self): convention_to_col_ix = {} for cv_name, cv_col_names in self._coordtype_column_names.items(): try: - convention_to_col_ix[cv_name] = [attr_to_col_ix[x] - for x in cv_col_names] + convention_to_col_ix[cv_name] = [ + attr_to_col_ix[x] for x in cv_col_names + ] except KeyError: pass @@ -718,8 +805,9 @@ def _read_next_timestep(self): try: image_cols = [attr_to_col_ix[x] for x in ["ix", "iy", "iz"]] except: - raise ValueError("Trajectory must have image flag in order " - "to unwrap.") + raise ValueError( + "Trajectory must have image flag in order " "to unwrap." + ) self._has_vels = all(x in attr_to_col_ix for x in ["vx", "vy", "vz"]) if self._has_vels: @@ -737,13 +825,17 @@ def _read_next_timestep(self): try: # this will automatically select in order of priority # unscaled, scaled, unwrapped, scaled_unwrapped - self.lammps_coordinate_convention = list(convention_to_col_ix)[0] + self.lammps_coordinate_convention = list(convention_to_col_ix)[ + 0 + ] except IndexError: raise ValueError("No coordinate information detected") elif not self.lammps_coordinate_convention in convention_to_col_ix: - raise ValueError(f"No coordinates following convention " - "{self.lammps_coordinate_convention} found in " - "timestep") + raise ValueError( + f"No coordinates following convention " + "{self.lammps_coordinate_convention} found in " + "timestep" + ) coord_cols = convention_to_col_ix[self.lammps_coordinate_convention] if self._unwrap: @@ -759,10 +851,13 @@ def _read_next_timestep(self): additional_keys = set(attrs).difference(self._parsable_columns) elif self._additional_columns: if not all([key in attrs for key in self._additional_columns]): - warnings.warn("Some of the additional columns are not present " - "in the file, they will be ignored") - additional_keys = \ - [key for key in self._additional_columns if key in attrs] + warnings.warn( + "Some of the additional columns are not present " + "in the file, they will be ignored" + ) + additional_keys = [ + key for key in self._additional_columns if key in attrs + ] else: additional_keys = [] for key in additional_keys: @@ -773,8 +868,9 @@ def _read_next_timestep(self): fields = f.readline().split() if ids: indices[i] = fields[attr_to_col_ix["id"]] - coords = np.array([fields[dim] for dim in coord_cols], - dtype=np.float32) + coords = np.array( + [fields[dim] for dim in coord_cols], dtype=np.float32 + ) if self._unwrap: images = coords[3:] @@ -791,8 +887,9 @@ def _read_next_timestep(self): # Collect additional cols for attribute_key in additional_keys: - ts.data[attribute_key][i] = \ - fields[attr_to_col_ix[attribute_key]] + ts.data[attribute_key][i] = fields[ + attr_to_col_ix[attribute_key] + ] order = np.argsort(indices) ts.positions = ts.positions[order] @@ -805,11 +902,12 @@ def _read_next_timestep(self): for attribute_key in additional_keys: ts.data[attribute_key] = ts.data[attribute_key][order] - if (self.lammps_coordinate_convention.startswith("scaled")): + if self.lammps_coordinate_convention.startswith("scaled"): # if coordinates are given in scaled format, undo that - ts.positions = distances.transform_StoR(ts.positions, - ts.dimensions) + ts.positions = distances.transform_StoR( + ts.positions, ts.dimensions + ) # Transform to origin after transformation of scaled variables - ts.positions -= np.array([xlo, ylo, zlo])[None,:] + ts.positions -= np.array([xlo, ylo, zlo])[None, :] return ts diff --git a/package/MDAnalysis/coordinates/MMTF.py b/package/MDAnalysis/coordinates/MMTF.py index 00ef4774378..7894e9dc4a1 100644 --- a/package/MDAnalysis/coordinates/MMTF.py +++ b/package/MDAnalysis/coordinates/MMTF.py @@ -50,7 +50,7 @@ def _parse_mmtf(fn): - if fn.endswith('gz'): + if fn.endswith("gz"): return mmtf.parse_gzip(fn) else: return mmtf.parse(fn) @@ -65,17 +65,19 @@ class MMTFReader(base.SingleFrameReaderBase): Protein Data Bank. The Reader will be removed in version 3.0. Users are encouraged to instead use alternative PDB formats. """ - format = 'MMTF' + + format = "MMTF" @store_init_arguments def __init__(self, filename, convert_units=True, n_atoms=None, **kwargs): - wmsg = ("The MMTF Reader is deprecated and will be removed in " - "MDAnalysis version 3.0.0") + wmsg = ( + "The MMTF Reader is deprecated and will be removed in " + "MDAnalysis version 3.0.0" + ) warnings.warn(wmsg, DeprecationWarning) super(MMTFReader, self).__init__( - filename, convert_units, n_atoms, - **kwargs + filename, convert_units, n_atoms, **kwargs ) @staticmethod @@ -87,9 +89,9 @@ def _format_hint(thing): return isinstance(thing, mmtf.MMTFDecoder) @due.dcite( - Doi('10.1371/journal.pcbi.1005575'), + Doi("10.1371/journal.pcbi.1005575"), description="MMTF Reader", - path='MDAnalysis.coordinates.MMTF', + path="MDAnalysis.coordinates.MMTF", ) def _read_first_frame(self): # TOOD: Check units? @@ -99,8 +101,7 @@ def _read_first_frame(self): top = _parse_mmtf(self.filename) self.n_atoms = top.num_atoms - self.ts = ts = self._Timestep(self.n_atoms, - **self._ts_kwargs) + self.ts = ts = self._Timestep(self.n_atoms, **self._ts_kwargs) ts._pos[:, 0] = top.x_coord_list ts._pos[:, 1] = top.y_coord_list ts._pos[:, 2] = top.z_coord_list diff --git a/package/MDAnalysis/coordinates/NAMDBIN.py b/package/MDAnalysis/coordinates/NAMDBIN.py index b9425f18f98..834ea346aee 100644 --- a/package/MDAnalysis/coordinates/NAMDBIN.py +++ b/package/MDAnalysis/coordinates/NAMDBIN.py @@ -49,29 +49,30 @@ class NAMDBINReader(base.SingleFrameReaderBase): """Reader for NAMD binary coordinate files. - - - .. versionadded:: 1.0.0 + + + .. versionadded:: 1.0.0 """ - format = ['COOR', 'NAMDBIN'] - units = {'length': 'Angstrom'} + format = ["COOR", "NAMDBIN"] + units = {"length": "Angstrom"} def _read_first_frame(self): # Read header - with open(self.filename, 'rb') as namdbin: + with open(self.filename, "rb") as namdbin: self.n_atoms = np.fromfile(namdbin, dtype=np.int32, count=1)[0] self.ts = self._Timestep(self.n_atoms, **self._ts_kwargs) self.ts.frame = 0 - coord_double = np.fromfile(namdbin, - dtype=np.float64, - count=self.n_atoms * 3) - self.ts._pos[:] = np.array( - coord_double, float).reshape(self.n_atoms, 3) + coord_double = np.fromfile( + namdbin, dtype=np.float64, count=self.n_atoms * 3 + ) + self.ts._pos[:] = np.array(coord_double, float).reshape( + self.n_atoms, 3 + ) @staticmethod def parse_n_atoms(filename, **kwargs): - with open(filename, 'rb') as namdbin: + with open(filename, "rb") as namdbin: n_atoms = np.fromfile(namdbin, dtype=np.int32, count=1)[0] return n_atoms @@ -93,7 +94,7 @@ def Writer(self, filename, **kwargs): class NAMDBINWriter(base.WriterBase): """Writer for NAMD binary coordinate files. - + Note ---- @@ -102,8 +103,9 @@ class NAMDBINWriter(base.WriterBase): .. versionadded:: 1.0.0 """ - format = ['COOR', 'NAMDBIN'] - units = {'time': None, 'length': 'Angstrom'} + + format = ["COOR", "NAMDBIN"] + units = {"time": None, "length": "Angstrom"} def __init__(self, filename, n_atoms=None, **kwargs): """ @@ -134,16 +136,16 @@ def _write_next_frame(self, obj): Deprecated support for Timestep argument has now been removed. Use AtomGroup or Universe as an input instead. """ - if hasattr(obj, 'atoms'): # AtomGroup or Universe + if hasattr(obj, "atoms"): # AtomGroup or Universe atoms = obj.atoms n_atoms = len(atoms) - coor = atoms.positions.reshape(n_atoms*3) + coor = atoms.positions.reshape(n_atoms * 3) else: errmsg = "Input obj is neither an AtomGroup or Universe" raise TypeError(errmsg) from None - with util.openany(self.filename, 'wb') as namdbin: + with util.openany(self.filename, "wb") as namdbin: # Write NUMATOMS - namdbin.write(pack('i', n_atoms)) + namdbin.write(pack("i", n_atoms)) # Write Coordinate - namdbin.write(pack('{:d}d'.format(len(coor)), *coor)) + namdbin.write(pack("{:d}d".format(len(coor)), *coor)) diff --git a/package/MDAnalysis/coordinates/PDBQT.py b/package/MDAnalysis/coordinates/PDBQT.py index f0913d6e049..44f19e54f83 100644 --- a/package/MDAnalysis/coordinates/PDBQT.py +++ b/package/MDAnalysis/coordinates/PDBQT.py @@ -141,8 +141,9 @@ class PDBQTReader(base.SingleFrameReaderBase): .. versionchanged:: 0.11.0 Frames now 0-based instead of 1-based """ - format = 'PDBQT' - units = {'time': None, 'length': 'Angstrom'} + + format = "PDBQT" + units = {"time": None, "length": "Angstrom"} def _read_first_frame(self): coords = [] @@ -152,21 +153,23 @@ def _read_first_frame(self): # Should only break at the 'END' of a model definition # and prevent premature exit for a torsion termination # , eg, ENDBRANCH - if line.startswith('END\n'): + if line.startswith("END\n"): break - if line.startswith('CRYST1'): + if line.startswith("CRYST1"): # lengths - x, y, z = np.float32((line[6:15], line[15:24], line[24:33])) + x, y, z = np.float32( + (line[6:15], line[15:24], line[24:33]) + ) # angles - A, B, G = np.float32((line[33:40], line[40:47], line[47:54])) + A, B, G = np.float32( + (line[33:40], line[40:47], line[47:54]) + ) unitcell[:] = x, y, z, A, B, G - if line.startswith(('ATOM', 'HETATM')): + if line.startswith(("ATOM", "HETATM")): # convert all entries at the end once for optimal speed coords.append([line[30:38], line[38:46], line[46:54]]) self.n_atoms = len(coords) - self.ts = self._Timestep.from_coordinates( - coords, - **self._ts_kwargs) + self.ts = self._Timestep.from_coordinates(coords, **self._ts_kwargs) self.ts.dimensions = unitcell self.ts.frame = 0 # 0-based frame number if self.convert_units: @@ -204,23 +207,27 @@ class PDBQTWriter(base.WriterBase): """ fmt = { - 'ATOM': ("ATOM {serial:5d} {name:<4.4s} {resName:<4.4s}" - "{chainID:1.1s}{resSeq:4d}{iCode:1.1s}" - " {pos[0]:8.3f}{pos[1]:8.3f}{pos[2]:8.3f}{occupancy:6.2f}" - "{tempFactor:6.2f} {charge:< 1.3f} {element:<2.2s}\n"), - 'REMARK': "REMARK {0}\n", - 'TITLE': "TITLE {0}\n", - 'CRYST1': ("CRYST1{box[0]:9.3f}{box[1]:9.3f}{box[2]:9.3f}" - "{ang[0]:7.2f}{ang[1]:7.2f}{ang[2]:7.2f} " - "{spacegroup:<11s}{zvalue:4d}\n"), + "ATOM": ( + "ATOM {serial:5d} {name:<4.4s} {resName:<4.4s}" + "{chainID:1.1s}{resSeq:4d}{iCode:1.1s}" + " {pos[0]:8.3f}{pos[1]:8.3f}{pos[2]:8.3f}{occupancy:6.2f}" + "{tempFactor:6.2f} {charge:< 1.3f} {element:<2.2s}\n" + ), + "REMARK": "REMARK {0}\n", + "TITLE": "TITLE {0}\n", + "CRYST1": ( + "CRYST1{box[0]:9.3f}{box[1]:9.3f}{box[2]:9.3f}" + "{ang[0]:7.2f}{ang[1]:7.2f}{ang[2]:7.2f} " + "{spacegroup:<11s}{zvalue:4d}\n" + ), } - format = 'PDBQT' - units = {'time': None, 'length': 'Angstrom'} + format = "PDBQT" + units = {"time": None, "length": "Angstrom"} pdb_coor_limits = {"min": -999.9995, "max": 9999.9995} def __init__(self, filename, **kwargs): - self.filename = util.filename(filename, ext='pdbqt', keep=True) - self.pdb = util.anyopen(self.filename, 'wt') + self.filename = util.filename(filename, ext="pdbqt", keep=True) + self.pdb = util.anyopen(self.filename, "wt") def close(self): self.pdb.close() @@ -262,21 +269,23 @@ def write(self, selection, frame=None): frame = 0 # should catch cases when we are analyzing a single PDB (?) atoms = selection.atoms # make sure to use atoms (Issue 46) - coor = atoms.positions # can write from selection == Universe (Issue 49) + coor = ( + atoms.positions + ) # can write from selection == Universe (Issue 49) # Check attributes attrs = {} missing_topology = [] for attr, dflt in ( - ('altLocs', ' '), - ('charges', 0.0), - ('icodes', ' '), - ('names', 'X'), - ('occupancies', 1.0), - ('resids', 1), - ('resnames', 'UNK'), - ('tempfactors', 0.0), - ('types', ' '), + ("altLocs", " "), + ("charges", 0.0), + ("icodes", " "), + ("names", "X"), + ("occupancies", 1.0), + ("resids", 1), + ("resnames", "UNK"), + ("tempfactors", 0.0), + ("types", " "), ): try: attrs[attr] = getattr(atoms, attr) @@ -285,18 +294,19 @@ def write(self, selection, frame=None): missing_topology.append(attr) # Order of preference: chainids -> segids -> blank string try: - attrs['chainids'] = atoms.chainids + attrs["chainids"] = atoms.chainids except AttributeError: try: - attrs['chainids'] = atoms.segids + attrs["chainids"] = atoms.segids except AttributeError: - attrs['chainids'] = itertools.cycle((' ',)) - missing_topology.append('chainids') + attrs["chainids"] = itertools.cycle((" ",)) + missing_topology.append("chainids") if missing_topology: warnings.warn( "Supplied AtomGroup was missing the following attributes: " "{miss}. These will be written with default values. " - "".format(miss=', '.join(missing_topology))) + "".format(miss=", ".join(missing_topology)) + ) # check if any coordinates are illegal (coordinates are already # in Angstroem per package default) @@ -310,28 +320,53 @@ def write(self, selection, frame=None): raise ValueError( "PDB files must have coordinate values between {0:.3f}" " and {1:.3f} Angstroem: No file was written." - "".format(self.pdb_coor_limits["min"], - self.pdb_coor_limits["max"])) + "".format( + self.pdb_coor_limits["min"], self.pdb_coor_limits["max"] + ) + ) # Write title record # http://www.wwpdb.org/documentation/file-format-content/format32/sect2.html line = "FRAME " + str(frame) + " FROM " + str(u.trajectory.filename) - self.pdb.write(self.fmt['TITLE'].format(line)) + self.pdb.write(self.fmt["TITLE"].format(line)) # Write CRYST1 record # http://www.wwpdb.org/documentation/file-format-content/format32/sect8.html box = self.convert_dimensions_to_unitcell(u.trajectory.ts) - self.pdb.write(self.fmt['CRYST1'].format(box=box[:3], ang=box[3:], - spacegroup='P 1', zvalue=1)) + self.pdb.write( + self.fmt["CRYST1"].format( + box=box[:3], ang=box[3:], spacegroup="P 1", zvalue=1 + ) + ) # Write atom records # http://www.wwpdb.org/documentation/file-format-content/format32/sect9.html - for serial, (pos, name, resname, chainid, resid, icode, - occupancy, tempfactor, charge, element) in enumerate( - zip(coor, attrs['names'], attrs['resnames'], attrs['chainids'], - attrs['resids'], attrs['icodes'], attrs['occupancies'], - attrs['tempfactors'], attrs['charges'], attrs['types']), - start=1): + for serial, ( + pos, + name, + resname, + chainid, + resid, + icode, + occupancy, + tempfactor, + charge, + element, + ) in enumerate( + zip( + coor, + attrs["names"], + attrs["resnames"], + attrs["chainids"], + attrs["resids"], + attrs["icodes"], + attrs["occupancies"], + attrs["tempfactors"], + attrs["charges"], + attrs["types"], + ), + start=1, + ): serial = util.ltruncate_int(serial, 5) # check for overflow here? resid = util.ltruncate_int(resid, 4) name = name[:4] @@ -339,18 +374,20 @@ def write(self, selection, frame=None): name = " " + name # customary to start in column 14 chainid = chainid.strip()[-1:] # take the last character - self.pdb.write(self.fmt['ATOM'].format( - serial=serial, - name=name, - resName=resname, - chainID=chainid, - resSeq=resid, - iCode=icode, - pos=pos, - occupancy=occupancy, - tempFactor=tempfactor, - charge=charge, - element=element, - )) + self.pdb.write( + self.fmt["ATOM"].format( + serial=serial, + name=name, + resName=resname, + chainID=chainid, + resSeq=resid, + iCode=icode, + pos=pos, + occupancy=occupancy, + tempFactor=tempfactor, + charge=charge, + element=element, + ) + ) self.close() diff --git a/package/MDAnalysis/coordinates/PQR.py b/package/MDAnalysis/coordinates/PQR.py index c93d783dc68..18e1ad4369f 100644 --- a/package/MDAnalysis/coordinates/PQR.py +++ b/package/MDAnalysis/coordinates/PQR.py @@ -122,24 +122,26 @@ class PQRReader(base.SingleFrameReaderBase): .. versionchanged:: 0.11.0 Frames now 0-based instead of 1-based """ - format = 'PQR' - units = {'time': None, 'length': 'Angstrom'} + + format = "PQR" + units = {"time": None, "length": "Angstrom"} # how to slice fields[x:y] to grab coordinates _SLICE_INDICES = { - 'ORIGINAL': (-5, -2), - 'NO_CHAINID': (-5, -2), - 'GROMACS': (-6, -3), + "ORIGINAL": (-5, -2), + "NO_CHAINID": (-5, -2), + "GROMACS": (-6, -3), } def _read_first_frame(self): from ..topology.PQRParser import PQRParser + flavour = None coords = [] with util.openany(self.filename) as pqrfile: for line in pqrfile: - if line.startswith(('ATOM', 'HETATM')): + if line.startswith(("ATOM", "HETATM")): if flavour is None: flavour = PQRParser.guess_flavour(line) x, y = self._SLICE_INDICES[flavour] @@ -149,8 +151,7 @@ def _read_first_frame(self): coords.append(fields[x:y]) self.n_atoms = len(coords) - self.ts = self._Timestep.from_coordinates( - coords, **self._ts_kwargs) + self.ts = self._Timestep.from_coordinates(coords, **self._ts_kwargs) self.ts.frame = 0 # 0-based frame number if self.convert_units: # in-place ! @@ -192,13 +193,16 @@ class PQRWriter(base.WriterBase): Files are now written in `wt` mode, and keep extensions, allowing for files to be written under compressed formats """ - format = 'PQR' - units = {'time': None, 'length': 'Angstrom'} + + format = "PQR" + units = {"time": None, "length": "Angstrom"} # serial, atomName, residueName, chainID, residueNumber, XYZ, charge, radius - fmt_ATOM = ("ATOM {serial:6d} {name:<4} {resname:<3} {chainid:1.1}" - " {resid:4d} {pos[0]:-8.3f} {pos[1]:-8.3f}" - " {pos[2]:-8.3f} {charge:-7.4f} {radius:6.4f}\n") + fmt_ATOM = ( + "ATOM {serial:6d} {name:<4} {resname:<3} {chainid:1.1}" + " {resid:4d} {pos[0]:-8.3f} {pos[1]:-8.3f}" + " {pos[2]:-8.3f} {charge:-7.4f} {radius:6.4f}\n" + ) fmt_remark = "REMARK {0} {1}\n" def __init__(self, filename, convert_units=True, **kwargs): @@ -214,9 +218,11 @@ def __init__(self, filename, convert_units=True, **kwargs): remark lines (list of strings) or single string to be added to the top of the PQR file """ - self.filename = util.filename(filename, ext='pqr', keep=True) - self.convert_units = convert_units # convert length and time to base units - self.remarks = kwargs.pop('remarks', "PQR file written by MDAnalysis") + self.filename = util.filename(filename, ext="pqr", keep=True) + self.convert_units = ( + convert_units # convert length and time to base units + ) + self.remarks = kwargs.pop("remarks", "PQR file written by MDAnalysis") def write(self, selection, frame=None): """Write selection at current trajectory frame to file. @@ -250,9 +256,13 @@ def write(self, selection, frame=None): frame = 0 # should catch cases when we are analyzing a single frame(?) atoms = selection.atoms # make sure to use atoms (Issue 46) - coordinates = atoms.positions # can write from selection == Universe (Issue 49) + coordinates = ( + atoms.positions + ) # can write from selection == Universe (Issue 49) if self.convert_units: - self.convert_pos_to_native(coordinates) # inplace because coordinates is already a copy + self.convert_pos_to_native( + coordinates + ) # inplace because coordinates is already a copy # Check atom attributes # required: @@ -266,11 +276,11 @@ def write(self, selection, frame=None): attrs = {} missing_topology = [] for attr, dflt in ( - ('names', itertools.cycle(('X',))), - ('resnames', itertools.cycle(('UNK',))), - ('resids', itertools.cycle((1,))), - ('charges', itertools.cycle((0.0,))), - ('radii', itertools.cycle((1.0,))), + ("names", itertools.cycle(("X",))), + ("resnames", itertools.cycle(("UNK",))), + ("resids", itertools.cycle((1,))), + ("charges", itertools.cycle((0.0,))), + ("radii", itertools.cycle((1.0,))), ): try: attrs[attr] = getattr(atoms, attr) @@ -283,16 +293,16 @@ def write(self, selection, frame=None): # if neither, use ' ' # if 'SYSTEM', use ' ' try: - attrs['chainids'] = atoms.chainids + attrs["chainids"] = atoms.chainids except AttributeError: try: - attrs['chainids'] = atoms.segids + attrs["chainids"] = atoms.segids except AttributeError: pass - if not 'chainids' in attrs or all(attrs['chainids'] == 'SYSTEM'): - attrs['chainids'] = itertools.cycle((' ',)) + if not "chainids" in attrs or all(attrs["chainids"] == "SYSTEM"): + attrs["chainids"] = itertools.cycle((" ",)) - if 'charges' in missing_topology: + if "charges" in missing_topology: total_charge = 0.0 else: total_charge = atoms.total_charge() @@ -301,28 +311,62 @@ def write(self, selection, frame=None): warnings.warn( "Supplied AtomGroup was missing the following attributes: " "{miss}. These will be written with default values. " - "".format(miss=', '.join(missing_topology))) + "".format(miss=", ".join(missing_topology)) + ) - with util.openany(self.filename, 'wt') as pqrfile: + with util.openany(self.filename, "wt") as pqrfile: # Header / Remarks # The *remarknumber* is typically 1 but :program:`pdb2pgr` # also uses 6 for the total charge and 5 for warnings. for rem in util.asiterable(self.remarks): pqrfile.write(self.fmt_remark.format(rem, 1)) - pqrfile.write(self.fmt_remark.format( - "Input: frame {0} of {1}".format(frame, u.trajectory.filename), - 5)) - pqrfile.write(self.fmt_remark.format( - "total charge: {0:+8.4f} e".format(total_charge), 6)) + pqrfile.write( + self.fmt_remark.format( + "Input: frame {0} of {1}".format( + frame, u.trajectory.filename + ), + 5, + ) + ) + pqrfile.write( + self.fmt_remark.format( + "total charge: {0:+8.4f} e".format(total_charge), 6 + ) + ) # Atom descriptions and coords - for atom_index, (pos, name, resname, chainid, resid, charge, radius) in enumerate(zip( - coordinates, attrs['names'], attrs['resnames'], attrs['chainids'], - attrs['resids'], attrs['charges'], attrs['radii']), start=1): + for atom_index, ( + pos, + name, + resname, + chainid, + resid, + charge, + radius, + ) in enumerate( + zip( + coordinates, + attrs["names"], + attrs["resnames"], + attrs["chainids"], + attrs["resids"], + attrs["charges"], + attrs["radii"], + ), + start=1, + ): # pad so that only 4-letter atoms are left-aligned name = " " + name if len(name) < 4 else name - pqrfile.write(self.fmt_ATOM.format( - serial=atom_index, name=name, resname=resname, - chainid=chainid, resid=resid, pos=pos, charge=charge, - radius=radius)) + pqrfile.write( + self.fmt_ATOM.format( + serial=atom_index, + name=name, + resname=resname, + chainid=chainid, + resid=resid, + pos=pos, + charge=charge, + radius=radius, + ) + ) diff --git a/package/MDAnalysis/coordinates/ParmEd.py b/package/MDAnalysis/coordinates/ParmEd.py index af29340a5d2..982231c11a6 100644 --- a/package/MDAnalysis/coordinates/ParmEd.py +++ b/package/MDAnalysis/coordinates/ParmEd.py @@ -21,13 +21,17 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # import warnings -from ..converters.ParmEd import (ParmEdConverter, ParmEdReader, - get_indices_from_subset, MDA2PMD,) +from ..converters.ParmEd import ( + ParmEdConverter, + ParmEdReader, + get_indices_from_subset, + MDA2PMD, +) warnings.warn( "This module is deprecated as of MDAnalysis version 2.0.0. " "It will be removed in MDAnalysis version 3.0.0. " "Please import the ParmEd classes from MDAnalysis.converters instead.", - category=DeprecationWarning + category=DeprecationWarning, ) diff --git a/package/MDAnalysis/coordinates/TNG.py b/package/MDAnalysis/coordinates/TNG.py index a5d868360f0..44c6890dcf7 100644 --- a/package/MDAnalysis/coordinates/TNG.py +++ b/package/MDAnalysis/coordinates/TNG.py @@ -148,7 +148,9 @@ class TNGReader(base.ReaderBase): _forces_blockname, ] - @due.dcite(Doi("10.1002/jcc.23495"), description="The TNG paper", path=__name__) + @due.dcite( + Doi("10.1002/jcc.23495"), description="The TNG paper", path=__name__ + ) @store_init_arguments def __init__(self, filename: str, convert_units: bool = True, **kwargs): """Initialize a TNG trajectory @@ -162,7 +164,9 @@ def __init__(self, filename: str, convert_units: bool = True, **kwargs): """ if not HAS_PYTNG: - raise ImportError("TNGReader: To read TNG files please install pytng") + raise ImportError( + "TNGReader: To read TNG files please install pytng" + ) super(TNGReader, self).__init__(filename, **kwargs) @@ -193,27 +197,35 @@ def __init__(self, filename: str, convert_units: bool = True, **kwargs): self._has_positions = self._positions_blockname in self._block_names if self._has_positions: self._special_block_present[self._positions_blockname] = True - self.ts.positions = self._file_iterator.make_ndarray_for_block_from_name( - self._positions_blockname + self.ts.positions = ( + self._file_iterator.make_ndarray_for_block_from_name( + self._positions_blockname + ) ) self._has_velocities = self._velocities_blockname in self._block_names if self._has_velocities: self._special_block_present[self._velocities_blockname] = True - self.ts.velocities = self._file_iterator.make_ndarray_for_block_from_name( - self._velocities_blockname + self.ts.velocities = ( + self._file_iterator.make_ndarray_for_block_from_name( + self._velocities_blockname + ) ) self._has_forces = self._forces_blockname in self._block_names if self._has_forces: self._special_block_present[self._forces_blockname] = True - self.ts.forces = self._file_iterator.make_ndarray_for_block_from_name( - self._forces_blockname + self.ts.forces = ( + self._file_iterator.make_ndarray_for_block_from_name( + self._forces_blockname + ) ) # check for any additional blocks that will be read into ts.data self._additional_blocks = [ - block for block in self._block_names if block not in self._special_blocks + block + for block in self._block_names + if block not in self._special_blocks ] self._check_strides_and_frames() self._frame = 0 @@ -265,7 +277,9 @@ def _check_strides_and_frames(self): " It will not be read" ) else: - self._additional_blocks_to_read.append(block) # pragma: no cover + self._additional_blocks_to_read.append( + block + ) # pragma: no cover else: self._additional_blocks_to_read.append(block) @@ -289,7 +303,9 @@ def parse_n_atoms(filename: str) -> int: """ if not HAS_PYTNG: - raise ImportError("TNGReader: To read TNG files please install pytng") + raise ImportError( + "TNGReader: To read TNG files please install pytng" + ) with pytng.TNGFileIterator(filename, "r") as tng: n_atoms = tng.n_atoms return n_atoms @@ -471,7 +487,9 @@ def _frame_to_ts( add_block_stride = self._block_strides[block] # check we are on stride for our block if not (add_block_stride % self._global_stride): - block_data = self._file_iterator.make_ndarray_for_block_from_name(block) + block_data = ( + self._file_iterator.make_ndarray_for_block_from_name(block) + ) # additional blocks read into ts.data dictionary ts.data[block] = curr_step.get_blockid( self._block_dictionary[block], block_data diff --git a/package/MDAnalysis/coordinates/TRC.py b/package/MDAnalysis/coordinates/TRC.py index 5d92db1af8c..da431448704 100644 --- a/package/MDAnalysis/coordinates/TRC.py +++ b/package/MDAnalysis/coordinates/TRC.py @@ -259,7 +259,9 @@ def _read_traj_properties(self): traj_properties["l_blockstart_offset"] = l_blockstart_offset if len(l_timestep_timevalues) >= 2: - traj_properties["dt"] = l_timestep_timevalues[1] - l_timestep_timevalues[0] + traj_properties["dt"] = ( + l_timestep_timevalues[1] - l_timestep_timevalues[0] + ) else: traj_properties["dt"] = 0 warnings.warn( @@ -296,7 +298,9 @@ def _read_GROMOS11_trajectory(self): tmp_buf.append(coords_str.split()) if np.array(tmp_buf).shape[0] == self.n_atoms: - frameDat["positions"] = np.asarray(tmp_buf, dtype=np.float64) + frameDat["positions"] = np.asarray( + tmp_buf, dtype=np.float64 + ) else: raise ValueError( "The trajectory contains the wrong number of atoms!" @@ -322,13 +326,19 @@ def _read_GROMOS11_trajectory(self): self.periodic = True gb_line3 = f.readline().split() - if np.sum(np.abs(np.array(gb_line3).astype(np.float64))) > 1e-10: + if ( + np.sum(np.abs(np.array(gb_line3).astype(np.float64))) + > 1e-10 + ): raise ValueError( "This reader doesnt't support a shifted origin!" ) gb_line4 = f.readline().split() - if np.sum(np.abs(np.array(gb_line4).astype(np.float64))) > 1e-10: + if ( + np.sum(np.abs(np.array(gb_line4).astype(np.float64))) + > 1e-10 + ): raise ValueError( "This reader " "doesnt't support " @@ -345,12 +355,14 @@ def _read_GROMOS11_trajectory(self): break elif any( - non_supp_bn in line for non_supp_bn in TRCReader.NOT_SUPPORTED_BLOCKS + non_supp_bn in line + for non_supp_bn in TRCReader.NOT_SUPPORTED_BLOCKS ): for non_supp_bn in TRCReader.NOT_SUPPORTED_BLOCKS: if non_supp_bn == line.strip(): warnings.warn( - non_supp_bn + " block is not supported!", UserWarning + non_supp_bn + " block is not supported!", + UserWarning, ) return frameDat @@ -360,7 +372,9 @@ def _read_frame(self, i): self._frame = i - 1 # Move position in file just (-2 byte) before the start of the block - self.trcfile.seek(self.traj_properties["l_blockstart_offset"][i] - 2, 0) + self.trcfile.seek( + self.traj_properties["l_blockstart_offset"][i] - 2, 0 + ) return self._read_next_timestep() @@ -381,7 +395,9 @@ def _reopen(self): def open_trajectory(self): if self.trcfile is not None: - raise IOError(errno.EALREADY, "TRC file already opened", self.filename) + raise IOError( + errno.EALREADY, "TRC file already opened", self.filename + ) # Reload trajectory file self.trcfile = util.anyopen(self.filename) diff --git a/package/MDAnalysis/coordinates/core.py b/package/MDAnalysis/coordinates/core.py index fe87cd005a3..951ee3fd0af 100644 --- a/package/MDAnalysis/coordinates/core.py +++ b/package/MDAnalysis/coordinates/core.py @@ -73,15 +73,14 @@ def reader(filename, format=None, **kwargs): """ if isinstance(filename, tuple): - Reader = get_reader_for(filename[0], - format=filename[1]) + Reader = get_reader_for(filename[0], format=filename[1]) filename = filename[0] else: Reader = get_reader_for(filename, format=format) try: return Reader(filename, **kwargs) except ValueError: - errmsg = f'Unable to read {filename} with {Reader}.' + errmsg = f"Unable to read {filename} with {Reader}." raise TypeError(errmsg) from None @@ -121,6 +120,9 @@ def writer(filename, n_atoms=None, **kwargs): Added `multiframe` keyword. See also :func:`get_writer_for`. """ - Writer = get_writer_for(filename, format=kwargs.pop('format', None), - multiframe=kwargs.pop('multiframe', None)) + Writer = get_writer_for( + filename, + format=kwargs.pop("format", None), + multiframe=kwargs.pop("multiframe", None), + ) return Writer(filename, n_atoms=n_atoms, **kwargs) diff --git a/package/MDAnalysis/coordinates/null.py b/package/MDAnalysis/coordinates/null.py index 71b490aad24..5b2f4f1a3fa 100644 --- a/package/MDAnalysis/coordinates/null.py +++ b/package/MDAnalysis/coordinates/null.py @@ -47,9 +47,10 @@ class NullWriter(base.WriterBase): a Writer but ignores all input. It can be used in order to suppress output. """ - format = 'NULL' + + format = "NULL" multiframe = True - units = {'time': 'ps', 'length': 'Angstrom'} + units = {"time": "ps", "length": "Angstrom"} def __init__(self, filename, **kwargs): pass diff --git a/package/pyproject.toml b/package/pyproject.toml index 413e4b68585..19d07228e7f 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -137,6 +137,7 @@ tables\.py | MDAnalysis/transformations/.*\.py | MDAnalysis/guesser/.*\.py | MDAnalysis/converters/.*\.py +| MDAnalysis/coordinates/.*\.py | MDAnalysis/tests/.*\.py | MDAnalysis/selections/.*\.py ) @@ -186,6 +187,7 @@ __pycache__ | MDAnalysis/coordinates/chemfiles\.py | MDAnalysis/coordinates/memory\.py | MDAnalysis/core/universe\.py +| .*\.pyx ) ''' required-version = '24' diff --git a/testsuite/MDAnalysisTests/coordinates/base.py b/testsuite/MDAnalysisTests/coordinates/base.py index dafeeedca35..1568dfab87a 100644 --- a/testsuite/MDAnalysisTests/coordinates/base.py +++ b/testsuite/MDAnalysisTests/coordinates/base.py @@ -26,8 +26,12 @@ import numpy as np import pytest from unittest import TestCase -from numpy.testing import (assert_equal, assert_almost_equal, - assert_array_almost_equal, assert_allclose) +from numpy.testing import ( + assert_equal, + assert_almost_equal, + assert_array_almost_equal, + assert_allclose, +) import MDAnalysis as mda from MDAnalysis.coordinates.timestep import Timestep @@ -48,41 +52,61 @@ def tearDown(self): def test_load_file(self): U = self.universe - assert_equal(len(U.atoms), self.ref_n_atoms, - "load Universe from file {0!s}".format(U.trajectory.filename)) - assert_equal(U.atoms.select_atoms('resid 150 and name HA2').atoms[0], - U.atoms[self.ref_E151HA2_index], "Atom selections") + assert_equal( + len(U.atoms), + self.ref_n_atoms, + "load Universe from file {0!s}".format(U.trajectory.filename), + ) + assert_equal( + U.atoms.select_atoms("resid 150 and name HA2").atoms[0], + U.atoms[self.ref_E151HA2_index], + "Atom selections", + ) def test_n_atoms(self): - assert_equal(self.universe.trajectory.n_atoms, self.ref_n_atoms, - "wrong number of atoms") + assert_equal( + self.universe.trajectory.n_atoms, + self.ref_n_atoms, + "wrong number of atoms", + ) def test_numres(self): - assert_equal(self.universe.atoms.n_residues, 214, - "wrong number of residues") + assert_equal( + self.universe.atoms.n_residues, 214, "wrong number of residues" + ) def test_n_frames(self): - assert_equal(self.universe.trajectory.n_frames, 1, - "wrong number of frames in pdb") + assert_equal( + self.universe.trajectory.n_frames, + 1, + "wrong number of frames in pdb", + ) def test_time(self): - assert_equal(self.universe.trajectory.time, 0.0, - "wrong time of the frame") + assert_equal( + self.universe.trajectory.time, 0.0, "wrong time of the frame" + ) def test_frame(self): assert_equal( - self.universe.trajectory.frame, 0, + self.universe.trajectory.frame, + 0, "wrong frame number (0-based, should be 0 for single frame " - "readers)") + "readers)", + ) def test_frame_index_0(self): self.universe.trajectory[0] - assert_equal(self.universe.trajectory.ts.frame, 0, - "frame number for frame index 0 should be 0") + assert_equal( + self.universe.trajectory.ts.frame, + 0, + "frame number for frame index 0 should be 0", + ) def test_frame_index_1_raises_IndexError(self): def go_to_2(traj=self.universe.trajectory): traj[1] + with pytest.raises(IndexError): go_to_2() @@ -92,21 +116,25 @@ def test_dt(self): assert_equal(1.0, self.universe.trajectory.dt) def test_coordinates(self): - A10CA = self.universe.select_atoms('name CA')[10] + A10CA = self.universe.select_atoms("name CA")[10] # restrict accuracy to maximum in PDB files (3 decimals) - assert_almost_equal(A10CA.position, - self.ref_coordinates['A10CA'], - 3, - err_msg="wrong coordinates for A10:CA") + assert_almost_equal( + A10CA.position, + self.ref_coordinates["A10CA"], + 3, + err_msg="wrong coordinates for A10:CA", + ) def test_distances(self): - NTERM = self.universe.select_atoms('name N')[0] - CTERM = self.universe.select_atoms('name C')[-1] + NTERM = self.universe.select_atoms("name N")[0] + CTERM = self.universe.select_atoms("name C")[-1] d = mda.lib.mdamath.norm(NTERM.position - CTERM.position) - assert_almost_equal(d, - self.ref_distances['endtoend'], - self.prec, - err_msg="distance between M1:N and G214:C") + assert_almost_equal( + d, + self.ref_distances["endtoend"], + self.prec, + err_msg="distance between M1:N and G214:C", + ) def test_full_slice(self): trj_iter = self.universe.trajectory[:] @@ -124,15 +152,24 @@ def test_pickle_singleframe_reader(self): reader_p = pickle.loads(pickle.dumps(reader)) reader_p_p = pickle.loads(pickle.dumps(reader_p)) assert_equal(len(reader), len(reader_p)) - assert_equal(reader.ts, reader_p.ts, - "Single-frame timestep is changed after pickling") + assert_equal( + reader.ts, + reader_p.ts, + "Single-frame timestep is changed after pickling", + ) assert_equal(len(reader), len(reader_p_p)) - assert_equal(reader.ts, reader_p_p.ts, - "Single-frame timestep is changed after double pickling") + assert_equal( + reader.ts, + reader_p_p.ts, + "Single-frame timestep is changed after double pickling", + ) reader.ts.positions[0] = np.array([1, 2, 3]) reader_p = pickle.loads(pickle.dumps(reader)) - assert_equal(reader.ts, reader_p.ts, - "Modification of ts not preserved after serialization") + assert_equal( + reader.ts, + reader_p.ts, + "Modification of ts not preserved after serialization", + ) class BaseReference(object): @@ -149,58 +186,65 @@ def __init__(self): self.aux_lowf = AUX_XVG_LOWF # test auxiliary with lower frequency self.aux_lowf_dt = 2 # has steps at 0ps, 2ps, 4ps # representative data for each trajectory frame, assuming 'closest' option - self.aux_lowf_data = [[2 ** 0], # frame 0 = 0ps = step 0 - [np.nan], # frame 1 = 1ps = no step - [2 ** 1], # frame 2 = 2ps = step 1 - [np.nan], # frame 3 = 3ps = no step - [2 ** 2], # frame 4 = 4ps = step 2 - ] + self.aux_lowf_data = [ + [2**0], # frame 0 = 0ps = step 0 + [np.nan], # frame 1 = 1ps = no step + [2**1], # frame 2 = 2ps = step 1 + [np.nan], # frame 3 = 3ps = no step + [2**2], # frame 4 = 4ps = step 2 + ] self.aux_lowf_frames_with_steps = [0, 2, 4] # trajectory frames with # corresponding auxiliary steps self.aux_highf = AUX_XVG_HIGHF # test auxiliary with higher frequency self.aux_highf_dt = 0.5 # has steps at 0, 0.5, 1, ... 3.5, 4ps - self.aux_highf_data = [[2 ** 0], # frame 0 = 0ps = step 0 - [2 ** 2], # frame 1 = 1ps = step 2 - [2 ** 4], # frame 2 = 2ps = step 4 - [2 ** 6], # frame 3 = 3ps = step 6 - [2 ** 8], # frame 4 = 4ps = step 8 - ] + self.aux_highf_data = [ + [2**0], # frame 0 = 0ps = step 0 + [2**2], # frame 1 = 1ps = step 2 + [2**4], # frame 2 = 2ps = step 4 + [2**6], # frame 3 = 3ps = step 6 + [2**8], # frame 4 = 4ps = step 8 + ] self.aux_highf_n_steps = 10 - self.aux_highf_all_data = [[2 ** i] for i in range(self.aux_highf_n_steps)] + self.aux_highf_all_data = [ + [2**i] for i in range(self.aux_highf_n_steps) + ] self.aux_offset_by = 0.25 self.first_frame = Timestep(self.n_atoms) - self.first_frame.positions = np.arange( - 3 * self.n_atoms).reshape(self.n_atoms, 3) + self.first_frame.positions = np.arange(3 * self.n_atoms).reshape( + self.n_atoms, 3 + ) self.first_frame.frame = 0 self.first_frame.aux.lowf = self.aux_lowf_data[0] self.first_frame.aux.highf = self.aux_highf_data[0] self.second_frame = self.first_frame.copy() - self.second_frame.positions = 2 ** 1 * self.first_frame.positions + self.second_frame.positions = 2**1 * self.first_frame.positions self.second_frame.frame = 1 self.second_frame.aux.lowf = self.aux_lowf_data[1] self.second_frame.aux.highf = self.aux_highf_data[1] self.last_frame = self.first_frame.copy() - self.last_frame.positions = 2 ** 4 * self.first_frame.positions + self.last_frame.positions = 2**4 * self.first_frame.positions self.last_frame.frame = self.n_frames - 1 self.last_frame.aux.lowf = self.aux_lowf_data[-1] self.last_frame.aux.highf = self.aux_highf_data[-1] # remember frames are 0 indexed self.jump_to_frame = self.first_frame.copy() - self.jump_to_frame.positions = 2 ** 3 * self.first_frame.positions + self.jump_to_frame.positions = 2**3 * self.first_frame.positions self.jump_to_frame.frame = 3 self.jump_to_frame.aux.lowf = self.aux_lowf_data[3] self.jump_to_frame.aux.highf = self.aux_highf_data[3] - self.dimensions = np.array([81.1, 82.2, 83.3, 75, 80, 85], - dtype=np.float32) - self.dimensions_second_frame = np.array([82.1, 83.2, 84.3, 75.1, 80.1, - 85.1], dtype=np.float32) + self.dimensions = np.array( + [81.1, 82.2, 83.3, 75, 80, 85], dtype=np.float32 + ) + self.dimensions_second_frame = np.array( + [82.1, 83.2, 84.3, 75.1, 80.1, 85.1], dtype=np.float32 + ) self.volume = mda.lib.mdamath.box_volume(self.dimensions) self.time = 0 self.dt = 1 @@ -208,7 +252,7 @@ def __init__(self): def iter_ts(self, i): ts = self.first_frame.copy() - ts.positions = 2 ** i * self.first_frame.positions + ts.positions = 2**i * self.first_frame.positions ts.time = i ts.frame = i ts.aux.lowf = np.array(self.aux_lowf_data[i]) @@ -221,15 +265,29 @@ class BaseReaderTest(object): @pytest.fixture() def reader(ref): reader = ref.reader(ref.trajectory) - reader.add_auxiliary('lowf', ref.aux_lowf, dt=ref.aux_lowf_dt, initial_time=0, time_selector=None) - reader.add_auxiliary('highf', ref.aux_highf, dt=ref.aux_highf_dt, initial_time=0, time_selector=None) + reader.add_auxiliary( + "lowf", + ref.aux_lowf, + dt=ref.aux_lowf_dt, + initial_time=0, + time_selector=None, + ) + reader.add_auxiliary( + "highf", + ref.aux_highf, + dt=ref.aux_highf_dt, + initial_time=0, + time_selector=None, + ) return reader @staticmethod @pytest.fixture() def transformed(ref): transformed = ref.reader(ref.trajectory) - transformed.add_transformations(translate([1,1,1]), translate([0,0,0.33])) + transformed.add_transformations( + translate([1, 1, 1]), translate([0, 0, 0.33]) + ) return transformed def test_n_atoms(self, ref, reader): @@ -240,8 +298,9 @@ def test_n_frames(self, ref, reader): def test_first_frame(self, ref, reader): reader.rewind() - assert_timestep_almost_equal(reader.ts, ref.first_frame, - decimal=ref.prec) + assert_timestep_almost_equal( + reader.ts, ref.first_frame, decimal=ref.prec + ) def test_double_close(self, reader): reader.close() @@ -250,14 +309,14 @@ def test_double_close(self, reader): def test_get_writer_1(self, ref, reader, tmpdir): with tmpdir.as_cwd(): - outfile = 'test-writer.' + ref.ext + outfile = "test-writer." + ref.ext with reader.Writer(outfile) as W: assert_equal(isinstance(W, ref.writer), True) assert_equal(W.n_atoms, reader.n_atoms) def test_get_writer_2(self, ref, reader, tmpdir): with tmpdir.as_cwd(): - outfile = 'test-writer.' + ref.ext + outfile = "test-writer." + ref.ext with reader.Writer(outfile, n_atoms=100) as W: assert_equal(isinstance(W, ref.writer), True) assert_equal(W.n_atoms, 100) @@ -276,9 +335,9 @@ def test_first_dimensions(self, ref, reader): if ref.dimensions is None: assert reader.ts.dimensions is None else: - assert_array_almost_equal(reader.ts.dimensions, - ref.dimensions, - decimal=ref.prec) + assert_array_almost_equal( + reader.ts.dimensions, ref.dimensions, decimal=ref.prec + ) def test_changing_dimensions(self, ref, reader): if ref.changing_dimensions: @@ -286,16 +345,18 @@ def test_changing_dimensions(self, ref, reader): if ref.dimensions is None: assert reader.ts.dimensions is None else: - assert_array_almost_equal(reader.ts.dimensions, - ref.dimensions, - decimal=ref.prec) + assert_array_almost_equal( + reader.ts.dimensions, ref.dimensions, decimal=ref.prec + ) reader[1] if ref.dimensions_second_frame is None: assert reader.ts.dimensions is None else: - assert_array_almost_equal(reader.ts.dimensions, - ref.dimensions_second_frame, - decimal=ref.prec) + assert_array_almost_equal( + reader.ts.dimensions, + ref.dimensions_second_frame, + decimal=ref.prec, + ) def test_volume(self, ref, reader): reader.rewind() @@ -306,47 +367,56 @@ def test_volume(self, ref, reader): def test_iter(self, ref, reader): for i, ts in enumerate(reader): - assert_timestep_almost_equal(ts, ref.iter_ts(i), - decimal=ref.prec) + assert_timestep_almost_equal(ts, ref.iter_ts(i), decimal=ref.prec) def test_add_same_auxname_raises_ValueError(self, ref, reader): with pytest.raises(ValueError): - reader.add_auxiliary('lowf', ref.aux_lowf) + reader.add_auxiliary("lowf", ref.aux_lowf) def test_remove_auxiliary(self, reader): - reader.remove_auxiliary('lowf') + reader.remove_auxiliary("lowf") with pytest.raises(AttributeError): - getattr(reader._auxs, 'lowf') + getattr(reader._auxs, "lowf") with pytest.raises(AttributeError): - getattr(reader.ts.aux, 'lowf') + getattr(reader.ts.aux, "lowf") def test_remove_nonexistant_auxiliary_raises_ValueError(self, reader): with pytest.raises(ValueError): - reader.remove_auxiliary('nonexistant') + reader.remove_auxiliary("nonexistant") def test_iter_auxiliary(self, ref, reader): # should go through all steps in 'highf' - for i, auxstep in enumerate(reader.iter_auxiliary('highf')): - assert_almost_equal(auxstep.data, ref.aux_highf_all_data[i], - err_msg="Auxiliary data does not match for " - "step {}".format(i)) + for i, auxstep in enumerate(reader.iter_auxiliary("highf")): + assert_almost_equal( + auxstep.data, + ref.aux_highf_all_data[i], + err_msg="Auxiliary data does not match for " + "step {}".format(i), + ) def test_get_aux_attribute(self, ref, reader): - assert_equal(reader.get_aux_attribute('lowf', 'dt'), - ref.aux_lowf_dt) + assert_equal(reader.get_aux_attribute("lowf", "dt"), ref.aux_lowf_dt) def test_iter_as_aux_cutoff(self, ref, reader): # load an auxiliary with the same dt but offset from trajectory, and a # cutoff of 0 - reader.add_auxiliary('offset', ref.aux_lowf, - dt=ref.dt, time_selector=None, - initial_time=ref.aux_offset_by, - cutoff=0) + reader.add_auxiliary( + "offset", + ref.aux_lowf, + dt=ref.dt, + time_selector=None, + initial_time=ref.aux_offset_by, + cutoff=0, + ) # no auxiliary steps will fall within the cutoff for any frame, so # iterating using iter_as_aux should give us nothing - num_frames = len([i for i in reader.iter_as_aux('offset')]) - assert_equal(num_frames, 0, "iter_as_aux should iterate over 0 frames," - " not {}".format(num_frames)) + num_frames = len([i for i in reader.iter_as_aux("offset")]) + assert_equal( + num_frames, + 0, + "iter_as_aux should iterate over 0 frames," + " not {}".format(num_frames), + ) def test_reload_auxiliaries_from_description(self, ref, reader): # get auxiliary desscriptions form existing reader @@ -357,12 +427,18 @@ def test_reload_auxiliaries_from_description(self, ref, reader): for aux in descriptions: reader.add_auxiliary(**aux) # should have the same number of auxiliaries - assert_equal(reader.aux_list, reader.aux_list, - 'Number of auxiliaries does not match') + assert_equal( + reader.aux_list, + reader.aux_list, + "Number of auxiliaries does not match", + ) # each auxiliary should be the same for auxname in reader.aux_list: - assert_equal(reader._auxs[auxname], reader._auxs[auxname], - 'AuxReaders do not match') + assert_equal( + reader._auxs[auxname], + reader._auxs[auxname], + "AuxReaders do not match", + ) def test_stop_iter(self, reader): # reset to 0 @@ -373,92 +449,122 @@ def test_stop_iter(self, reader): def test_transformations_iter(self, ref, transformed): # test one iteration and see if the transformations are applied - v1 = np.float32((1,1,1)) - v2 = np.float32((0,0,0.33)) + v1 = np.float32((1, 1, 1)) + v2 = np.float32((0, 0, 0.33)) for i, ts in enumerate(transformed): idealcoords = ref.iter_ts(i).positions + v1 + v2 - assert_array_almost_equal(ts.positions, idealcoords, decimal=ref.prec) + assert_array_almost_equal( + ts.positions, idealcoords, decimal=ref.prec + ) def test_transformations_2iter(self, ref, transformed): # Are the transformations applied and # are the coordinates "overtransformed"? - v1 = np.float32((1,1,1)) - v2 = np.float32((0,0,0.33)) - idealcoords=[] + v1 = np.float32((1, 1, 1)) + v2 = np.float32((0, 0, 0.33)) + idealcoords = [] for i, ts in enumerate(transformed): idealcoords.append(ref.iter_ts(i).positions + v1 + v2) - assert_array_almost_equal(ts.positions, idealcoords[i], decimal=ref.prec) + assert_array_almost_equal( + ts.positions, idealcoords[i], decimal=ref.prec + ) for i, ts in enumerate(transformed): assert_almost_equal(ts.positions, idealcoords[i], decimal=ref.prec) def test_transformations_slice(self, ref, transformed): # Are the transformations applied when iterating over a slice of the trajectory? - v1 = np.float32((1,1,1)) - v2 = np.float32((0,0,0.33)) - for i,ts in enumerate(transformed[2:3:1]): + v1 = np.float32((1, 1, 1)) + v2 = np.float32((0, 0, 0.33)) + for i, ts in enumerate(transformed[2:3:1]): idealcoords = ref.iter_ts(ts.frame).positions + v1 + v2 - assert_array_almost_equal(ts.positions, idealcoords, decimal = ref.prec) + assert_array_almost_equal( + ts.positions, idealcoords, decimal=ref.prec + ) def test_transformations_switch_frame(self, ref, transformed): # This test checks if the transformations are applied and if the coordinates # "overtransformed" on different situations # Are the transformations applied when we switch to a different frame? - v1 = np.float32((1,1,1)) - v2 = np.float32((0,0,0.33)) + v1 = np.float32((1, 1, 1)) + v2 = np.float32((0, 0, 0.33)) first_ideal = ref.iter_ts(0).positions + v1 + v2 - if len(transformed)>1: - assert_array_almost_equal(transformed[0].positions, first_ideal, decimal = ref.prec) + if len(transformed) > 1: + assert_array_almost_equal( + transformed[0].positions, first_ideal, decimal=ref.prec + ) second_ideal = ref.iter_ts(1).positions + v1 + v2 - assert_array_almost_equal(transformed[1].positions, second_ideal, decimal = ref.prec) + assert_array_almost_equal( + transformed[1].positions, second_ideal, decimal=ref.prec + ) # What if we comeback to the previous frame? - assert_array_almost_equal(transformed[0].positions, first_ideal, decimal = ref.prec) + assert_array_almost_equal( + transformed[0].positions, first_ideal, decimal=ref.prec + ) # How about we switch the frame to itself? - assert_array_almost_equal(transformed[0].positions, first_ideal, decimal = ref.prec) + assert_array_almost_equal( + transformed[0].positions, first_ideal, decimal=ref.prec + ) else: - assert_array_almost_equal(transformed[0].positions, first_ideal, decimal = ref.prec) + assert_array_almost_equal( + transformed[0].positions, first_ideal, decimal=ref.prec + ) - def test_transformation_rewind(self,ref, transformed): + def test_transformation_rewind(self, ref, transformed): # this test checks if the transformations are applied after rewinding the # trajectory - v1 = np.float32((1,1,1)) - v2 = np.float32((0,0,0.33)) + v1 = np.float32((1, 1, 1)) + v2 = np.float32((0, 0, 0.33)) ideal_coords = ref.iter_ts(0).positions + v1 + v2 transformed.rewind() - assert_array_almost_equal(transformed[0].positions, ideal_coords, decimal = ref.prec) + assert_array_almost_equal( + transformed[0].positions, ideal_coords, decimal=ref.prec + ) - def test_transformations_copy(self,ref,transformed): + def test_transformations_copy(self, ref, transformed): # this test checks if transformations are carried over a copy and if the # coordinates of the copy are equal to the original's - v1 = np.float32((1,1,1)) - v2 = np.float32((0,0,0.33)) + v1 = np.float32((1, 1, 1)) + v2 = np.float32((0, 0, 0.33)) new = transformed.copy() - assert_equal(transformed.transformations, new.transformations, - "transformations are not equal") + assert_equal( + transformed.transformations, + new.transformations, + "transformations are not equal", + ) for i, ts in enumerate(new): ideal_coords = ref.iter_ts(i).positions + v1 + v2 - assert_array_almost_equal(ts.positions, ideal_coords, decimal = ref.prec) + assert_array_almost_equal( + ts.positions, ideal_coords, decimal=ref.prec + ) def test_add_another_transformations_raises_ValueError(self, transformed): # After defining the transformations, the workflow cannot be changed with pytest.raises(ValueError): - transformed.add_transformations(translate([2,2,2])) + transformed.add_transformations(translate([2, 2, 2])) def test_pickle_reader(self, reader): reader_p = pickle.loads(pickle.dumps(reader)) assert_equal(len(reader), len(reader_p)) - assert_equal(reader.ts, reader_p.ts, - "Timestep is changed after pickling") + assert_equal( + reader.ts, reader_p.ts, "Timestep is changed after pickling" + ) reader_p_p = pickle.loads(pickle.dumps(reader_p)) assert_equal(len(reader), len(reader_p_p)) - assert_equal(reader.ts, reader_p_p.ts, - "Timestep is changed after double pickling") + assert_equal( + reader.ts, + reader_p_p.ts, + "Timestep is changed after double pickling", + ) reader.ts.positions[0] = np.array([1, 2, 3]) reader_p = pickle.loads(pickle.dumps(reader)) - assert_equal(reader.ts, reader_p.ts, - "Modification of ts not preserved after serialization") + assert_equal( + reader.ts, + reader_p.ts, + "Modification of ts not preserved after serialization", + ) def test_frame_collect_all_same(self, reader): # check that the timestep resets so that the base reference is the same @@ -467,83 +573,105 @@ def test_frame_collect_all_same(self, reader): if isinstance(reader, mda.coordinates.memory.MemoryReader): pytest.xfail("memoryreader allows independent coordinates") if isinstance(reader, mda.coordinates.DCD.DCDReader): - pytest.xfail("DCDReader allows independent coordinates." - "This behaviour is deprecated and will be changed" - "in 3.0") + pytest.xfail( + "DCDReader allows independent coordinates." + "This behaviour is deprecated and will be changed" + "in 3.0" + ) collected_ts = [] for i, ts in enumerate(reader): collected_ts.append(ts.positions) - for array in collected_ts: + for array in collected_ts: assert_allclose(array, collected_ts[0]) - @pytest.mark.parametrize('order', ('fac', 'fca', 'afc', 'acf', 'caf', 'cfa')) + @pytest.mark.parametrize( + "order", ("fac", "fca", "afc", "acf", "caf", "cfa") + ) def test_timeseries_shape(self, reader, order): timeseries = reader.timeseries(order=order) - a_index = order.index('a') - f_index = order.index('f') - c_index = order.index('c') - assert(timeseries.shape[a_index] == reader.n_atoms) - assert(timeseries.shape[f_index] == len(reader)) - assert(timeseries.shape[c_index] == 3) - - @pytest.mark.parametrize('slice', ([0,2,1], [0,10,2], [0,10,3])) + a_index = order.index("a") + f_index = order.index("f") + c_index = order.index("c") + assert timeseries.shape[a_index] == reader.n_atoms + assert timeseries.shape[f_index] == len(reader) + assert timeseries.shape[c_index] == 3 + + @pytest.mark.parametrize("slice", ([0, 2, 1], [0, 10, 2], [0, 10, 3])) def test_timeseries_values(self, reader, slice): ts_positions = [] if isinstance(reader, mda.coordinates.memory.MemoryReader): - pytest.xfail("MemoryReader uses deprecated stop inclusive" - " indexing, see Issue #3893") + pytest.xfail( + "MemoryReader uses deprecated stop inclusive" + " indexing, see Issue #3893" + ) if slice[1] > len(reader): pytest.skip("too few frames in reader") for i in range(slice[0], slice[1], slice[2]): ts = reader[i] ts_positions.append(ts.positions.copy()) positions = np.asarray(ts_positions) - timeseries = reader.timeseries(start=slice[0], stop=slice[1], - step=slice[2], order='fac') + timeseries = reader.timeseries( + start=slice[0], stop=slice[1], step=slice[2], order="fac" + ) assert_allclose(timeseries, positions) - @pytest.mark.parametrize('asel', ("index 1", "index 2", "index 1 to 3")) + @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, to_guess=()).select_atoms(asel) - timeseries = reader.timeseries(atoms, order='fac') - assert(timeseries.shape[0] == len(reader)) - assert(timeseries.shape[1] == len(atoms)) - assert(timeseries.shape[2] == 3) + timeseries = reader.timeseries(atoms, order="fac") + assert timeseries.shape[0] == len(reader) + assert timeseries.shape[1] == len(atoms) + assert timeseries.shape[2] == 3 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, to_guess=()).select_atoms(None) + with pytest.warns( + UserWarning, + match="Empty string to select atoms, empty group returned.", + ): + 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, to_guess=()).select_atoms(None) + with pytest.warns( + UserWarning, + match="Empty string to select atoms, empty group returned.", + ): + 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, to_guess=()).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') + timeseries = reader.timeseries(asel=atoms, order="fac") def test_timeseries_atomgroup(self, reader): - atoms = mda.Universe(reader.filename, to_guess=()).select_atoms("index 1") - timeseries = reader.timeseries(atomgroup=atoms, order='fac') + 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, to_guess=()).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') + timeseries = reader.timeseries( + atomgroup=atoms, asel=atoms, order="fac" + ) class MultiframeReaderTest(BaseReaderTest): def test_last_frame(self, ref, reader): ts = reader[-1] - assert_timestep_almost_equal(ts, ref.last_frame, - decimal=ref.prec) + assert_timestep_almost_equal(ts, ref.last_frame, decimal=ref.prec) def test_go_over_last_frame(self, ref, reader): with pytest.raises(IndexError): @@ -551,8 +679,7 @@ def test_go_over_last_frame(self, ref, reader): def test_frame_jump(self, ref, reader): ts = reader[ref.jump_to_frame.frame] - assert_timestep_almost_equal(ts, ref.jump_to_frame, - decimal=ref.prec) + assert_timestep_almost_equal(ts, ref.jump_to_frame, decimal=ref.prec) def test_frame_jump_issue1942(self, ref, reader): """Test for issue 1942 (especially XDR on macOS)""" @@ -566,49 +693,50 @@ def test_frame_jump_issue1942(self, ref, reader): def test_next_gives_second_frame(self, ref, reader): reader = ref.reader(ref.trajectory) ts = reader.next() - assert_timestep_almost_equal(ts, ref.second_frame, - decimal=ref.prec) + assert_timestep_almost_equal(ts, ref.second_frame, decimal=ref.prec) def test_reopen(self, ref, reader): reader.close() reader._reopen() ts = reader.next() - assert_timestep_almost_equal(ts, ref.first_frame, - decimal=ref.prec) + assert_timestep_almost_equal(ts, ref.first_frame, decimal=ref.prec) def test_rename_aux(self, ref, reader): - reader.rename_aux('lowf', 'lowf_renamed') + reader.rename_aux("lowf", "lowf_renamed") # data should now be in aux namespace under new name - assert_equal(reader.ts.aux.lowf_renamed, - ref.aux_lowf_data[0]) + assert_equal(reader.ts.aux.lowf_renamed, ref.aux_lowf_data[0]) # old name should be removed with pytest.raises(AttributeError): - getattr(reader.ts.aux, 'lowf') + getattr(reader.ts.aux, "lowf") # new name should be retained next(reader) - assert_equal(reader.ts.aux.lowf_renamed, - ref.aux_lowf_data[1]) + assert_equal(reader.ts.aux.lowf_renamed, ref.aux_lowf_data[1]) def test_iter_as_aux_highf(self, ref, reader): # auxiliary has a higher frequency, so iter_as_aux should behave the # same as regular iteration over the trjectory - for i, ts in enumerate(reader.iter_as_aux('highf')): - assert_timestep_almost_equal(ts, ref.iter_ts(i), - decimal=ref.prec) + for i, ts in enumerate(reader.iter_as_aux("highf")): + assert_timestep_almost_equal(ts, ref.iter_ts(i), decimal=ref.prec) def test_iter_as_aux_lowf(self, ref, reader): # auxiliary has a lower frequency, so iter_as_aux should iterate over # only frames where there is a corresponding auxiliary value - for i, ts in enumerate(reader.iter_as_aux('lowf')): - assert_timestep_almost_equal(ts, - ref.iter_ts(ref.aux_lowf_frames_with_steps[i]), - decimal=ref.prec) - - @pytest.mark.parametrize("accessor", [ - lambda traj: traj[[0, 1, 2]], - lambda traj: traj[:3], - lambda traj: traj], - ids=["indexed", "sliced", "all"]) + for i, ts in enumerate(reader.iter_as_aux("lowf")): + assert_timestep_almost_equal( + ts, + ref.iter_ts(ref.aux_lowf_frames_with_steps[i]), + decimal=ref.prec, + ) + + @pytest.mark.parametrize( + "accessor", + [ + lambda traj: traj[[0, 1, 2]], + lambda traj: traj[:3], + lambda traj: traj, + ], + ids=["indexed", "sliced", "all"], + ) def test_iter_rewinds(self, reader, accessor): for ts_indices in accessor(reader): pass @@ -618,21 +746,31 @@ def test_iter_rewinds(self, reader, accessor): # but also maintain its relative position. def test_pickle_next_ts_reader(self, reader): reader_p = pickle.loads(pickle.dumps(reader)) - assert_equal(next(reader), next(reader_p), - "Next timestep is changed after pickling") + assert_equal( + next(reader), + next(reader_p), + "Next timestep is changed after pickling", + ) reader_p_p = pickle.loads(pickle.dumps(reader_p)) - assert_equal(next(reader), next(reader_p_p), - "Next timestep is changed after double pickling") + assert_equal( + next(reader), + next(reader_p_p), + "Next timestep is changed after double pickling", + ) # To make sure pickle works for last frame. def test_pickle_last_ts_reader(self, reader): # move current ts to last frame. reader[-1] reader_p = pickle.loads(pickle.dumps(reader)) - assert_equal(len(reader), len(reader_p), - "Last timestep is changed after pickling") - assert_equal(reader.ts, reader_p.ts, - "Last timestep is changed after pickling") + assert_equal( + len(reader), + len(reader_p), + "Last timestep is changed after pickling", + ) + assert_equal( + reader.ts, reader_p.ts, "Last timestep is changed after pickling" + ) class BaseWriterTest(object): @@ -644,18 +782,17 @@ def reader(ref): @staticmethod @pytest.fixture() def u_no_resnames(): - return make_Universe(['names', 'resids'], trajectory=True) + return make_Universe(["names", "resids"], trajectory=True) @staticmethod @pytest.fixture() def u_no_resids(): - return make_Universe(['names', 'resnames'], trajectory=True) + return make_Universe(["names", "resnames"], trajectory=True) @staticmethod @pytest.fixture() def u_no_names(): - return make_Universe(['resids', 'resnames'], - trajectory=True) + return make_Universe(["resids", "resnames"], trajectory=True) @staticmethod @pytest.fixture() @@ -664,7 +801,7 @@ def universe(ref): def test_write_different_box(self, ref, universe, tmpdir): if ref.changing_dimensions: - outfile = 'write-dimensions-test' + ref.ext + outfile = "write-dimensions-test" + ref.ext with tmpdir.as_cwd(): with ref.writer(outfile, universe.atoms.n_atoms) as W: for ts in universe.trajectory: @@ -675,12 +812,12 @@ def test_write_different_box(self, ref, universe, tmpdir): for ts_ref, ts_w in zip(universe.trajectory, written): universe.dimensions[:3] += 1 - assert_array_almost_equal(universe.dimensions, - ts_w.dimensions, - decimal=ref.prec) + assert_array_almost_equal( + universe.dimensions, ts_w.dimensions, decimal=ref.prec + ) - def test_write_trajectory_atomgroup(self, ref,reader, universe, tmpdir): - outfile = 'write-atoms-test.' + ref.ext + def test_write_trajectory_atomgroup(self, ref, reader, universe, tmpdir): + outfile = "write-atoms-test." + ref.ext with tmpdir.as_cwd(): with ref.writer(outfile, universe.atoms.n_atoms) as w: for ts in universe.trajectory: @@ -688,19 +825,26 @@ def test_write_trajectory_atomgroup(self, ref,reader, universe, tmpdir): self._check_copy(outfile, ref, reader) def test_write_trajectory_universe(self, ref, reader, universe, tmpdir): - outfile = 'write-uni-test.' + ref.ext + outfile = "write-uni-test." + ref.ext with tmpdir.as_cwd(): with ref.writer(outfile, universe.atoms.n_atoms) as w: for ts in universe.trajectory: w.write(universe) self._check_copy(outfile, ref, reader) - def test_write_selection(self, ref, reader, universe, u_no_resnames, - u_no_resids, u_no_names, tmpdir): - sel_str = 'resid 1' + def test_write_selection( + self, + ref, + reader, + universe, + u_no_resnames, + u_no_resids, + u_no_names, + tmpdir, + ): + sel_str = "resid 1" sel = universe.select_atoms(sel_str) - outfile = 'write-selection-test.' + ref.ext - + outfile = "write-selection-test." + ref.ext with tmpdir.as_cwd(): with ref.writer(outfile, sel.n_atoms) as W: @@ -710,20 +854,23 @@ def test_write_selection(self, ref, reader, universe, u_no_resnames, copy = ref.reader(outfile) for orig_ts, copy_ts in zip(universe.trajectory, copy): assert_array_almost_equal( - copy_ts._pos, sel.atoms.positions, ref.prec, + copy_ts._pos, + sel.atoms.positions, + ref.prec, err_msg="coordinate mismatch between original and written " - "trajectory at frame {} (orig) vs {} (copy)".format( - orig_ts.frame, copy_ts.frame)) + "trajectory at frame {} (orig) vs {} (copy)".format( + orig_ts.frame, copy_ts.frame + ), + ) def _check_copy(self, fname, ref, reader): copy = ref.reader(fname) assert_equal(reader.n_frames, copy.n_frames) for orig_ts, copy_ts in zip(reader, copy): - assert_timestep_almost_equal( - copy_ts, orig_ts, decimal=ref.prec) + assert_timestep_almost_equal(copy_ts, orig_ts, decimal=ref.prec) def test_write_none(self, ref, tmpdir): - outfile = 'write-none.' + ref.ext + outfile = "write-none." + ref.ext with tmpdir.as_cwd(): with pytest.raises(TypeError): with ref.writer(outfile, 42) as w: @@ -732,13 +879,13 @@ def test_write_none(self, ref, tmpdir): def test_no_container(self, ref, tmpdir): with tmpdir.as_cwd(): if ref.container_format: - ref.writer('foo') + ref.writer("foo") else: with pytest.raises(TypeError): - ref.writer('foo') + ref.writer("foo") def test_write_not_changing_ts(self, ref, universe, tmpdir): - outfile = 'write-not-changing-ts.' + ref.ext + outfile = "write-not-changing-ts." + ref.ext copy_ts = universe.trajectory.ts.copy() with tmpdir.as_cwd(): @@ -747,63 +894,91 @@ def test_write_not_changing_ts(self, ref, universe, tmpdir): assert_timestep_almost_equal(copy_ts, universe.trajectory.ts) -def assert_timestep_equal(A, B, msg=''): - """ assert that two timesteps are exactly equal and commutative - """ +def assert_timestep_equal(A, B, msg=""): + """assert that two timesteps are exactly equal and commutative""" assert A == B, msg assert B == A, msg def assert_timestep_almost_equal(A, B, decimal=6, verbose=True): if not isinstance(A, mda.coordinates.timestep.Timestep): - raise AssertionError('A is not of type Timestep') + raise AssertionError("A is not of type Timestep") if not isinstance(B, mda.coordinates.timestep.Timestep): - raise AssertionError('B is not of type Timestep') + raise AssertionError("B is not of type Timestep") if A.frame != B.frame: - raise AssertionError('A and B refer to different frames: ' - 'A.frame = {}, B.frame={}'.format( - A.frame, B.frame)) + raise AssertionError( + "A and B refer to different frames: " + "A.frame = {}, B.frame={}".format(A.frame, B.frame) + ) if not np.allclose(A.time, B.time): - raise AssertionError('A and B refer to different times: ' - 'A.time = {}, B.time={}'.format( - A.time, B.time)) + raise AssertionError( + "A and B refer to different times: " + "A.time = {}, B.time={}".format(A.time, B.time) + ) if A.n_atoms != B.n_atoms: - raise AssertionError('A and B have a differnent number of atoms: ' - 'A.n_atoms = {}, B.n_atoms = {}'.format( - A.n_atoms, B.n_atoms)) + raise AssertionError( + "A and B have a differnent number of atoms: " + "A.n_atoms = {}, B.n_atoms = {}".format(A.n_atoms, B.n_atoms) + ) if A.has_positions != B.has_positions: - raise AssertionError('Only one Timestep has positions:' - 'A.has_positions = {}, B.has_positions = {}'.format( - A.has_positions, B.has_positions)) + raise AssertionError( + "Only one Timestep has positions:" + "A.has_positions = {}, B.has_positions = {}".format( + A.has_positions, B.has_positions + ) + ) if A.has_positions: - assert_array_almost_equal(A.positions, B.positions, decimal=decimal, - err_msg='Timestep positions', - verbose=verbose) + assert_array_almost_equal( + A.positions, + B.positions, + decimal=decimal, + err_msg="Timestep positions", + verbose=verbose, + ) if A.has_velocities != B.has_velocities: - raise AssertionError('Only one Timestep has velocities:' - 'A.has_velocities = {}, B.has_velocities = {}'.format( - A.has_velocities, B.has_velocities)) + raise AssertionError( + "Only one Timestep has velocities:" + "A.has_velocities = {}, B.has_velocities = {}".format( + A.has_velocities, B.has_velocities + ) + ) if A.has_velocities: - assert_array_almost_equal(A.velocities, B.velocities, decimal=decimal, - err_msg='Timestep velocities', - verbose=verbose) + assert_array_almost_equal( + A.velocities, + B.velocities, + decimal=decimal, + err_msg="Timestep velocities", + verbose=verbose, + ) if A.has_forces != B.has_forces: - raise AssertionError('Only one Timestep has forces:' - 'A.has_forces = {}, B.has_forces = {}'.format( - A.has_forces, B.has_forces)) + raise AssertionError( + "Only one Timestep has forces:" + "A.has_forces = {}, B.has_forces = {}".format( + A.has_forces, B.has_forces + ) + ) if A.has_forces: - assert_array_almost_equal(A.forces, B.forces, decimal=decimal, - err_msg='Timestep forces', verbose=verbose) + assert_array_almost_equal( + A.forces, + B.forces, + decimal=decimal, + err_msg="Timestep forces", + verbose=verbose, + ) # Check we've got auxiliaries before comparing values (auxiliaries aren't written # so we won't have aux values to compare when testing writer) if len(A.aux) > 0 and len(B.aux) > 0: - assert_equal(A.aux, B.aux, err_msg='Auxiliary values do not match: ' - 'A.aux = {}, B.aux = {}'.format(A.aux, B.aux)) + assert_equal( + A.aux, + B.aux, + err_msg="Auxiliary values do not match: " + "A.aux = {}, B.aux = {}".format(A.aux, B.aux), + ) diff --git a/testsuite/MDAnalysisTests/coordinates/reference.py b/testsuite/MDAnalysisTests/coordinates/reference.py index 73a91809852..8d43a9f925b 100644 --- a/testsuite/MDAnalysisTests/coordinates/reference.py +++ b/testsuite/MDAnalysisTests/coordinates/reference.py @@ -23,12 +23,19 @@ import numpy as np from MDAnalysisTests import datafiles -from MDAnalysisTests.datafiles import (PDB_small, PDB, LAMMPSdata, - LAMMPSdata2, LAMMPSdcd2, - LAMMPSdata_mini, - LAMMPSdata_additional_columns, - PSF_TRICLINIC, DCD_TRICLINIC, - PSF_NAMD_TRICLINIC, DCD_NAMD_TRICLINIC) +from MDAnalysisTests.datafiles import ( + PDB_small, + PDB, + LAMMPSdata, + LAMMPSdata2, + LAMMPSdcd2, + LAMMPSdata_mini, + LAMMPSdata_additional_columns, + PSF_TRICLINIC, + DCD_TRICLINIC, + PSF_NAMD_TRICLINIC, + DCD_NAMD_TRICLINIC, +) class RefAdKSmall(object): @@ -41,20 +48,22 @@ class RefAdKSmall(object): All distances must be in ANGSTROEM as this is the MDAnalysis default unit. All readers must return Angstroem by default. """ + filename = datafiles.PDB_small ref_coordinates = { # G11:CA, copied frm adk_open.pdb - 'A10CA': np.array([-1.198, 7.937, 22.654]), + "A10CA": np.array([-1.198, 7.937, 22.654]), } - ref_distances = {'endtoend': 11.016959} + ref_distances = {"endtoend": 11.016959} ref_E151HA2_index = 2314 ref_n_atoms = 3341 ref_charmm_totalcharge = -4.0 ref_charmm_Hcharges = [0.33] + 203 * [0.31] ref_charmm_ArgCAcharges = 13 * [0.07] ref_charmm_ProNcharges = 10 * [-0.29] - ref_unitcell = np.array([80.017, 80.017, 80.017, 60., 60., 90.], - dtype=np.float32) + ref_unitcell = np.array( + [80.017, 80.017, 80.017, 60.0, 60.0, 90.0], dtype=np.float32 + ) ref_volume = 0.0 @@ -68,19 +77,21 @@ class RefAdK(object): All distances must be in ANGSTROEM as this is the MDAnalysis default unit. All readers must return Angstroem by default. """ + filename = datafiles.PDB ref_coordinates = { # Angstroem as MDAnalysis unit!! - 'A10CA': np.array([62.97600174, 62.08800125, 20.2329998]), + "A10CA": np.array([62.97600174, 62.08800125, 20.2329998]), } - ref_distances = {'endtoend': 9.3513174} + ref_distances = {"endtoend": 9.3513174} ref_E151HA2_index = 2314 ref_n_atoms = 47681 ref_Na_sel_size = 4 # CRYST1 80.017 80.017 80.017 60.00 60.00 90.00 - ref_unitcell = np.array([80.017, 80.017, 80.017, 60., 60., 90.], - dtype=np.float32) - #ref_volume = 362270.0 # computed with Gromacs ## NOT EXACT! + ref_unitcell = np.array( + [80.017, 80.017, 80.017, 60.0, 60.0, 90.0], dtype=np.float32 + ) + # ref_volume = 362270.0 # computed with Gromacs ## NOT EXACT! ref_volume = 362269.520669292 @@ -94,6 +105,7 @@ class Ref2r9r(object): All distances must be in ANGSTROEM as this is the MDAnalysis default unit. All readers must return Angstroem by default. """ + ref_n_atoms = 1284 ref_sum_centre_of_geometry = -98.24146 ref_n_frames = 10 @@ -115,6 +127,7 @@ class RefACHE(object): # 472.2592159509659 """ + ref_n_atoms = 252 ref_proteinatoms = ref_n_atoms ref_sum_centre_of_geometry = 472.2592159509659 # 430.44807815551758 @@ -138,6 +151,7 @@ class RefCappedAla(object): # 686.276834487915 """ + ref_n_atoms = 5071 ref_proteinatoms = 22 ref_sum_centre_of_geometry = 686.276834487915 @@ -155,6 +169,7 @@ class RefVGV(object): ref_sum_centre_of_geometry = np.sum([protein.center_of_geometry() for ts in w.trajectory]) """ + topology = datafiles.PRMncdf filename = datafiles.NCDF ref_n_atoms = 2661 @@ -163,11 +178,13 @@ class RefVGV(object): ref_n_frames = 30 ref_periodic = True + class RefTZ2(object): """Reference values for the cpptraj testcase tz2.truncoct.nc Used under the GPL v3. """ + topology = datafiles.PRM7 filename = datafiles.NCDFtruncoct ref_n_atoms = 5827 @@ -175,37 +192,53 @@ class RefTZ2(object): ref_sum_centre_of_geometry = -68.575745 ref_n_frames = 10 ref_periodic = True - + class RefTRZ(object): # ref_coordinates = {} # ref_distances = {'endtoend': } ref_n_atoms = 8184 - ref_dimensions = np.array([55.422830581665039, 55.422830581665039, - 55.422830581665039, 90., 90., 90.], - dtype=np.float32) + ref_dimensions = np.array( + [ + 55.422830581665039, + 55.422830581665039, + 55.422830581665039, + 90.0, + 90.0, + 90.0, + ], + dtype=np.float32, + ) ref_volume = 170241.762765 ref_n_frames = 6 - ref_coordinates = np.array([72.3163681, -130.31130981, 19.97969055], - dtype=np.float32) - ref_velocities = np.array([[14.83297443, 18.02611542, 6.07733774]], - dtype=np.float32) + ref_coordinates = np.array( + [72.3163681, -130.31130981, 19.97969055], dtype=np.float32 + ) + ref_velocities = np.array( + [[14.83297443, 18.02611542, 6.07733774]], dtype=np.float32 + ) ref_delta = 0.001 ref_time = 0.01 - ref_title = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901234' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901234') + ref_title = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901234" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901234" + ) class RefLAMMPSData(object): filename = LAMMPSdata n_atoms = 18364 - pos_atom1 = np.array([11.89985657, 48.4455719, 19.09719849], - dtype=np.float32) - vel_atom1 = np.array([-0.005667593, 0.00791380978, -0.00300779533], - dtype=np.float32) - dimensions = np.array([55.42282867, 55.42282867, 55.42282867, 90., 90., 90. - ], - dtype=np.float32) + pos_atom1 = np.array( + [11.89985657, 48.4455719, 19.09719849], dtype=np.float32 + ) + vel_atom1 = np.array( + [-0.005667593, 0.00791380978, -0.00300779533], dtype=np.float32 + ) + dimensions = np.array( + [55.42282867, 55.42282867, 55.42282867, 90.0, 90.0, 90.0], + dtype=np.float32, + ) + class RefLAMMPSDataDCD(object): format = "LAMMPS" @@ -215,22 +248,38 @@ class RefLAMMPSDataDCD(object): n_frames = 5 dt = 0.5 # ps per frame mean_dimensions = np.array( - [ 50.66186142, 47.18824387, 52.33762741, - 90. , 90. , 90. ], dtype=np.float32) + [50.66186142, 47.18824387, 52.33762741, 90.0, 90.0, 90.0], + dtype=np.float32, + ) class RefLAMMPSDataMini(object): filename = LAMMPSdata_mini n_atoms = 1 - pos_atom1 = np.array([11.89985657, 48.4455719, 19.09719849], - dtype=np.float32) - vel_atom1 = np.array([-0.005667593, 0.00791380978, -0.00300779533], - dtype=np.float32) - dimensions = np.array([60., 50., 30., 90., 90., 90.], dtype=np.float32) + pos_atom1 = np.array( + [11.89985657, 48.4455719, 19.09719849], dtype=np.float32 + ) + vel_atom1 = np.array( + [-0.005667593, 0.00791380978, -0.00300779533], dtype=np.float32 + ) + dimensions = np.array( + [60.0, 50.0, 30.0, 90.0, 90.0, 90.0], dtype=np.float32 + ) class RefLAMMPSDataAdditionalColumns(object): - q = np.array([2.58855e-03, 6.91952e-05, 1.05548e-02, 4.20319e-03, - 9.19172e-03, 4.79777e-03, 6.36864e-04, 5.87125e-03, - -2.18125e-03, 6.88910e-03]) + q = np.array( + [ + 2.58855e-03, + 6.91952e-05, + 1.05548e-02, + 4.20319e-03, + 9.19172e-03, + 4.79777e-03, + 6.36864e-04, + 5.87125e-03, + -2.18125e-03, + 6.88910e-03, + ] + ) p = np.array(5 * [1.1, 1.2]) diff --git a/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py b/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py index 66394ee4d74..9fb8fe29ea7 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_amber_inpcrd.py @@ -22,10 +22,10 @@ # import numpy as np -from numpy.testing import (assert_allclose, assert_equal) +from numpy.testing import assert_allclose, assert_equal import MDAnalysis as mda -from MDAnalysisTests.datafiles import (INPCRD, XYZ_five) +from MDAnalysisTests.datafiles import INPCRD, XYZ_five class TestINPCRDReader(object): @@ -34,16 +34,21 @@ class TestINPCRDReader(object): @staticmethod def _check_ts(ts): # Check a ts has the right values in - ref_pos = np.array([[6.6528795, 6.6711416, -8.5963255], - [7.3133773, 5.8359736, -8.8294175], - [8.3254058, 6.2227613, -8.7098593], - [7.0833200, 5.5038197, -9.8417650], - [7.1129439, 4.6170351, -7.9729560]]) + ref_pos = np.array( + [ + [6.6528795, 6.6711416, -8.5963255], + [7.3133773, 5.8359736, -8.8294175], + [8.3254058, 6.2227613, -8.7098593], + [7.0833200, 5.5038197, -9.8417650], + [7.1129439, 4.6170351, -7.9729560], + ] + ) for ref, val in zip(ref_pos, ts._pos): assert_allclose(ref, val) def test_reader(self): from MDAnalysis.coordinates.INPCRD import INPReader + r = INPReader(INPCRD) assert_equal(r.n_atoms, 5) self._check_ts(r.ts) @@ -53,5 +58,5 @@ def test_universe_inpcrd(self): self._check_ts(u.trajectory.ts) def test_universe_restrt(self): - u = mda.Universe(XYZ_five, INPCRD, format='RESTRT') + u = mda.Universe(XYZ_five, INPCRD, format="RESTRT") self._check_ts(u.trajectory.ts) diff --git a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py index 84bcd128cf5..4614e3e4d1a 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py +++ b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py @@ -20,22 +20,31 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -import sys +import os import platform +import sys import warnings -import numpy as np -import os - -import pytest - -from numpy.testing import (assert_equal, assert_almost_equal) import MDAnalysis as mda +import numpy as np +import pytest from MDAnalysis.transformations import translate -from MDAnalysisTests.datafiles import (PDB, PSF, CRD, DCD, - GRO, XTC, TRR, PDB_small, PDB_closed, - LAMMPS_chain, LAMMPSDUMP_chain1, - LAMMPSDUMP_chain2,) +from numpy.testing import assert_almost_equal, assert_equal + +from MDAnalysisTests.datafiles import ( + CRD, + DCD, + GRO, + PDB, + PSF, + TRR, + XTC, + LAMMPS_chain, + LAMMPSDUMP_chain1, + LAMMPSDUMP_chain2, + PDB_closed, + PDB_small, +) from MDAnalysisTests.util import no_warning @@ -44,22 +53,28 @@ class TestChainReader(object): @pytest.fixture() def universe(self): - return mda.Universe(PSF, - [DCD, CRD, DCD, CRD, DCD, CRD, CRD]) + return mda.Universe(PSF, [DCD, CRD, DCD, CRD, DCD, CRD, CRD]) @pytest.fixture() def transformed(ref): - return mda.Universe(PSF, - [DCD, CRD, DCD, CRD, DCD, CRD, CRD], - transformations=[translate([10,10,10])]) + 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__()) - + assert_equal( + "", + u.trajectory.__repr__(), + ) def test_truncated_repr(self, universe): - assert_equal("", universe.trajectory.__repr__()) + assert_equal( + "", + universe.trajectory.__repr__(), + ) def test_next_trajectory(self, universe): universe.trajectory.rewind() @@ -67,26 +82,34 @@ def test_next_trajectory(self, universe): assert_equal(universe.trajectory.ts.frame, 1, "loading frame 2") def test_n_atoms(self, universe): - assert_equal(universe.trajectory.n_atoms, 3341, - "wrong number of atoms") + assert_equal( + universe.trajectory.n_atoms, 3341, "wrong number of atoms" + ) def test_n_frames(self, universe): - assert_equal(universe.trajectory.n_frames, 3 * 98 + 4, - "wrong number of frames in chained dcd") + assert_equal( + universe.trajectory.n_frames, + 3 * 98 + 4, + "wrong number of frames in chained dcd", + ) def test_iteration(self, universe): for ts in universe.trajectory: pass # just forward to last frame - assert_equal(universe.trajectory.n_frames - 1, ts.frame, - "iteration yielded wrong number of frames ({0:d}), " - "should be {1:d}".format(ts.frame, - universe.trajectory.n_frames)) + assert_equal( + universe.trajectory.n_frames - 1, + ts.frame, + "iteration yielded wrong number of frames ({0:d}), " + "should be {1:d}".format(ts.frame, universe.trajectory.n_frames), + ) def test_jump_lastframe_trajectory(self, universe): universe.trajectory[-1] - assert_equal(universe.trajectory.ts.frame + 1, - universe.trajectory.n_frames, - "indexing last frame with trajectory[-1]") + assert_equal( + universe.trajectory.ts.frame + 1, + universe.trajectory.n_frames, + "indexing last frame with trajectory[-1]", + ) def test_slice_trajectory(self, universe): frames = [ts.frame for ts in universe.trajectory[5:17:3]] @@ -112,13 +135,17 @@ def test_frame(self, universe): # forward to frame where we repeat original dcd again: # dcd:0..97 crd:98 dcd:99..196 universe.trajectory[99] - assert_equal(universe.atoms.positions, coord0, - "coordinates at frame 1 and 100 should be the same!") + assert_equal( + universe.atoms.positions, + coord0, + "coordinates at frame 1 and 100 should be the same!", + ) def test_time(self, universe): universe.trajectory[30] # index and frames 0-based assert_almost_equal( - universe.trajectory.time, 30.0, 5, err_msg="Wrong time of frame") + universe.trajectory.time, 30.0, 5, err_msg="Wrong time of frame" + ) def test_write_dcd(self, universe, tmpdir): """test that ChainReader written dcd (containing crds) is correct @@ -129,53 +156,63 @@ def test_write_dcd(self, universe, tmpdir): W.write(universe) universe.trajectory.rewind() u = mda.Universe(PSF, outfile) - for (ts_orig, ts_new) in zip(universe.trajectory, u.trajectory): + for ts_orig, ts_new in zip(universe.trajectory, u.trajectory): assert_almost_equal( ts_orig._pos, 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]) + vector = np.float32([10, 10, 10]) # # Are the transformations applied and # are the coordinates "overtransformed"? # iterate once: for ts in transformed.trajectory: frame = ts.frame ref = universe.trajectory[frame].positions + vector - assert_almost_equal(ts.positions, ref, decimal = 6) + assert_almost_equal(ts.positions, ref, decimal=6) # iterate again: for ts in transformed.trajectory: frame = ts.frame ref = universe.trajectory[frame].positions + vector - assert_almost_equal(ts.positions, ref, decimal = 6) + assert_almost_equal(ts.positions, ref, decimal=6) def test_transform_slice(self, universe, transformed): - vector = np.float32([10,10,10]) + vector = np.float32([10, 10, 10]) # what happens when we slice the trajectory? for ts in transformed.trajectory[5:17:3]: frame = ts.frame ref = universe.trajectory[frame].positions + vector - assert_almost_equal(ts.positions, ref, decimal = 6) + assert_almost_equal(ts.positions, ref, decimal=6) def test_transform_switch(self, universe, transformed): - vector = np.float32([10,10,10]) + vector = np.float32([10, 10, 10]) # grab a frame: ref = universe.trajectory[2].positions + vector - assert_almost_equal(transformed.trajectory[2].positions, ref, decimal = 6) + assert_almost_equal( + transformed.trajectory[2].positions, ref, decimal=6 + ) # now switch to another frame newref = universe.trajectory[10].positions + vector - assert_almost_equal(transformed.trajectory[10].positions, newref, decimal = 6) + 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) + assert_almost_equal( + transformed.trajectory[2].positions, ref, decimal=6 + ) def test_transfrom_rewind(self, universe, transformed): - vector = np.float32([10,10,10]) + vector = np.float32([10, 10, 10]) ref = universe.trajectory[0].positions + vector transformed.trajectory.rewind() - assert_almost_equal(transformed.trajectory.ts.positions, ref, decimal = 6) + assert_almost_equal( + transformed.trajectory.ts.positions, ref, decimal=6 + ) class TestChainReaderCommonDt(object): @@ -185,7 +222,8 @@ class TestChainReaderCommonDt(object): @pytest.fixture() def trajectory(self): universe = mda.Universe( - PSF, [DCD, CRD, DCD, CRD, DCD, CRD, CRD], dt=self.common_dt) + PSF, [DCD, CRD, DCD, CRD, DCD, CRD, CRD], dt=self.common_dt + ) return universe.trajectory def test_time(self, trajectory): @@ -196,56 +234,66 @@ def test_time(self, trajectory): trajectory.time, trajectory.frame * self.common_dt, 5, - err_msg="Wrong time for frame {0:d}".format(frame_n)) + err_msg="Wrong time for frame {0:d}".format(frame_n), + ) class TestChainReaderFormats(object): """Test of ChainReader with explicit formats (Issue 76).""" def test_set_all_format_tuples(self): - universe = mda.Universe(GRO, [(PDB, 'pdb'), (XTC, 'xtc'), (TRR, - 'trr')]) + universe = mda.Universe( + GRO, [(PDB, "pdb"), (XTC, "xtc"), (TRR, "trr")] + ) assert universe.trajectory.n_frames == 21 assert_equal(universe.trajectory.filenames, [PDB, XTC, TRR]) def test_set_all_format_lammps(self): universe = mda.Universe( - LAMMPS_chain, [LAMMPSDUMP_chain1, LAMMPSDUMP_chain2], - format="LAMMPSDUMP", continuous=True) + LAMMPS_chain, + [LAMMPSDUMP_chain1, LAMMPSDUMP_chain2], + format="LAMMPSDUMP", + continuous=True, + ) assert universe.trajectory.n_frames == 11 # Test whether the amount of time between frames is consistently = 1 time_values = np.array([ts.time for ts in universe.trajectory]) - dt_array = time_values[1:]-time_values[:-1] + dt_array = time_values[1:] - time_values[:-1] assert np.unique(dt_array) == 1 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'), - (TRR, 'trr')], format='gro') + 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'), - (TRR, 'trr')], format='pdb') - assert 'Unable to read' in str(errinfo.value) - + mda.Universe( + GRO, + [(PDB, "pdb"), GRO, GRO, (XTC, "xtc"), (TRR, "trr")], + format="pdb", + ) + assert "Unable to read" in str(errinfo.value) def test_set_one_format_tuple(self): - universe = mda.Universe(PSF, [(PDB_small, 'pdb'), DCD]) + universe = mda.Universe(PSF, [(PDB_small, "pdb"), DCD]) assert universe.trajectory.n_frames == 99 def test_set_all_formats(self): with pytest.raises(TypeError) as errinfo: - mda.Universe(PDB, [PDB, GRO], format='gro') - assert 'Unable to read' in str(errinfo.value) + mda.Universe(PDB, [PDB, GRO], format="gro") + assert "Unable to read" in str(errinfo.value) - universe = mda.Universe(GRO, [PDB, PDB, PDB], format='pdb') + universe = mda.Universe(GRO, [PDB, PDB, PDB], format="pdb") assert_equal(universe.trajectory.filenames, [PDB, PDB, PDB]) -def build_trajectories(folder, sequences, fmt='xtc'): +def build_trajectories(folder, sequences, fmt="xtc"): """ A scenario is given as a series of time sequences. The result is returned as a list of time and origin of the frame. Each element of that @@ -257,7 +305,7 @@ def build_trajectories(folder, sequences, fmt='xtc'): https://gist.github.com/jbarnoud/cacd0957d3df01d1577f640b20e86039 """ - template = 'trjcat_test_{{}}.{}'.format(fmt) + template = "trjcat_test_{{}}.{}".format(fmt) template = os.path.join(folder, template) # Use an empty universe to have a topology @@ -288,38 +336,63 @@ def __init__(self, seq, n_frames, order): self.n_frames = n_frames self.order = order - @pytest.mark.parametrize('fmt', ('xtc', 'trr')) - @pytest.mark.parametrize('seq_info',[ - SequenceInfo(seq=([0, 1, 2, 3], [2, 3, 4, 5], [4, 5, 6, 7]), - n_frames=8, - order=[0, 0, 1, 1, 2, 2, 2, 2]), - SequenceInfo(seq=([0, 1, 2, 4], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), - n_frames=10, - order=np.ones(10)), - SequenceInfo(seq=([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3]), - n_frames=4, - order=np.ones(4)), - SequenceInfo(seq=([5, 6, 7, 8, 9], [2, 3, 4, 5, 6], [0, 1, 2, 3]), - n_frames=10, - order=[2, 2, 1, 1, 1, 0, 0, 0, 0, 0]), - SequenceInfo(seq=([0, 1, 2],) * 3, - n_frames=3, - order=[2, 2, 2]), - SequenceInfo(seq=([0, 1, 2, 3,], [3, 4], [4, 5, 6, 7]), - n_frames=8, - order=[0, 0, 0, 1, 2, 2, 2, 2]), - SequenceInfo(seq=([5, 6, 7, 8, 9], [2, 3, 4, 5, 6], [0, 1, 2, 3]), - n_frames=10, - order=[2, 2, 1, 1, 1, 0, 0, 0, 0, 0]), - SequenceInfo(seq=[list(range(0, 6)), list(range(2, 5)), - list(range(2, 5)), list(range(2, 5)), - list(range(3, 8))], - n_frames=8, - order=[0, 0, 3, 4, 4, 4, 4, 4]), - ]) + @pytest.mark.parametrize("fmt", ("xtc", "trr")) + @pytest.mark.parametrize( + "seq_info", + [ + SequenceInfo( + seq=([0, 1, 2, 3], [2, 3, 4, 5], [4, 5, 6, 7]), + n_frames=8, + order=[0, 0, 1, 1, 2, 2, 2, 2], + ), + SequenceInfo( + seq=([0, 1, 2, 4], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + n_frames=10, + order=np.ones(10), + ), + SequenceInfo( + seq=([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3]), + n_frames=4, + order=np.ones(4), + ), + SequenceInfo( + seq=([5, 6, 7, 8, 9], [2, 3, 4, 5, 6], [0, 1, 2, 3]), + n_frames=10, + order=[2, 2, 1, 1, 1, 0, 0, 0, 0, 0], + ), + SequenceInfo(seq=([0, 1, 2],) * 3, n_frames=3, order=[2, 2, 2]), + SequenceInfo( + seq=( + [0, 1, 2, 3], + [3, 4], + [4, 5, 6, 7], + ), + n_frames=8, + order=[0, 0, 0, 1, 2, 2, 2, 2], + ), + SequenceInfo( + seq=([5, 6, 7, 8, 9], [2, 3, 4, 5, 6], [0, 1, 2, 3]), + n_frames=10, + order=[2, 2, 1, 1, 1, 0, 0, 0, 0, 0], + ), + SequenceInfo( + seq=[ + list(range(0, 6)), + list(range(2, 5)), + list(range(2, 5)), + list(range(2, 5)), + list(range(3, 8)), + ], + n_frames=8, + order=[0, 0, 3, 4, 4, 4, 4, 4], + ), + ], + ) def test_order(self, seq_info, tmpdir, fmt): folder = str(tmpdir) - utop, fnames = build_trajectories(folder, sequences=seq_info.seq, fmt=fmt) + utop, fnames = build_trajectories( + folder, sequences=seq_info.seq, fmt=fmt + ) 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): @@ -330,14 +403,20 @@ def test_order(self, seq_info, tmpdir, fmt): 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,) + utop, fnames = build_trajectories( + folder, + sequences=sequences, + ) 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,) + utop, fnames = build_trajectories( + folder, + sequences=sequences, + ) u = mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) assert u.trajectory.n_frames == 9 @@ -345,7 +424,10 @@ def test_warning(self, tmpdir): folder = str(tmpdir) # this sequence *should* trigger a warning sequences = ([0, 1, 2, 3], [5, 6, 7]) - utop, fnames = build_trajectories(folder, sequences=sequences,) + utop, fnames = build_trajectories( + folder, + sequences=sequences, + ) with pytest.warns(UserWarning): mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) @@ -353,7 +435,10 @@ def test_interleaving_error(self, tmpdir): folder = str(tmpdir) # interleaving is not supported by chainreader sequences = ([0, 2, 4, 6], [1, 3, 5, 7]) - utop, fnames = build_trajectories(folder, sequences=sequences,) + utop, fnames = build_trajectories( + folder, + sequences=sequences, + ) with pytest.raises(RuntimeError): mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) @@ -361,7 +446,10 @@ def test_easy_trigger_warning(self, tmpdir): folder = str(tmpdir) # this sequence shouldn't trigger a warning sequences = ([0, 1, 2, 3], [2, 3, 4, 5], [4, 5, 6, 7]) - utop, fnames = build_trajectories(folder, sequences=sequences,) + utop, fnames = build_trajectories( + folder, + sequences=sequences, + ) with no_warning(UserWarning): with warnings.catch_warnings(): # for windows Python 3.10 ignore: @@ -370,14 +458,24 @@ def test_easy_trigger_warning(self, tmpdir): # that still imports six if sys.version_info >= (3, 10): warnings.filterwarnings( - action='ignore', - category=ImportWarning) - mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) + action="ignore", category=ImportWarning + ) + 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,) + sequences = ( + [0, 1, 2, 3], + [ + 5, + ], + ) + utop, fnames = build_trajectories( + folder, + sequences=sequences, + ) with pytest.raises(RuntimeError): mda.Universe(utop._topology, fnames, continuous=True, to_guess=()) @@ -396,33 +494,41 @@ def test_unsupported_filetypes(self): mda.Universe(PDB, [PDB, XTC], continuous=True) -@pytest.mark.parametrize('l, ref', ([((0, 3), (3, 3), (4, 7)), (0, 1, 2)], - [((0, 9), (0, 4)), (0, 1)], - [((0, 3), (2, 2), (3, 3), (2, 6), (5, 9)), (0, 1, 3, 2, 4)], - [((0, 2), (4, 9), (0, 4), (7, 9)), (0, 2, 1, 3)], - [((0, 5), (2, 4), (2, 4), (2, 4), (3, 7)), (0, 1, 2, 3, 4)] - )) +@pytest.mark.parametrize( + "l, ref", + ( + [((0, 3), (3, 3), (4, 7)), (0, 1, 2)], + [((0, 9), (0, 4)), (0, 1)], + [((0, 3), (2, 2), (3, 3), (2, 6), (5, 9)), (0, 1, 3, 2, 4)], + [((0, 2), (4, 9), (0, 4), (7, 9)), (0, 2, 1, 3)], + [((0, 5), (2, 4), (2, 4), (2, 4), (3, 7)), (0, 1, 2, 3, 4)], + ), +) def test_multilevel_arg_sort(l, ref): indices = mda.coordinates.chain.multi_level_argsort(l) assert_equal(indices, ref) -@pytest.mark.parametrize('l, ref', ([((0, 4), (3, 6), (6, 9)), (0, 1, 2)], - [((0, 3), (3, 4), (4, 7)), (0, 1, 2)], - [((0, 3), (3, 5), (4, 7)), (0, 1, 2)], - [((0, 3), (0, 4), (3, 5), (4, 7)), (1, 2, 3)], - [((0, 3), (0, 4)), (1,)], - [((0, 3), (0, 3)), (1,)], - [((1, 3), (0, 4)), (0,)], - [((0, 3), ), (0, )], - [((0, 3), (5, 9)), (0, 1)], - [((0, 3), (0, 3), (5, 9)), (1, 2)], - [((0, 3), (0, 3), (0, 3), (5, 9)), (2, 3)], - [((0, 5), (2, 4), (2, 4), (2, 4), (3, 7)), (0, 3, 4)], - [((0, 3), (2, 4), (2, 4), (2, 4), (3, 7)), (0, 3, 4)], - [((0, 3), (2, 4), (4, 7), (4, 7), (4, 7), (6, 9)), (0, 1, 4, 5)], - [((0, 6), (2, 5), (2, 5), (2, 5), (3, 8)), (0, 3, 4)], - )) +@pytest.mark.parametrize( + "l, ref", + ( + [((0, 4), (3, 6), (6, 9)), (0, 1, 2)], + [((0, 3), (3, 4), (4, 7)), (0, 1, 2)], + [((0, 3), (3, 5), (4, 7)), (0, 1, 2)], + [((0, 3), (0, 4), (3, 5), (4, 7)), (1, 2, 3)], + [((0, 3), (0, 4)), (1,)], + [((0, 3), (0, 3)), (1,)], + [((1, 3), (0, 4)), (0,)], + [((0, 3),), (0,)], + [((0, 3), (5, 9)), (0, 1)], + [((0, 3), (0, 3), (5, 9)), (1, 2)], + [((0, 3), (0, 3), (0, 3), (5, 9)), (2, 3)], + [((0, 5), (2, 4), (2, 4), (2, 4), (3, 7)), (0, 3, 4)], + [((0, 3), (2, 4), (2, 4), (2, 4), (3, 7)), (0, 3, 4)], + [((0, 3), (2, 4), (4, 7), (4, 7), (4, 7), (6, 9)), (0, 1, 4, 5)], + [((0, 6), (2, 5), (2, 5), (2, 5), (3, 8)), (0, 3, 4)], + ), +) def test_filter_times(l, ref): indices = mda.coordinates.chain.filter_times(l, dt=1) assert_equal(indices, ref) @@ -466,11 +572,15 @@ def test_issue_3349(): u_expected_filenames = np.array([None, str(DCD)]) u2_expected_filenames = np.array([None, None, None, str(DCD)]) - assert_equal("", - u.trajectory.__repr__()) + assert_equal( + "", + u.trajectory.__repr__(), + ) assert_equal(u_expected_filenames, u.trajectory.filenames) - assert_equal("", - u2.trajectory.__repr__()) + assert_equal( + "", + u2.trajectory.__repr__(), + ) assert_equal(u2_expected_filenames, u2.trajectory.filenames) diff --git a/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py b/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py index fd489977fe2..a556414de30 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py +++ b/testsuite/MDAnalysisTests/coordinates/test_chemfiles.py @@ -54,7 +54,9 @@ def test_version_check(version, monkeypatch): ChemfilesWriter("") -@pytest.mark.skipif(not check_chemfiles_version(), reason="Wrong version of chemfiles") +@pytest.mark.skipif( + not check_chemfiles_version(), reason="Wrong version of chemfiles" +) class TestChemfileXYZ(MultiframeReaderTest): @staticmethod @pytest.fixture @@ -97,7 +99,9 @@ def __init__(self): self.dimensions = None -@pytest.mark.skipif(not check_chemfiles_version(), reason="Wrong version of chemfiles") +@pytest.mark.skipif( + not check_chemfiles_version(), reason="Wrong version of chemfiles" +) class TestChemfilesReader(MultiframeReaderTest): @staticmethod @pytest.fixture() @@ -142,7 +146,9 @@ def test_copy(self, ref): assert_allclose(original.ts.positions, copy.ts.positions) -@pytest.mark.skipif(not check_chemfiles_version(), reason="Wrong version of chemfiles") +@pytest.mark.skipif( + not check_chemfiles_version(), reason="Wrong version of chemfiles" +) class TestChemfilesWriter(BaseWriterTest): @staticmethod @pytest.fixture() @@ -159,7 +165,9 @@ def test_no_extension_raises(self, ref): ref.writer("foo") -@pytest.mark.skipif(not check_chemfiles_version(), reason="Wrong version of chemfiles") +@pytest.mark.skipif( + not check_chemfiles_version(), reason="Wrong version of chemfiles" +) class TestChemfiles(object): def test_read_chemfiles_format(self): u = mda.Universe( @@ -183,7 +191,9 @@ def test_changing_system_size(self, tmpdir): with open(outfile, "w") as fd: fd.write(VARYING_XYZ) - u = mda.Universe(outfile, format="chemfiles", topology_format="XYZ") + u = mda.Universe( + outfile, format="chemfiles", topology_format="XYZ" + ) with pytest.raises(IOError): u.trajectory._read_next_timestep() @@ -194,7 +204,9 @@ def test_wrong_open_mode(self): def check_topology(self, reference, file): u = mda.Universe(reference) - atoms = set([(atom.name, atom.type, atom.record_type) for atom in u.atoms]) + atoms = set( + [(atom.name, atom.type, atom.record_type) for atom in u.atoms] + ) bonds = set([(bond.atoms[0].ix, bond.atoms[1].ix) for bond in u.bonds]) check = mda.Universe(file) diff --git a/testsuite/MDAnalysisTests/coordinates/test_copying.py b/testsuite/MDAnalysisTests/coordinates/test_copying.py index 91fb87fd465..6a6cd865906 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_copying.py +++ b/testsuite/MDAnalysisTests/coordinates/test_copying.py @@ -21,6 +21,7 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # import numpy as np + try: from numpy import shares_memory except ImportError: @@ -64,39 +65,41 @@ from MDAnalysis.auxiliary.EDR import HAS_PYEDR -@pytest.fixture(params=[ - # formatname, filename - ('CRD', CRD, dict()), - ('DATA', LAMMPSdata_mini, dict(n_atoms=1)), - ('DCD', DCD, dict()), - ('DMS', DMS, dict()), - ('CONFIG', DLP_CONFIG, dict()), - ('HISTORY', DLP_HISTORY, dict()), - ('INPCRD', INPCRD, dict()), - ('GMS', GMS_ASYMOPT, dict()), - ('GRO', GRO, dict()), - ('MMTF', MMTF, dict()), - ('MOL2', mol2_molecules, dict()), - ('PDB', PDB_small, dict()), - ('PQR', PQR, dict()), - ('PDBQT', PDBQT_input, dict()), - ('TRR', TRR, dict()), - ('TRZ', TRZ, dict(n_atoms=8184)), - ('TRJ', TRJ, dict(n_atoms=252)), - ('XTC', XTC, dict()), - ('XPDB', XPDB_small, dict()), - ('XYZ', XYZ_mini, dict()), - ('NCDF', NCDF, dict()), - ('memory', np.arange(60).reshape(2, 10, 3).astype(np.float64), dict()), - ('CHAIN', [GRO, GRO, GRO], dict()), - ('FHIAIMS', FHIAIMS, dict()), - pytest.param( - ('GSD', GSD, dict()), - marks=pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') - ), - ('NAMDBIN', NAMDBIN, dict()), - ('TXYZ', TXYZ, dict()), -]) +@pytest.fixture( + params=[ + # formatname, filename + ("CRD", CRD, dict()), + ("DATA", LAMMPSdata_mini, dict(n_atoms=1)), + ("DCD", DCD, dict()), + ("DMS", DMS, dict()), + ("CONFIG", DLP_CONFIG, dict()), + ("HISTORY", DLP_HISTORY, dict()), + ("INPCRD", INPCRD, dict()), + ("GMS", GMS_ASYMOPT, dict()), + ("GRO", GRO, dict()), + ("MMTF", MMTF, dict()), + ("MOL2", mol2_molecules, dict()), + ("PDB", PDB_small, dict()), + ("PQR", PQR, dict()), + ("PDBQT", PDBQT_input, dict()), + ("TRR", TRR, dict()), + ("TRZ", TRZ, dict(n_atoms=8184)), + ("TRJ", TRJ, dict(n_atoms=252)), + ("XTC", XTC, dict()), + ("XPDB", XPDB_small, dict()), + ("XYZ", XYZ_mini, dict()), + ("NCDF", NCDF, dict()), + ("memory", np.arange(60).reshape(2, 10, 3).astype(np.float64), dict()), + ("CHAIN", [GRO, GRO, GRO], dict()), + ("FHIAIMS", FHIAIMS, dict()), + pytest.param( + ("GSD", GSD, dict()), + marks=pytest.mark.skipif(not HAS_GSD, reason="gsd not installed"), + ), + ("NAMDBIN", NAMDBIN, dict()), + ("TXYZ", TXYZ, dict()), + ] +) def ref_reader(request): fmt_name, filename, extras = request.param @@ -108,45 +111,52 @@ def ref_reader(request): r.close() -@pytest.fixture(params=[ - # formatname, filename - ('CRD', CRD, dict()), - ('DATA', LAMMPSdata_mini, dict(n_atoms=1)), - ('DCD', DCD, dict()), - ('DMS', DMS, dict()), - ('CONFIG', DLP_CONFIG, dict()), - ('HISTORY', DLP_HISTORY, dict()), - ('INPCRD', INPCRD, dict()), - ('GMS', GMS_ASYMOPT, dict()), - ('GRO', GRO, dict()), - ('MMTF', MMTF, dict()), - ('MOL2', mol2_molecules, dict()), - ('PDB', PDB_small, dict()), - ('PQR', PQR, dict()), - ('PDBQT', PDBQT_input, dict()), - ('TRR', TRR, dict()), - ('TRZ', TRZ, dict(n_atoms=8184)), - ('TRJ', TRJ, dict(n_atoms=252)), - ('XTC', XTC, dict()), - ('XPDB', XPDB_small, dict()), - ('XYZ', XYZ_mini, dict()), - ('NCDF', NCDF, dict(mmap=False)), - ('memory', np.arange(60).reshape(2, 10, 3).astype(np.float64), dict()), - ('CHAIN', [GRO, GRO, GRO], dict()), - ('FHIAIMS', FHIAIMS, dict()), - pytest.param( - ('GSD', GSD, dict()), - marks=pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') - ), - ('NAMDBIN', NAMDBIN, dict()), - ('TXYZ', TXYZ, dict()), -]) +@pytest.fixture( + params=[ + # formatname, filename + ("CRD", CRD, dict()), + ("DATA", LAMMPSdata_mini, dict(n_atoms=1)), + ("DCD", DCD, dict()), + ("DMS", DMS, dict()), + ("CONFIG", DLP_CONFIG, dict()), + ("HISTORY", DLP_HISTORY, dict()), + ("INPCRD", INPCRD, dict()), + ("GMS", GMS_ASYMOPT, dict()), + ("GRO", GRO, dict()), + ("MMTF", MMTF, dict()), + ("MOL2", mol2_molecules, dict()), + ("PDB", PDB_small, dict()), + ("PQR", PQR, dict()), + ("PDBQT", PDBQT_input, dict()), + ("TRR", TRR, dict()), + ("TRZ", TRZ, dict(n_atoms=8184)), + ("TRJ", TRJ, dict(n_atoms=252)), + ("XTC", XTC, dict()), + ("XPDB", XPDB_small, dict()), + ("XYZ", XYZ_mini, dict()), + ("NCDF", NCDF, dict(mmap=False)), + ("memory", np.arange(60).reshape(2, 10, 3).astype(np.float64), dict()), + ("CHAIN", [GRO, GRO, GRO], dict()), + ("FHIAIMS", FHIAIMS, dict()), + pytest.param( + ("GSD", GSD, dict()), + marks=pytest.mark.skipif(not HAS_GSD, reason="gsd not installed"), + ), + ("NAMDBIN", NAMDBIN, dict()), + ("TXYZ", TXYZ, dict()), + ] +) def ref_reader_extra_args(request): fmt_name, filename, extras = request.param r = get_reader_for(filename, format=fmt_name)( - filename, convert_units=False, dt=2, time_offset=10, - foo="bar", **extras) + filename, + convert_units=False, + dt=2, + time_offset=10, + foo="bar", + **extras, + ) try: yield r finally: @@ -189,23 +199,23 @@ def test_reader_copied_extra_attributes(original_and_copy_extra_args): # memory reader subclass protoreader directly and # therefore don't have convert_units or _ts_kwargs if original.__class__.__bases__[0].__name__ != "ProtoReader": - assert original.format is not 'MEMORY' + assert original.format is not "MEMORY" assert original.convert_units is False assert copy.convert_units is False - assert original._ts_kwargs['time_offset'] == 10 - assert copy._ts_kwargs['time_offset'] == 10 - assert original._ts_kwargs['dt'] == 2 - assert copy._ts_kwargs['dt'] == 2 + assert original._ts_kwargs["time_offset"] == 10 + assert copy._ts_kwargs["time_offset"] == 10 + assert original._ts_kwargs["dt"] == 2 + assert copy._ts_kwargs["dt"] == 2 - assert original.ts.data['time_offset'] == 10 - assert copy.ts.data['time_offset'] == 10 + assert original.ts.data["time_offset"] == 10 + assert copy.ts.data["time_offset"] == 10 # Issue #3689 XTC and XDR overwrite `dt` - if original.format not in ('XTC', 'TRR'): - assert original.ts.data['dt'] == 2 - assert copy.ts.data['dt'] == 2 + if original.format not in ("XTC", "TRR"): + assert original.ts.data["dt"] == 2 + assert copy.ts.data["dt"] == 2 - assert copy._kwargs['foo'] == 'bar' + assert copy._kwargs["foo"] == "bar" # checking that non-base attributes are also copied (netcdf reader) if hasattr(original, "_mmap"): @@ -217,7 +227,7 @@ def test_reader_independent_iteration(original_and_copy): # check that the two Readers iterate independently original, copy = original_and_copy if len(original) < 2: - pytest.skip('Single frame reader') + pytest.skip("Single frame reader") # initially at same frame assert original.ts.frame == copy.ts.frame @@ -231,7 +241,7 @@ def test_reader_initial_frame_maintained(original_and_copy): original, _ = original_and_copy if len(original) < 2: - pytest.skip('Single frame reader') + pytest.skip("Single frame reader") # seek original[1] @@ -241,6 +251,7 @@ def test_reader_initial_frame_maintained(original_and_copy): assert original.ts.frame == copy.ts.frame assert_equal(original.ts.positions, copy.ts.positions) + def test_reader_initial_next(original_and_copy): # check that the filehandle (or equivalent) in the copied Reader # is identical to the original @@ -248,7 +259,7 @@ def test_reader_initial_next(original_and_copy): original, _ = original_and_copy if len(original) < 3: - pytest.skip('Requires 3 frames') + pytest.skip("Requires 3 frames") original[1] @@ -259,7 +270,7 @@ def test_reader_initial_next(original_and_copy): original.next() copy.next() - + assert original.ts.frame == copy.ts.frame assert_equal(original.ts.positions, copy.ts.positions) @@ -277,8 +288,9 @@ def test_timestep_copied(ref_reader): assert new.ts.positions.dtype == np.float32 -@pytest.mark.skipif(shares_memory == False, - reason='old numpy lacked shares_memory') +@pytest.mark.skipif( + shares_memory == False, reason="old numpy lacked shares_memory" +) def test_positions_share_memory(original_and_copy): # check that the memory in Timestep objects is unique original, copy = original_and_copy diff --git a/testsuite/MDAnalysisTests/coordinates/test_crd.py b/testsuite/MDAnalysisTests/coordinates/test_crd.py index cffde92d75a..be68d82eae8 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_crd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_crd.py @@ -23,9 +23,7 @@ from collections import OrderedDict import pytest -from numpy.testing import ( - assert_equal,assert_allclose -) +from numpy.testing import assert_equal, assert_allclose import MDAnalysis as mda import os @@ -41,10 +39,11 @@ def u(self): @pytest.fixture() def outfile(self, tmpdir): - return os.path.join(str(tmpdir), 'test.crd') + return os.path.join(str(tmpdir), "test.crd") - @pytest.mark.parametrize('testfile', - ['test.crd', 'test.crd.bz2', 'test.crd.gz']) + @pytest.mark.parametrize( + "testfile", ["test.crd", "test.crd.bz2", "test.crd.gz"] + ) def test_write_atoms(self, u, testfile, tmpdir): # Test that written file when read gives same coordinates with tmpdir.as_cwd(): @@ -52,8 +51,7 @@ def test_write_atoms(self, u, testfile, tmpdir): u2 = mda.Universe(testfile) - assert_equal(u.atoms.positions, - u2.atoms.positions) + assert_equal(u.atoms.positions, u2.atoms.positions) def test_roundtrip(self, u, outfile): # Write out a copy of the Universe, and compare this against the original @@ -62,9 +60,9 @@ def test_roundtrip(self, u, outfile): u.atoms.write(outfile) def CRD_iter(fn): - with open(fn, 'r') as inf: + with open(fn, "r") as inf: for line in inf: - if not line.startswith('*'): + if not line.startswith("*"): yield line for ref, other in zip(CRD_iter(CRD), CRD_iter(outfile)): @@ -74,9 +72,9 @@ def test_write_EXT(self, u, outfile): # Use the `extended` keyword to force the EXT format u.atoms.write(outfile, extended=True) - with open(outfile, 'r') as inf: + with open(outfile, "r") as inf: format_line = inf.readlines()[2] - assert 'EXT' in format_line, "EXT format expected" + assert "EXT" in format_line, "EXT format expected" def test_write_EXT_read(self, u, outfile): # Read EXT format and check atom positions @@ -84,56 +82,61 @@ def test_write_EXT_read(self, u, outfile): u2 = mda.Universe(outfile) - sel1 = u.select_atoms('all') - sel2 = u2.select_atoms('all') + sel1 = u.select_atoms("all") + sel2 = u2.select_atoms("all") cog1 = sel1.center_of_geometry() cog2 = sel2.center_of_geometry() - assert_equal(len(u.atoms), len(u2.atoms)), 'Equal number of '\ - 'atoms expected in both CRD formats' - assert_equal(len(u.atoms.residues), - len(u2.atoms.residues)), 'Equal number of residues expected in'\ - 'both CRD formats' - assert_equal(len(u.atoms.segments), - len(u2.atoms.segments)), 'Equal number of segments expected in'\ - 'both CRD formats' - assert_allclose(cog1, cog2, rtol=1e-6, atol=0), 'Same centroid expected for both CRD formats' + assert_equal( + len(u.atoms), len(u2.atoms) + ), "Equal number of " "atoms expected in both CRD formats" + assert_equal( + len(u.atoms.residues), len(u2.atoms.residues) + ), "Equal number of residues expected in" "both CRD formats" + assert_equal( + len(u.atoms.segments), len(u2.atoms.segments) + ), "Equal number of segments expected in" "both CRD formats" + assert_allclose( + cog1, cog2, rtol=1e-6, atol=0 + ), "Same centroid expected for both CRD formats" class TestCRDWriterMissingAttrs(object): # All required attributes with the default value - req_attrs = OrderedDict([ - ('resnames', 'UNK'), - ('resids', 1), - ('names', 'X'), - ('tempfactors', 0.0), - ]) - - @pytest.mark.parametrize('missing_attr', req_attrs) + req_attrs = OrderedDict( + [ + ("resnames", "UNK"), + ("resids", 1), + ("names", "X"), + ("tempfactors", 0.0), + ] + ) + + @pytest.mark.parametrize("missing_attr", req_attrs) def test_warns(self, missing_attr, tmpdir): attrs = list(self.req_attrs.keys()) attrs.remove(missing_attr) u = make_Universe(attrs, trajectory=True) - outfile = str(tmpdir) + '/out.crd' + outfile = str(tmpdir) + "/out.crd" with pytest.warns(UserWarning): u.atoms.write(outfile) - @pytest.mark.parametrize('missing_attr', req_attrs) + @pytest.mark.parametrize("missing_attr", req_attrs) def test_write(self, missing_attr, tmpdir): attrs = list(self.req_attrs.keys()) attrs.remove(missing_attr) u = make_Universe(attrs, trajectory=True) - outfile = str(tmpdir) + '/out.crd' + outfile = str(tmpdir) + "/out.crd" u.atoms.write(outfile) u2 = mda.Universe(outfile) # Check all other attrs aren't disturbed for attr in attrs: - assert_equal(getattr(u.atoms, attr), - getattr(u2.atoms, attr)) + assert_equal(getattr(u.atoms, attr), getattr(u2.atoms, attr)) # Check missing attr is as expected - assert_equal(getattr(u2.atoms, missing_attr), - self.req_attrs[missing_attr]) + assert_equal( + getattr(u2.atoms, missing_attr), self.req_attrs[missing_attr] + ) diff --git a/testsuite/MDAnalysisTests/coordinates/test_dcd.py b/testsuite/MDAnalysisTests/coordinates/test_dcd.py index 9d27f8163f7..32dadddd963 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_dcd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_dcd.py @@ -27,18 +27,34 @@ import MDAnalysis as mda from MDAnalysis.coordinates.DCD import DCDReader -from numpy.testing import (assert_equal, assert_array_equal, - assert_almost_equal, assert_array_almost_equal) - -from MDAnalysisTests.datafiles import (DCD, PSF, DCD_empty, PRMncdf, NCDF, - COORDINATES_TOPOLOGY, COORDINATES_DCD, - PSF_TRICLINIC, DCD_TRICLINIC, - PSF_NAMD_TRICLINIC, DCD_NAMD_TRICLINIC, - PSF_NAMD_GBIS, DCD_NAMD_GBIS, - PDB_closed) -from MDAnalysisTests.coordinates.base import (MultiframeReaderTest, - BaseReference, - BaseWriterTest) +from numpy.testing import ( + assert_equal, + assert_array_equal, + assert_almost_equal, + assert_array_almost_equal, +) + +from MDAnalysisTests.datafiles import ( + DCD, + PSF, + DCD_empty, + PRMncdf, + NCDF, + COORDINATES_TOPOLOGY, + COORDINATES_DCD, + PSF_TRICLINIC, + DCD_TRICLINIC, + PSF_NAMD_TRICLINIC, + DCD_NAMD_TRICLINIC, + PSF_NAMD_GBIS, + DCD_NAMD_GBIS, + PDB_closed, +) +from MDAnalysisTests.coordinates.base import ( + MultiframeReaderTest, + BaseReference, + BaseWriterTest, +) import pytest @@ -50,7 +66,7 @@ def __init__(self): self.topology = COORDINATES_TOPOLOGY self.reader = mda.coordinates.DCD.DCDReader self.writer = mda.coordinates.DCD.DCDWriter - self.ext = 'xtc' + self.ext = "xtc" self.prec = 3 self.changing_dimensions = True @@ -76,37 +92,47 @@ def test_with_statement(self): assert_equal( N, 98, - err_msg="with_statement: DCDReader reads wrong number of frames") + err_msg="with_statement: DCDReader reads wrong number of frames", + ) assert_array_equal( frames, np.arange(0, N), - err_msg="with_statement: DCDReader does not read all frames") + err_msg="with_statement: DCDReader does not read all frames", + ) def test_set_time(self): u = mda.Universe(PSF, DCD) - assert_almost_equal(u.trajectory.time, 1.0, - decimal=5) + assert_almost_equal(u.trajectory.time, 1.0, decimal=5) -@pytest.mark.parametrize('fstart', (0, 1, 2, 37, None)) +@pytest.mark.parametrize("fstart", (0, 1, 2, 37, None)) def test_write_istart(universe_dcd, tmpdir, fstart): - outfile = str(tmpdir.join('test.dcd')) - nsavc = universe_dcd.trajectory._file.header['nsavc'] + outfile = str(tmpdir.join("test.dcd")) + nsavc = universe_dcd.trajectory._file.header["nsavc"] istart = fstart * nsavc if fstart is not None else None - with mda.Writer(outfile, universe_dcd.atoms.n_atoms, - nsavc=nsavc, istart=istart) as w: + with mda.Writer( + outfile, universe_dcd.atoms.n_atoms, nsavc=nsavc, istart=istart + ) as w: for ts in universe_dcd.trajectory: w.write(universe_dcd.atoms) u = mda.Universe(PSF, outfile) - assert_almost_equal(u.trajectory._file.header['istart'], - istart if istart is not None else u.trajectory._file.header['nsavc']) + assert_almost_equal( + u.trajectory._file.header["istart"], + istart if istart is not None else u.trajectory._file.header["nsavc"], + ) # issue #1819 times = [ts.time for ts in u.trajectory] fstart = fstart if fstart is not None else 1 - ref_times = (np.arange(universe_dcd.trajectory.n_frames) + fstart) * universe_dcd.trajectory.dt - assert_almost_equal(times, ref_times, decimal=5, - err_msg="Times not identical after setting istart={}".format(istart)) + ref_times = ( + np.arange(universe_dcd.trajectory.n_frames) + fstart + ) * universe_dcd.trajectory.dt + assert_almost_equal( + times, + ref_times, + decimal=5, + err_msg="Times not identical after setting istart={}".format(istart), + ) class TestDCDWriter(BaseWriterTest): @@ -117,10 +143,9 @@ def ref(): def test_write_random_unitcell(tmpdir): - testname = str(tmpdir.join('test.dcd')) + testname = str(tmpdir.join("test.dcd")) rstate = np.random.RandomState(1178083) - random_unitcells = rstate.uniform( - high=80, size=(98, 6)).astype(np.float64) + random_unitcells = rstate.uniform(high=80, size=(98, 6)).astype(np.float64) u = mda.Universe(PSF, DCD) with mda.Writer(testname, n_atoms=u.atoms.n_atoms) as w: @@ -130,15 +155,15 @@ def test_write_random_unitcell(tmpdir): u2 = mda.Universe(PSF, testname) for index, ts in enumerate(u2.trajectory): - assert_array_almost_equal(u2.trajectory.dimensions, - random_unitcells[index], - decimal=5) + assert_array_almost_equal( + u2.trajectory.dimensions, random_unitcells[index], decimal=5 + ) def test_empty_dimension_warning(tmpdir): u = mda.Universe(PDB_closed) - testname = str(tmpdir.join('test.dcd')) + testname = str(tmpdir.join("test.dcd")) with mda.Writer(testname, n_atoms=u.atoms.n_atoms) as w: msg = "zeroed unitcell will be written" @@ -150,7 +175,8 @@ def test_empty_dimension_warning(tmpdir): # Legacy tests # ################ -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def universe_dcd(): return mda.Universe(PSF, DCD) @@ -171,8 +197,7 @@ def test_jump_last_frame(universe_dcd): assert universe_dcd.trajectory.ts.frame == 97 -@pytest.mark.parametrize("start, stop, step", ((5, 17, 3), - (20, 5, -1))) +@pytest.mark.parametrize("start, stop, step", ((5, 17, 3), (20, 5, -1))) def test_slice(universe_dcd, start, stop, step): frames = [ts.frame for ts in universe_dcd.trajectory[start:stop:step]] assert_array_equal(frames, np.arange(start, stop, step)) @@ -185,8 +210,9 @@ def test_array_like(universe_dcd, array_like): assert_array_equal(frames, ar) -@pytest.mark.parametrize("indices", ([0, 4, 2, 3, 0, 1], - [0, 0, 1, 1, 2, 1, 1])) +@pytest.mark.parametrize( + "indices", ([0, 4, 2, 3, 0, 1], [0, 0, 1, 1, 2, 1, 1]) +) def test_list_indices(universe_dcd, indices): frames = [ts.frame for ts in universe_dcd.trajectory[indices]] assert_array_equal(frames, indices) @@ -194,41 +220,63 @@ def test_list_indices(universe_dcd, indices): @pytest.mark.parametrize( "slice, length", - [([None, None, None], 98), ([0, None, None], 98), ([None, 98, None], 98), - ([None, None, 1], 98), ([None, None, -1], 98), ([2, 6, 2], 2), - ([0, 10, None], 10), ([2, 10, None], 8), ([0, 1, 1], 1), ([1, 1, 1], 0), - ([1, 2, 1], 1), ([1, 2, 2], 1), ([1, 4, 2], 2), ([1, 4, 4], 1), - ([0, 5, 5], 1), ([3, 5, 1], 2), ([4, 0, -1], 4), ([5, 0, -2], 3), - ([5, 0, -4], 2)]) + [ + ([None, None, None], 98), + ([0, None, None], 98), + ([None, 98, None], 98), + ([None, None, 1], 98), + ([None, None, -1], 98), + ([2, 6, 2], 2), + ([0, 10, None], 10), + ([2, 10, None], 8), + ([0, 1, 1], 1), + ([1, 1, 1], 0), + ([1, 2, 1], 1), + ([1, 2, 2], 1), + ([1, 4, 2], 2), + ([1, 4, 4], 1), + ([0, 5, 5], 1), + ([3, 5, 1], 2), + ([4, 0, -1], 4), + ([5, 0, -2], 3), + ([5, 0, -4], 2), + ], +) def test_timeseries_slices(slice, length, universe_dcd): start, stop, step = slice - allframes = universe_dcd.trajectory.timeseries(order='fac') - xyz = universe_dcd.trajectory.timeseries(start=start, stop=stop, step=step, - order='fac') + allframes = universe_dcd.trajectory.timeseries(order="fac") + xyz = universe_dcd.trajectory.timeseries( + start=start, stop=stop, step=step, order="fac" + ) assert len(xyz) == length assert_array_almost_equal(xyz, allframes[start:stop:step]) -@pytest.mark.parametrize("order, shape", ( - ('fac', (98, 3341, 3)), - ('fca', (98, 3, 3341)), - ('afc', (3341, 98, 3)), - ('acf', (3341, 3, 98)), - ('caf', (3, 3341, 98)), - ('cfa', (3, 98, 3341)), )) +@pytest.mark.parametrize( + "order, shape", + ( + ("fac", (98, 3341, 3)), + ("fca", (98, 3, 3341)), + ("afc", (3341, 98, 3)), + ("acf", (3341, 3, 98)), + ("caf", (3, 3341, 98)), + ("cfa", (3, 98, 3341)), + ), +) def test_timeseries_order(order, shape, universe_dcd): x = universe_dcd.trajectory.timeseries(order=order) assert x.shape == shape -@pytest.mark.parametrize("indices", [[1, 2, 3, 4], [5, 10, 15, 19], - [9, 4, 2, 0, 50]]) +@pytest.mark.parametrize( + "indices", [[1, 2, 3, 4], [5, 10, 15, 19], [9, 4, 2, 0, 50]] +) def test_timeseries_atomindices(indices, universe_dcd): - allframes = universe_dcd.trajectory.timeseries(order='afc') - asel = universe_dcd.atoms[indices] - xyz = universe_dcd.trajectory.timeseries(asel=asel, order='afc') - assert len(xyz) == len(indices) - assert_array_almost_equal(xyz, allframes[indices]) + allframes = universe_dcd.trajectory.timeseries(order="afc") + asel = universe_dcd.atoms[indices] + xyz = universe_dcd.trajectory.timeseries(asel=asel, order="afc") + assert len(xyz) == len(indices) + assert_array_almost_equal(xyz, allframes[indices]) def test_reader_set_dt(): @@ -236,17 +284,19 @@ def test_reader_set_dt(): frame = 3 u = mda.Universe(PSF, DCD, dt=dt) dcdheader = u.trajectory._file.header - fstart = dcdheader['istart'] / dcdheader['nsavc'] - assert_almost_equal(u.trajectory[frame].time, (frame + fstart)*dt, - err_msg="setting time step dt={0} failed: " - "actually used dt={1}".format( - dt, u.trajectory._ts_kwargs['dt'])) - assert_almost_equal(u.trajectory.dt, dt, - err_msg="trajectory.dt does not match set dt") - - -@pytest.mark.parametrize("ext, decimal", (("dcd", 4), - ("xtc", 3))) + fstart = dcdheader["istart"] / dcdheader["nsavc"] + assert_almost_equal( + u.trajectory[frame].time, + (frame + fstart) * dt, + err_msg="setting time step dt={0} failed: " + "actually used dt={1}".format(dt, u.trajectory._ts_kwargs["dt"]), + ) + assert_almost_equal( + u.trajectory.dt, dt, err_msg="trajectory.dt does not match set dt" + ) + + +@pytest.mark.parametrize("ext, decimal", (("dcd", 4), ("xtc", 3))) def test_writer_dt(tmpdir, ext, decimal): dt = 5.0 # set time step to 5 ps universe_dcd = mda.Universe(PSF, DCD, dt=dt) @@ -259,13 +309,21 @@ def test_writer_dt(tmpdir, ext, decimal): W.write(universe_dcd.atoms) uw = mda.Universe(PSF, outfile) - assert_almost_equal(uw.trajectory.totaltime, - (uw.trajectory.n_frames - 1) * dt, decimal=decimal, - err_msg="Total time mismatch for ext={}".format(ext)) + assert_almost_equal( + uw.trajectory.totaltime, + (uw.trajectory.n_frames - 1) * dt, + decimal=decimal, + err_msg="Total time mismatch for ext={}".format(ext), + ) times = np.array([uw.trajectory.time for ts in uw.trajectory]) frames = np.arange(1, uw.trajectory.n_frames + 1) # traj starts at 1*dt - assert_array_almost_equal(times, frames * dt, decimal=decimal, - err_msg="Times mismatch for ext={}".format(ext)) + assert_array_almost_equal( + times, + frames * dt, + decimal=decimal, + err_msg="Times mismatch for ext={}".format(ext), + ) + @pytest.mark.parametrize("variable, default", (("istart", 0), ("nsavc", 1))) def test_DCDWriter_default(tmpdir, variable, default): @@ -277,8 +335,8 @@ def test_DCDWriter_default(tmpdir, variable, default): header = DCD.header assert header[variable] == default -@pytest.mark.parametrize("ext, decimal", (("dcd", 5), - ("xtc", 2))) + +@pytest.mark.parametrize("ext, decimal", (("dcd", 5), ("xtc", 2))) def test_other_writer(universe_dcd, tmpdir, ext, decimal): t = universe_dcd.trajectory outfile = str(tmpdir.join("test.{}".format(ext))) @@ -288,14 +346,17 @@ def test_other_writer(universe_dcd, tmpdir, ext, decimal): uw = mda.Universe(PSF, outfile) # check that the coordinates are identical for each time step - for orig_ts, written_ts in zip(universe_dcd.trajectory, - uw.trajectory): - assert_array_almost_equal(written_ts.positions, orig_ts.positions, - decimal, - err_msg="coordinate mismatch between " - "original and written trajectory at " - "frame {} (orig) vs {} (written)".format( - orig_ts.frame, written_ts.frame)) + for orig_ts, written_ts in zip(universe_dcd.trajectory, uw.trajectory): + assert_array_almost_equal( + written_ts.positions, + orig_ts.positions, + decimal, + err_msg="coordinate mismatch between " + "original and written trajectory at " + "frame {} (orig) vs {} (written)".format( + orig_ts.frame, written_ts.frame + ), + ) def test_single_frame(universe_dcd, tmpdir): @@ -305,15 +366,17 @@ def test_single_frame(universe_dcd, tmpdir): W.write(u.atoms) w = mda.Universe(PSF, outfile) assert w.trajectory.n_frames == 1 - assert_almost_equal(w.atoms.positions, - u.atoms.positions, - 3, - err_msg="coordinates do not match") + assert_almost_equal( + w.atoms.positions, + u.atoms.positions, + 3, + err_msg="coordinates do not match", + ) def test_write_no_natoms(): with pytest.raises(ValueError): - mda.Writer('foobar.dcd') + mda.Writer("foobar.dcd") def test_writer_trajectory_no_natoms(tmpdir, universe_dcd): @@ -326,18 +389,20 @@ class RefCHARMMtriclinicDCD(object): trajectory = DCD_TRICLINIC # time(ps) A B C alpha beta gamma (length in Angstrome, angles in degrees) # dcd starts at t = 1ps - ref_dimensions = np.array([ - [1., 35.44604, 35.06156, 34.1585, 91.32802, 61.73521, 44.40703], - [2., 34.65957, 34.22689, 33.09897, 90.56206, 61.79192, 44.14549], - [3., 34.52772, 34.66422, 33.53881, 90.55859, 63.11228, 40.14044], - [4., 34.43749, 33.38432, 34.02133, 88.82457, 64.98057, 36.77397], - [5., 33.73129, 32.47752, 34.18961, 89.88102, 65.89032, 36.10921], - [6., 33.78703, 31.90317, 34.98833, 90.03092, 66.12877, 35.07141], - [7., 33.24708, 31.18271, 34.9654, 93.11122, 68.17743, 35.73643], - [8., 32.92599, 30.31393, 34.99197, 93.89051, 69.3799, 33.48945], - [9., 32.15295, 30.43056, 34.96157, 96.01416, 71.50115, 32.56111], - [10., 31.99748, 30.21518, 35.24292, 95.85821, 71.08429, 31.85939] - ]) + ref_dimensions = np.array( + [ + [1.0, 35.44604, 35.06156, 34.1585, 91.32802, 61.73521, 44.40703], + [2.0, 34.65957, 34.22689, 33.09897, 90.56206, 61.79192, 44.14549], + [3.0, 34.52772, 34.66422, 33.53881, 90.55859, 63.11228, 40.14044], + [4.0, 34.43749, 33.38432, 34.02133, 88.82457, 64.98057, 36.77397], + [5.0, 33.73129, 32.47752, 34.18961, 89.88102, 65.89032, 36.10921], + [6.0, 33.78703, 31.90317, 34.98833, 90.03092, 66.12877, 35.07141], + [7.0, 33.24708, 31.18271, 34.9654, 93.11122, 68.17743, 35.73643], + [8.0, 32.92599, 30.31393, 34.99197, 93.89051, 69.3799, 33.48945], + [9.0, 32.15295, 30.43056, 34.96157, 96.01416, 71.50115, 32.56111], + [10.0, 31.99748, 30.21518, 35.24292, 95.85821, 71.08429, 31.85939], + ] + ) class RefNAMDtriclinicDCD(object): @@ -346,25 +411,39 @@ class RefNAMDtriclinicDCD(object): # vmd topology trajectory # molinfo 0 get {a b c alpha beta gamma} # time(ps) A B C alpha beta gamma (length in Angstrome, angles in degrees) - ref_dimensions = np.array([ - [1., 38.426594, 38.393101, 44.759800, 90.000000, 90.000000, 60.028915], - ]) + ref_dimensions = np.array( + [ + [ + 1.0, + 38.426594, + 38.393101, + 44.759800, + 90.000000, + 90.000000, + 60.028915, + ], + ] + ) @pytest.mark.parametrize("ref", (RefCHARMMtriclinicDCD, RefNAMDtriclinicDCD)) def test_read_unitcell_triclinic(ref): u = mda.Universe(ref.topology, ref.trajectory) for ts, box in zip(u.trajectory, ref.ref_dimensions[:, 1:]): - assert_array_almost_equal(ts.dimensions, box, 4, - err_msg="box dimensions A,B,C,alpha," - "beta,gamma not identical at frame " - "{}".format(ts.frame)) + assert_array_almost_equal( + ts.dimensions, + box, + 4, + err_msg="box dimensions A,B,C,alpha," + "beta,gamma not identical at frame " + "{}".format(ts.frame), + ) @pytest.mark.parametrize("ref", (RefCHARMMtriclinicDCD, RefNAMDtriclinicDCD)) def test_write_unitcell_triclinic(ref, tmpdir): u = mda.Universe(ref.topology, ref.trajectory) - outfile = 'triclinic.dcd' + outfile = "triclinic.dcd" with tmpdir.as_cwd(): with u.trajectory.OtherWriter(outfile) as w: for ts in u.trajectory: @@ -372,15 +451,19 @@ def test_write_unitcell_triclinic(ref, tmpdir): w = mda.Universe(ref.topology, outfile) for ts_orig, ts_copy in zip(u.trajectory, w.trajectory): - assert_almost_equal(ts_orig.dimensions, ts_copy.dimensions, 4, - err_msg="DCD->DCD: unit cell dimensions wrong " - "at frame {0}".format(ts_orig.frame)) + assert_almost_equal( + ts_orig.dimensions, + ts_copy.dimensions, + 4, + err_msg="DCD->DCD: unit cell dimensions wrong " + "at frame {0}".format(ts_orig.frame), + ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def ncdf2dcd(tmpdir_factory): pytest.importorskip("netCDF4") - testfile = tmpdir_factory.mktemp('dcd').join('ncdf2dcd.dcd') + testfile = tmpdir_factory.mktemp("dcd").join("ncdf2dcd.dcd") testfile = str(testfile) ncdf = mda.Universe(PRMncdf, NCDF) with mda.Writer(testfile, n_atoms=ncdf.atoms.n_atoms) as w: @@ -392,32 +475,36 @@ def ncdf2dcd(tmpdir_factory): def test_ncdf2dcd_unitcell(ncdf2dcd): ncdf, dcd = ncdf2dcd for ts_ncdf, ts_dcd in zip(ncdf.trajectory, dcd.trajectory): - assert_almost_equal(ts_ncdf.dimensions, - ts_dcd.dimensions, - 3) + assert_almost_equal(ts_ncdf.dimensions, ts_dcd.dimensions, 3) def test_ncdf2dcd_coords(ncdf2dcd): ncdf, dcd = ncdf2dcd for ts_ncdf, ts_dcd in zip(ncdf.trajectory, dcd.trajectory): - assert_almost_equal(ts_ncdf.positions, - ts_dcd.positions, - 3) - -@pytest.fixture(params=[(PSF, DCD), - (PSF_TRICLINIC, DCD_TRICLINIC), - (PSF_NAMD_TRICLINIC, DCD_NAMD_TRICLINIC), - (PSF_NAMD_GBIS, DCD_NAMD_GBIS)]) + assert_almost_equal(ts_ncdf.positions, ts_dcd.positions, 3) + + +@pytest.fixture( + params=[ + (PSF, DCD), + (PSF_TRICLINIC, DCD_TRICLINIC), + (PSF_NAMD_TRICLINIC, DCD_NAMD_TRICLINIC), + (PSF_NAMD_GBIS, DCD_NAMD_GBIS), + ] +) def universe(request): psf, dcd = request.param yield mda.Universe(psf, dcd) + def test_ts_time(universe): # issue #1819 u = universe header = u.trajectory._file.header - ref_times = [(ts.frame + header['istart']/header['nsavc'])*ts.dt - for ts in u.trajectory] + ref_times = [ + (ts.frame + header["istart"] / header["nsavc"]) * ts.dt + for ts in u.trajectory + ] times = [ts.time for ts in u.trajectory] assert_almost_equal(times, ref_times, decimal=5) diff --git a/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py b/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py index 81b1aded061..63d0a09464c 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py +++ b/testsuite/MDAnalysisTests/coordinates/test_dlpoly.py @@ -24,12 +24,17 @@ import numpy as np import pytest -from numpy.testing import (assert_equal, assert_allclose) +from numpy.testing import assert_equal, assert_allclose -from MDAnalysisTests.datafiles import (DLP_CONFIG, DLP_CONFIG_minimal, - DLP_CONFIG_order, DLP_HISTORY, - DLP_HISTORY_minimal, DLP_HISTORY_order, - DLP_HISTORY_minimal_cell) +from MDAnalysisTests.datafiles import ( + DLP_CONFIG, + DLP_CONFIG_minimal, + DLP_CONFIG_order, + DLP_HISTORY, + DLP_HISTORY_minimal, + DLP_HISTORY_order, + DLP_HISTORY_minimal_cell, +) class _DLPConfig(object): @@ -46,10 +51,16 @@ def ts(self, rd): return rd.ts def test_read_unitcell(self, ts): - ref = np.array([[18.6960000000, 0.0000000000, 0.0000000000], - [0.0000000000, 18.6960000000, 0.0000000000], - [0.0000000000, 0.0000000000, 18.6960000000]]) - assert_allclose(ts.dimensions, mda.coordinates.core.triclinic_box(*ref)) + ref = np.array( + [ + [18.6960000000, 0.0000000000, 0.0000000000], + [0.0000000000, 18.6960000000, 0.0000000000], + [0.0000000000, 0.0000000000, 18.6960000000], + ] + ) + assert_allclose( + ts.dimensions, mda.coordinates.core.triclinic_box(*ref) + ) def test_positions(self, ts): ref = np.array([-7.608595309, -7.897790000, -7.892053559]) @@ -84,19 +95,19 @@ def test_read_unitcell(self): # cythonised class can no longer raise AttributeError # so changed to test of has_... properties def test_velocities(self, ts): - assert(ts.has_velocities == False) + assert ts.has_velocities == False def test_forces(self, ts): - assert(ts.has_forces == False) + assert ts.has_forces == False class _DLPConfig2(object): @pytest.fixture() def u(self): - return mda.Universe(self.f, format='CONFIG') + return mda.Universe(self.f, format="CONFIG") def test_names(self, u): - ref = ['C', 'B', 'A'] + ref = ["C", "B", "A"] assert_equal([a.name for a in u.atoms], ref) def test_pos(self, u): @@ -104,7 +115,7 @@ def test_pos(self, u): assert_allclose(u.atoms[2].position, ref) def test_vel(self, u): - ref = np.array([2.637614561, 0.5778767520E-01, -1.704765568]) + ref = np.array([2.637614561, 0.5778767520e-01, -1.704765568]) assert_allclose(u.atoms[2].velocity, ref) def test_for(self, u): @@ -135,8 +146,7 @@ def test_for(self): class _DLHistory(object): @pytest.fixture() def u(self): - return mda.Universe(self.f, format='HISTORY') - + return mda.Universe(self.f, format="HISTORY") def test_len(self, u): assert_equal(len(u.trajectory), 3) @@ -155,38 +165,64 @@ def test_slicing_2(self, u): assert_equal(nums, [1]) def test_position(self, u): - ref = np.array([[-7.595541651, -7.898808509, -7.861763110 - ], [-7.019565641, -7.264933320, -7.045213551], - [-6.787470785, -6.912685099, -6.922156843]]) + ref = np.array( + [ + [-7.595541651, -7.898808509, -7.861763110], + [-7.019565641, -7.264933320, -7.045213551], + [-6.787470785, -6.912685099, -6.922156843], + ] + ) for ts, r in zip(u.trajectory, ref): assert_allclose(u.atoms[0].position, r) def test_velocity(self, u): - ref = np.array([[1.109901682, -1.500264697, 4.752251711 - ], [-1.398479696, 2.091141311, 1.957430003], - [0.2570827995, -0.7146878577, -3.547444215]]) + ref = np.array( + [ + [1.109901682, -1.500264697, 4.752251711], + [-1.398479696, 2.091141311, 1.957430003], + [0.2570827995, -0.7146878577, -3.547444215], + ] + ) for ts, r in zip(u.trajectory, ref): assert_allclose(u.atoms[0].velocity, r) def test_force(self, u): - ref = np.array([[-2621.386432, 1579.334443, 1041.103241 - ], [-1472.262341, 2450.379615, -8149.916193], - [2471.802059, -3828.467296, 3596.679326]]) + ref = np.array( + [ + [-2621.386432, 1579.334443, 1041.103241], + [-1472.262341, 2450.379615, -8149.916193], + [2471.802059, -3828.467296, 3596.679326], + ] + ) for ts, r in zip(u.trajectory, ref): assert_allclose(u.atoms[0].force, r) def test_unitcell(self, u): - ref1 = np.array([[18.6796195135, 0.0000058913, -0.0000139999 - ], [0.0000058913, 18.6794658887, -0.0000016255], - [-0.0000139999, -0.0000016255, 18.6797229304]]) - ref2 = np.array([[17.2277221163, -0.0044216126, -0.0003229237 - ], [-0.0044205826, 17.2124253987, 0.0019439244], - [-0.0003226531, 0.0019445826, 17.2416976104]]) - ref3 = np.array([[16.5435673205, -0.0108424742, 0.0014935464 - ], [-0.0108333201, 16.5270298891, 0.0011094612], - [0.0014948739, 0.0011058349, 16.5725517831]]) + ref1 = np.array( + [ + [18.6796195135, 0.0000058913, -0.0000139999], + [0.0000058913, 18.6794658887, -0.0000016255], + [-0.0000139999, -0.0000016255, 18.6797229304], + ] + ) + ref2 = np.array( + [ + [17.2277221163, -0.0044216126, -0.0003229237], + [-0.0044205826, 17.2124253987, 0.0019439244], + [-0.0003226531, 0.0019445826, 17.2416976104], + ] + ) + ref3 = np.array( + [ + [16.5435673205, -0.0108424742, 0.0014935464], + [-0.0108333201, 16.5270298891, 0.0011094612], + [0.0014948739, 0.0011058349, 16.5725517831], + ] + ) for ts, r in zip(u.trajectory, [ref1, ref2, ref3]): - assert_allclose(ts.dimensions, mda.coordinates.core.triclinic_box(*r)) + assert_allclose( + ts.dimensions, mda.coordinates.core.triclinic_box(*r) + ) class TestDLPolyHistory(_DLHistory): @@ -202,11 +238,11 @@ class TestDLPolyHistoryMinimal(_DLHistory): def test_velocity(self, u): with pytest.raises(mda.NoDataError): - getattr(u.atoms[0], 'velocity') + getattr(u.atoms[0], "velocity") def test_force(self, u): with pytest.raises(mda.NoDataError): - getattr(u.atoms[0], 'force') + getattr(u.atoms[0], "force") def test_unitcell(self): pass @@ -217,8 +253,8 @@ class TestDLPolyHistoryMinimalCell(_DLHistory): def test_velocity(self, u): with pytest.raises(mda.NoDataError): - getattr(u.atoms[0], 'velocity') + getattr(u.atoms[0], "velocity") def test_force(self, u): with pytest.raises(mda.NoDataError): - getattr(u.atoms[0], 'force') + getattr(u.atoms[0], "force") diff --git a/testsuite/MDAnalysisTests/coordinates/test_dms.py b/testsuite/MDAnalysisTests/coordinates/test_dms.py index 01823a6ec66..e4d9ac1ee64 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_dms.py +++ b/testsuite/MDAnalysisTests/coordinates/test_dms.py @@ -28,7 +28,7 @@ from MDAnalysis.lib.mdamath import triclinic_vectors -from MDAnalysisTests.datafiles import (DMS) +from MDAnalysisTests.datafiles import DMS class TestDMSReader(object): @@ -44,7 +44,7 @@ def test_global_cell(self, ts): assert ts.dimensions is None # cythonised class can no longer raise AttributeError - # so changed to test of has_velocities + # so changed to test of has_velocities def test_velocities(self, ts): assert_equal(ts.has_velocities, False) @@ -56,28 +56,39 @@ def test_number_of_coords(self, universe): def test_coords_atom_0(self, universe): # Desired coordinates taken directly from the SQLite file. Check unit # conversion - coords_0 = np.array([-11.0530004501343, - 26.6800003051758, - 12.7419996261597, ], - dtype=np.float32) + coords_0 = np.array( + [ + -11.0530004501343, + 26.6800003051758, + 12.7419996261597, + ], + dtype=np.float32, + ) assert_equal(universe.atoms[0].position, coords_0) def test_n_frames(self, universe): - assert_equal(universe.trajectory.n_frames, 1, - "wrong number of frames in pdb") + assert_equal( + universe.trajectory.n_frames, 1, "wrong number of frames in pdb" + ) def test_time(self, universe): - assert_equal(universe.trajectory.time, 0.0, - "wrong time of the frame") + assert_equal(universe.trajectory.time, 0.0, "wrong time of the frame") def test_frame(self, universe): - assert_equal(universe.trajectory.frame, 0, "wrong frame number " - "(0-based, should be 0 for single frame readers)") + assert_equal( + universe.trajectory.frame, + 0, + "wrong frame number " + "(0-based, should be 0 for single frame readers)", + ) def test_frame_index_0(self, universe): universe.trajectory[0] - assert_equal(universe.trajectory.ts.frame, 0, - "frame number for frame index 0 should be 0") + assert_equal( + universe.trajectory.ts.frame, + 0, + "frame number for frame index 0 should be 0", + ) def test_frame_index_1_raises_IndexError(self, universe): with pytest.raises(IndexError): diff --git a/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py b/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py index 463ddc59075..f1ec55b1917 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py +++ b/testsuite/MDAnalysisTests/coordinates/test_fhiaims.py @@ -27,54 +27,55 @@ import MDAnalysis as mda import numpy as np from MDAnalysisTests import make_Universe -from MDAnalysisTests.coordinates.base import ( - _SingleFrameReader, BaseWriterTest) +from MDAnalysisTests.coordinates.base import _SingleFrameReader, BaseWriterTest from MDAnalysisTests.datafiles import FHIAIMS -from numpy.testing import (assert_equal, - assert_array_almost_equal, - assert_almost_equal) +from numpy.testing import ( + assert_equal, + assert_array_almost_equal, + assert_almost_equal, +) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def universe(): return mda.Universe(FHIAIMS, FHIAIMS) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def universe_from_one_file(): return mda.Universe(FHIAIMS) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def ref(): return RefFHIAIMS() -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def good_input_natural_units(): - buffer = 'atom 0.1 0.2 0.3 H\natom 0.2 0.4 0.6 H\natom 0.3 0.6 0.9 H' + buffer = "atom 0.1 0.2 0.3 H\natom 0.2 0.4 0.6 H\natom 0.3 0.6 0.9 H" return StringIO(buffer) -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def good_input_with_velocity(): - buffer = 'atom 0.1 0.1 0.1 H\nvelocity 0.1 0.1 0.1' + buffer = "atom 0.1 0.1 0.1 H\nvelocity 0.1 0.1 0.1" return StringIO(buffer) class RefFHIAIMS(object): - from MDAnalysis.coordinates.FHIAIMS import (FHIAIMSReader, FHIAIMSWriter) + from MDAnalysis.coordinates.FHIAIMS import FHIAIMSReader, FHIAIMSWriter filename, trajectory, topology = [FHIAIMS] * 3 reader, writer = FHIAIMSReader, FHIAIMSWriter - pos_atom1 = np.asarray( - [6.861735, 2.103823, 37.753513], dtype=np.float32) + pos_atom1 = np.asarray([6.861735, 2.103823, 37.753513], dtype=np.float32) dimensions = np.asarray( - [18.6, 18.6, 55.8, 90., 90., 90.], dtype=np.float32) + [18.6, 18.6, 55.8, 90.0, 90.0, 90.0], dtype=np.float32 + ) n_atoms = 6 n_frames = 1 time = 0.0 - ext = '.in' + ext = ".in" prec = 6 container_format = True changing_dimensions = False @@ -84,88 +85,119 @@ class TestFHIAIMSReader(object): prec = 6 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def bad_input_missing_periodicity(self): - buffer = 'lattice_vector 1.0 0.0 0.0\nlattice_vector 0.0 1.0 0.0\natom 0.1 0.1 0.1 H' + buffer = "lattice_vector 1.0 0.0 0.0\nlattice_vector 0.0 1.0 0.0\natom 0.1 0.1 0.1 H" return StringIO(buffer) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def bad_input_relative_positions_no_box(self): - buffer = 'atom_frac 0.1 0.1 0.1 H' + buffer = "atom_frac 0.1 0.1 0.1 H" return StringIO(buffer) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def bad_input_missing_velocity(self): - buffer = 'atom 0.1 0.1 0.1 H\natom 0.2 0.2 0.2 H\nvelocity 0.1 0.1 0.1' + buffer = "atom 0.1 0.1 0.1 H\natom 0.2 0.2 0.2 H\nvelocity 0.1 0.1 0.1" return StringIO(buffer) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def bad_input_velocity_wrong_position(self): - buffer = 'atom 0.1 0.1 0.1 H\natom 0.2 0.2 0.2 H\nvelocity 0.1 0.1 0.1\nvelocity 0.1 0.1 0.1' + buffer = "atom 0.1 0.1 0.1 H\natom 0.2 0.2 0.2 H\nvelocity 0.1 0.1 0.1\nvelocity 0.1 0.1 0.1" return StringIO(buffer) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def bad_input_wrong_input_line(self): - buffer = 'garbage' + buffer = "garbage" return StringIO(buffer) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def good_input_mixed_units(self): - buffer = 'lattice_vector 1.0 0.0 0.0\nlattice_vector 0.0 2.0 0.0\nlattice_vector 0.0 0.0 3.0\natom 0.1 0.2 0.3 H\natom_frac 0.2 0.2 0.2 H\natom_frac 0.3 0.3 0.3 H' + buffer = "lattice_vector 1.0 0.0 0.0\nlattice_vector 0.0 2.0 0.0\nlattice_vector 0.0 0.0 3.0\natom 0.1 0.2 0.3 H\natom_frac 0.2 0.2 0.2 H\natom_frac 0.3 0.3 0.3 H" return StringIO(buffer) def test_single_file(self, universe, universe_from_one_file): - assert_almost_equal(universe.atoms.positions, universe_from_one_file.atoms.positions, - self.prec, "FHIAIMSReader failed to load universe from single file") + assert_almost_equal( + universe.atoms.positions, + universe_from_one_file.atoms.positions, + self.prec, + "FHIAIMSReader failed to load universe from single file", + ) def test_uses_FHIAIMSReader(self, universe): from MDAnalysis.coordinates.FHIAIMS import FHIAIMSReader - assert isinstance(universe.trajectory, - FHIAIMSReader), "failed to choose FHIAIMSReader" + assert isinstance( + universe.trajectory, FHIAIMSReader + ), "failed to choose FHIAIMSReader" def test_dimensions(self, ref, universe): assert_almost_equal( - ref.dimensions, universe.dimensions, - self.prec, "FHIAIMSReader failed to get unitcell dimensions") + ref.dimensions, + universe.dimensions, + self.prec, + "FHIAIMSReader failed to get unitcell dimensions", + ) def test_n_atoms(self, ref, universe): assert_equal( - ref.n_atoms, universe.trajectory.n_atoms, - "FHIAIMSReader failed to get the right number of atoms") + ref.n_atoms, + universe.trajectory.n_atoms, + "FHIAIMSReader failed to get the right number of atoms", + ) def test_fhiaims_positions(self, ref, universe): # first particle - assert_almost_equal(ref.pos_atom1, - universe.atoms.positions[0], - self.prec, - "FHIAIMSReader failed to read coordinates properly") + assert_almost_equal( + ref.pos_atom1, + universe.atoms.positions[0], + self.prec, + "FHIAIMSReader failed to read coordinates properly", + ) def test_n_frames(self, ref, universe): - assert_equal(ref.n_frames, universe.trajectory.n_frames, - "wrong number of frames") + assert_equal( + ref.n_frames, + universe.trajectory.n_frames, + "wrong number of frames", + ) def test_time(self, ref, universe): - assert_equal(ref.time, universe.trajectory.time, - "wrong time of the frame") + assert_equal( + ref.time, universe.trajectory.time, "wrong time of the frame" + ) - def test_bad_input_missing_periodicity(self, bad_input_missing_periodicity): + def test_bad_input_missing_periodicity( + self, bad_input_missing_periodicity + ): with pytest.raises(ValueError, match="Found partial periodicity"): u = mda.Universe(bad_input_missing_periodicity, format="FHIAIMS") - def test_bad_input_relative_positions_no_box(self, bad_input_relative_positions_no_box): - with pytest.raises(ValueError, match="Found relative coordinates in FHI-AIMS file without lattice info"): + def test_bad_input_relative_positions_no_box( + self, bad_input_relative_positions_no_box + ): + with pytest.raises( + ValueError, + match="Found relative coordinates in FHI-AIMS file without lattice info", + ): u = mda.Universe( - bad_input_relative_positions_no_box, format="FHIAIMS") + bad_input_relative_positions_no_box, format="FHIAIMS" + ) def test_bad_input_missing_velocity(self, bad_input_missing_velocity): - with pytest.raises(ValueError, match="Found incorrect number of velocity tags"): + with pytest.raises( + ValueError, match="Found incorrect number of velocity tags" + ): u = mda.Universe(bad_input_missing_velocity, format="FHIAIMS") - def test_bad_input_velocity_wrong_position(self, bad_input_velocity_wrong_position): - with pytest.raises(ValueError, match="Non-conforming line .velocity must follow"): + def test_bad_input_velocity_wrong_position( + self, bad_input_velocity_wrong_position + ): + with pytest.raises( + ValueError, match="Non-conforming line .velocity must follow" + ): u = mda.Universe( - bad_input_velocity_wrong_position, format="FHIAIMS") + bad_input_velocity_wrong_position, format="FHIAIMS" + ) def test_bad_input_wrong_input_line(self, bad_input_wrong_input_line): with pytest.raises(ValueError, match="Non-conforming line"): @@ -173,20 +205,26 @@ def test_bad_input_wrong_input_line(self, bad_input_wrong_input_line): def test_good_input_with_velocity(self, good_input_with_velocity): u = mda.Universe(good_input_with_velocity, format="FHIAIMS") - assert_almost_equal(u.atoms.velocities[0], - np.asarray([0.1, 0.1, 0.1]), - self.prec, - "FHIAIMSReader failed to read velocities properly") - - def test_mixed_units(self, good_input_natural_units, good_input_mixed_units): + assert_almost_equal( + u.atoms.velocities[0], + np.asarray([0.1, 0.1, 0.1]), + self.prec, + "FHIAIMSReader failed to read velocities properly", + ) + + def test_mixed_units( + self, good_input_natural_units, good_input_mixed_units + ): u_natural = mda.Universe(good_input_natural_units, format="FHIAIMS") u_mixed = mda.Universe(good_input_mixed_units, format="FHIAIMS") print(u_natural.atoms.positions) print(u_mixed.atoms.positions) - assert_almost_equal(u_natural.atoms.positions, - u_mixed.atoms.positions, - self.prec, - "FHIAIMSReader failed to read positions in lattice units properly") + assert_almost_equal( + u_natural.atoms.positions, + u_mixed.atoms.positions, + self.prec, + "FHIAIMSReader failed to read positions in lattice units properly", + ) class TestFHIAIMSWriter(BaseWriterTest): @@ -195,46 +233,60 @@ class TestFHIAIMSWriter(BaseWriterTest): @pytest.fixture def outfile(self, tmpdir): - return str(tmpdir.mkdir("FHIAIMSWriter").join('primitive-fhiaims-writer' + self.ext)) + return str( + tmpdir.mkdir("FHIAIMSWriter").join( + "primitive-fhiaims-writer" + self.ext + ) + ) def test_writer(self, universe, outfile): - """Test writing from a single frame FHIAIMS file to a FHIAIMS file. - """ + """Test writing from a single frame FHIAIMS file to a FHIAIMS file.""" universe.atoms.write(outfile) u = mda.Universe(FHIAIMS, outfile) - assert_almost_equal(u.atoms.positions, - universe.atoms.positions, self.prec, - err_msg="Writing FHIAIMS file with FHIAIMSWriter " - "does not reproduce original coordinates") + assert_almost_equal( + u.atoms.positions, + universe.atoms.positions, + self.prec, + err_msg="Writing FHIAIMS file with FHIAIMSWriter " + "does not reproduce original coordinates", + ) def test_writer_with_velocity(self, good_input_with_velocity, outfile): - """Test writing from a single frame FHIAIMS file to a FHIAIMS file. - """ + """Test writing from a single frame FHIAIMS file to a FHIAIMS file.""" universe_in = mda.Universe(good_input_with_velocity, format="FHIAIMS") universe_in.atoms.write(outfile) u = mda.Universe(outfile) - assert_almost_equal(u.atoms.velocities, - universe_in.atoms.velocities, self.prec, - err_msg="Writing FHIAIMS file with FHIAIMSWriter " - "does not reproduce original velocities") + assert_almost_equal( + u.atoms.velocities, + universe_in.atoms.velocities, + self.prec, + err_msg="Writing FHIAIMS file with FHIAIMSWriter " + "does not reproduce original velocities", + ) def test_writer_no_atom_names(self, u_no_names, outfile): u_no_names.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array(['X'] * u_no_names.atoms.n_atoms) + expected = np.array(["X"] * u_no_names.atoms.n_atoms) assert_equal(u.atoms.names, expected) def test_writer_with_n_atoms_none(self, good_input_natural_units, outfile): u = mda.Universe(good_input_natural_units, format="FHIAIMS") with mda.Writer(outfile, natoms=None) as w: w.write(u.atoms) - with open(outfile, 'r') as fhiaimsfile: + with open(outfile, "r") as fhiaimsfile: line = fhiaimsfile.readline().strip() assert line.startswith( - 'atom'), "Line written incorrectly with FHIAIMSWriter" + "atom" + ), "Line written incorrectly with FHIAIMSWriter" assert line.endswith( - 'H'), "Line written incorrectly with FHIAIMSWriter" + "H" + ), "Line written incorrectly with FHIAIMSWriter" line = np.asarray(line.split()[1:-1], dtype=np.float32) - assert_almost_equal(line, [0.1, 0.2, 0.3], self.prec, - err_msg="Writing FHIAIMS file with FHIAIMSWriter " - "does not reproduce original positions") + assert_almost_equal( + line, + [0.1, 0.2, 0.3], + self.prec, + err_msg="Writing FHIAIMS file with FHIAIMSWriter " + "does not reproduce original positions", + ) diff --git a/testsuite/MDAnalysisTests/coordinates/test_gms.py b/testsuite/MDAnalysisTests/coordinates/test_gms.py index 08ac1a9bcdc..355e0658ba2 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gms.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gms.py @@ -23,11 +23,11 @@ import pytest import numpy as np -from numpy.testing import (assert_equal, assert_almost_equal) +from numpy.testing import assert_equal, assert_almost_equal import MDAnalysis as mda from MDAnalysis.coordinates.GMS import GMSReader -from MDAnalysisTests.datafiles import (GMS_ASYMOPT, GMS_ASYMSURF, GMS_SYMOPT) +from MDAnalysisTests.datafiles import GMS_ASYMOPT, GMS_ASYMSURF, GMS_SYMOPT class _GMSBase(object): @@ -36,10 +36,11 @@ def u(self): return mda.Universe(self.filename) def test_n_frames(self, u): - assert_equal(u.trajectory.n_frames, - self.n_frames, - err_msg="Wrong number of frames read from {}".format( - self.flavour)) + assert_equal( + u.trajectory.n_frames, + self.n_frames, + err_msg="Wrong number of frames read from {}".format(self.flavour), + ) def test_random_access(self, u): u = u @@ -59,12 +60,12 @@ def test_random_access(self, u): @staticmethod def _calcFD(u): u.trajectory.rewind() - pp = (u.trajectory.ts._pos[0] - u.trajectory.ts._pos[3]) - z1 = np.sqrt(sum(pp ** 2)) + pp = u.trajectory.ts._pos[0] - u.trajectory.ts._pos[3] + z1 = np.sqrt(sum(pp**2)) for i in range(5): u.trajectory.next() - pp = (u.trajectory.ts._pos[0] - u.trajectory.ts._pos[3]) - z2 = np.sqrt(sum(pp ** 2)) + pp = u.trajectory.ts._pos[0] - u.trajectory.ts._pos[3] + z2 = np.sqrt(sum(pp**2)) return z1 - z2 def test_rewind(self, u): @@ -77,15 +78,18 @@ def test_next(self, u): assert_equal(u.trajectory.ts.frame, 1, "loading frame 1") def test_dt(self, u): - assert_almost_equal(u.trajectory.dt, - 1.0, - 4, - err_msg="wrong timestep dt") + assert_almost_equal( + u.trajectory.dt, 1.0, 4, err_msg="wrong timestep dt" + ) def test_step5distances(self, u): - assert_almost_equal(self._calcFD(u), self.step5d, decimal=5, - err_msg="Wrong 1-4 atom distance change after " - "5 steps for {}".format(self.flavour)) + assert_almost_equal( + self._calcFD(u), + self.step5d, + decimal=5, + err_msg="Wrong 1-4 atom distance change after " + "5 steps for {}".format(self.flavour), + ) class TestGMSReader(_GMSBase): diff --git a/testsuite/MDAnalysisTests/coordinates/test_gro.py b/testsuite/MDAnalysisTests/coordinates/test_gro.py index 7dcdbbc029a..c307bdc8ca6 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gro.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gro.py @@ -28,7 +28,9 @@ from MDAnalysis.transformations import translate from MDAnalysisTests import make_Universe from MDAnalysisTests.coordinates.base import ( - BaseReference, BaseReaderTest, BaseWriterTest, + BaseReference, + BaseReaderTest, + BaseWriterTest, ) from MDAnalysisTests.coordinates.reference import RefAdK from MDAnalysisTests.datafiles import ( @@ -53,63 +55,79 @@ class TestGROReaderOld(RefAdK): # lower prec in gro!! (3 decimals nm -> 2 decimals in Angstroem) prec = 2 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return mda.Universe(GRO) def test_load_gro(self, universe): U = universe - assert_equal(len(U.atoms), self.ref_n_atoms, - "load Universe from small GRO") - assert_equal(U.atoms.select_atoms('resid 150 and name HA2').atoms[0], - U.atoms[self.ref_E151HA2_index], "Atom selections") + assert_equal( + len(U.atoms), self.ref_n_atoms, "load Universe from small GRO" + ) + assert_equal( + U.atoms.select_atoms("resid 150 and name HA2").atoms[0], + U.atoms[self.ref_E151HA2_index], + "Atom selections", + ) def test_coordinates(self, universe): - A10CA = universe.select_atoms('name CA')[10] - assert_almost_equal(A10CA.position, - self.ref_coordinates['A10CA'], - self.prec, - err_msg="wrong coordinates for A10:CA") + A10CA = universe.select_atoms("name CA")[10] + assert_almost_equal( + A10CA.position, + self.ref_coordinates["A10CA"], + self.prec, + err_msg="wrong coordinates for A10:CA", + ) def test_distances(self, universe): # NOTe that the prec is only 1 decimal: subtracting two low precision # coordinates low prec: 9.3455122920041109; high prec (from pdb): # 9.3513174 - NTERM = universe.select_atoms('name N')[0] - CTERM = universe.select_atoms('name C')[-1] + NTERM = universe.select_atoms("name N")[0] + CTERM = universe.select_atoms("name C")[-1] d = mda.lib.mdamath.norm(NTERM.position - CTERM.position) - assert_almost_equal(d, self.ref_distances['endtoend'], self.prec - 1, - err_msg="distance between M1:N and G214:C") + assert_almost_equal( + d, + self.ref_distances["endtoend"], + self.prec - 1, + err_msg="distance between M1:N and G214:C", + ) def test_selection(self, universe): - na = universe.select_atoms('resname NA+') - assert_equal(len(na), self.ref_Na_sel_size, - "Atom selection of last atoms in file") + na = universe.select_atoms("resname NA+") + assert_equal( + len(na), + self.ref_Na_sel_size, + "Atom selection of last atoms in file", + ) def test_unitcell(self, universe): assert_almost_equal( universe.trajectory.ts.dimensions, self.ref_unitcell, self.prec, - err_msg="unit cell dimensions (rhombic dodecahedron)") + err_msg="unit cell dimensions (rhombic dodecahedron)", + ) class TestGROReaderNoConversionOld(RefAdK): prec = 3 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return mda.Universe(GRO, convert_units=False) def test_coordinates(self, universe): # note: these are the native coordinates in nm; for the test to succeed # we loaded with convert_units=False - A10CA = universe.select_atoms('name CA')[10] + A10CA = universe.select_atoms("name CA")[10] # coordinates in nm - assert_almost_equal(A10CA.position, - RefAdK.ref_coordinates['A10CA'] / 10.0, - self.prec, err_msg="wrong native coordinates " - "(in nm) for A10:CA") + assert_almost_equal( + A10CA.position, + RefAdK.ref_coordinates["A10CA"] / 10.0, + self.prec, + err_msg="wrong native coordinates " "(in nm) for A10:CA", + ) def test_distances(self, universe): # 3 decimals on nm in gro but we compare to the distance @@ -118,13 +136,16 @@ def test_distances(self, universe): # Arrays are not almost equal distance between M1:N and G214:C # ACTUAL: 0.93455122920041123 # DESIRED: 0.93513173999999988 - NTERM = universe.select_atoms('name N')[0] - CTERM = universe.select_atoms('name C')[-1] + NTERM = universe.select_atoms("name N")[0] + CTERM = universe.select_atoms("name C")[-1] d = mda.lib.mdamath.norm(NTERM.position - CTERM.position) # coordinates in nm - assert_almost_equal(d, RefAdK.ref_distances['endtoend'] / 10.0, - self.prec - 1, err_msg="distance between M1:N " - "and G214:C") + assert_almost_equal( + d, + RefAdK.ref_distances["endtoend"] / 10.0, + self.prec - 1, + err_msg="distance between M1:N " "and G214:C", + ) def test_unitcell(self, universe): # lengths in A : convert to nm @@ -132,21 +153,24 @@ def test_unitcell(self, universe): universe.trajectory.ts.dimensions[:3], self.ref_unitcell[:3] / 10.0, self.prec, - err_msg="unit cell A,B,C (rhombic dodecahedron)") + err_msg="unit cell A,B,C (rhombic dodecahedron)", + ) # angles should not have changed assert_almost_equal( universe.trajectory.ts.dimensions[3:], self.ref_unitcell[3:], self.prec, - err_msg="unit cell alpha,beta,gamma (rhombic dodecahedron)") + err_msg="unit cell alpha,beta,gamma (rhombic dodecahedron)", + ) def test_volume(self, universe): # ref lengths in A (which was originally converted from nm) assert_almost_equal( universe.trajectory.ts.volume, - self.ref_volume / 1000., + self.ref_volume / 1000.0, 3, - err_msg="wrong volume for unitcell (rhombic dodecahedron)") + err_msg="wrong volume for unitcell (rhombic dodecahedron)", + ) class GROReference(BaseReference): @@ -156,30 +180,32 @@ def __init__(self): self.topology = COORDINATES_GRO self.reader = GROReader self.writer = GROWriter - self.ext = 'gro' + self.ext = "gro" self.n_frames = 1 self.prec = 4 self.first_frame.velocities = np.array( - [[0.0000, 0.100, 0.200], - [0.300, 0.400, 0.500], - [0.600, 0.700, 0.800], - [0.900, 1.000, 1.100], - [1.200, 1.300, 1.400]], - dtype=np.float32) + [ + [0.0000, 0.100, 0.200], + [0.300, 0.400, 0.500], + [0.600, 0.700, 0.800], + [0.900, 1.000, 1.100], + [1.200, 1.300, 1.400], + ], + dtype=np.float32, + ) self.totaltime = 0 self.container_format = True class TestGROReader(BaseReaderTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GROReference() def test_time(self, ref, reader): u = mda.Universe(ref.topology, ref.trajectory) - assert_equal(u.trajectory.time, 0.0, - "wrong time of the frame") + assert_equal(u.trajectory.time, 0.0, "wrong time of the frame") def test_full_slice(self, ref, reader): u = mda.Universe(ref.topology, ref.trajectory) @@ -190,30 +216,29 @@ def test_full_slice(self, ref, reader): class TestGROWriter(BaseWriterTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GROReference() def test_write_velocities(self, ref, tmpdir): u = mda.Universe(ref.topology, ref.trajectory) with tmpdir.as_cwd(): - outfile = 'write-velocities-test.' + ref.ext + outfile = "write-velocities-test." + ref.ext u.atoms.write(outfile) u2 = mda.Universe(outfile) - assert_almost_equal(u.atoms.velocities, - u2.atoms.velocities) + assert_almost_equal(u.atoms.velocities, u2.atoms.velocities) def test_write_no_resnames(self, u_no_resnames, ref, tmpdir): - outfile = 'write-no-resnames-test.' + ref.ext + outfile = "write-no-resnames-test." + ref.ext with tmpdir.as_cwd(): u_no_resnames.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array(['UNK'] * u_no_resnames.atoms.n_atoms) + expected = np.array(["UNK"] * u_no_resnames.atoms.n_atoms) assert_equal(u.atoms.resnames, expected) def test_write_no_resids(self, u_no_resids, ref, tmpdir): - outfile = 'write-no-resids-test.' + ref.ext + outfile = "write-no-resids-test." + ref.ext with tmpdir.as_cwd(): u_no_resids.atoms.write(outfile) u = mda.Universe(outfile) @@ -221,11 +246,11 @@ def test_write_no_resids(self, u_no_resids, ref, tmpdir): assert_equal(u.residues.resids, expected) def test_writer_no_atom_names(self, u_no_names, ref, tmpdir): - outfile = 'write-no-names-test.' + ref.ext + outfile = "write-no-names-test." + ref.ext with tmpdir.as_cwd(): u_no_names.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array(['X'] * u_no_names.atoms.n_atoms) + expected = np.array(["X"] * u_no_names.atoms.n_atoms) assert_equal(u.atoms.names, expected) def test_check_coordinate_limits_min(self, ref, tmpdir): @@ -235,7 +260,7 @@ def test_check_coordinate_limits_min(self, ref, tmpdir): # parallel tests u = mda.Universe(GRO) u.atoms[2000].position = [11.589, -999.9995 * 10, 22.2] # nm -> A - outfile = 'coordinate-limits-min-test.' + ref.ext + outfile = "coordinate-limits-min-test." + ref.ext with tmpdir.as_cwd(): with pytest.raises(ValueError): u.atoms.write(outfile) @@ -248,7 +273,7 @@ def test_check_coordinate_limits_max(self, ref, tmpdir): u = mda.Universe(GRO) # nm -> A ; [ob] 9999.9996 not caught u.atoms[1000].position = [0, 9999.9999 * 10, 1] - outfile = 'coordinate-limits-max-test.' + ref.ext + outfile = "coordinate-limits-max-test." + ref.ext with tmpdir.as_cwd(): with pytest.raises(ValueError): u.atoms.write(outfile) @@ -260,7 +285,7 @@ def test_check_coordinate_limits_max_noconversion(self, ref, tmpdir): # parallel tests u = mda.Universe(GRO, convert_units=False) u.atoms[1000].position = [22.2, 9999.9999, 37.89] - outfile = 'coordinate-limits-max-noconversion-test.' + ref.ext + outfile = "coordinate-limits-max-noconversion-test." + ref.ext with tmpdir.as_cwd(): with pytest.raises(ValueError): u.atoms.write(outfile, convert_units=False) @@ -277,38 +302,48 @@ def __init__(self): class TestGROReaderNoConversion(BaseReaderTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GRONoConversionReference() @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def reader(ref): reader = ref.reader(ref.trajectory, convert_units=False) - reader.add_auxiliary('lowf', ref.aux_lowf, - dt=ref.aux_lowf_dt, - initial_time=0, time_selector=None) - reader.add_auxiliary('highf', ref.aux_highf, - dt=ref.aux_highf_dt, - initial_time=0, time_selector=None) + reader.add_auxiliary( + "lowf", + ref.aux_lowf, + dt=ref.aux_lowf_dt, + initial_time=0, + time_selector=None, + ) + reader.add_auxiliary( + "highf", + ref.aux_highf, + dt=ref.aux_highf_dt, + initial_time=0, + time_selector=None, + ) return reader - + @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def transformed(ref): transformed = ref.reader(ref.trajectory, convert_units=False) - transformed.add_transformations(translate([1,1,1]), translate([0,0,0.33])) + transformed.add_transformations( + translate([1, 1, 1]), translate([0, 0, 0.33]) + ) return transformed class TestGROWriterNoConversion(BaseWriterTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GRONoConversionReference() @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def writer(ref): return ref.writer(ref.trajectory, convert_units=False) @@ -319,24 +354,27 @@ def __init__(self): self.trajectory = COORDINATES_GRO_INCOMPLETE_VELOCITY self.topology = COORDINATES_GRO_INCOMPLETE_VELOCITY self.first_frame.velocities = np.array( - [[0.0000, 0.100, 0.200], - [0.000, 0.000, 0.000], - [0.600, 0.700, 0.800], - [0.900, 1.000, 1.100], - [1.200, 1.300, 1.400]], - dtype=np.float32) + [ + [0.0000, 0.100, 0.200], + [0.000, 0.000, 0.000], + [0.600, 0.700, 0.800], + [0.900, 1.000, 1.100], + [1.200, 1.300, 1.400], + ], + dtype=np.float32, + ) class TestGROReaderIncompleteVelocities(BaseReaderTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GROReaderIncompleteVelocitiesReference() class TestGROWriterIncompleteVelocities(BaseWriterTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GROReaderIncompleteVelocitiesReference() @@ -350,14 +388,14 @@ def __init__(self): class TestGROBZ2Reader(BaseReaderTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GROBZReference() class TestGROBZ2Writer(BaseWriterTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GROBZReference() @@ -371,7 +409,7 @@ def __init__(self): class TestGROLargeWriter(BaseWriterTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return GROLargeReference() @@ -380,26 +418,30 @@ def test_writer_large(self, ref, tmpdir): Test that atom numbers are truncated for large GRO files (Issue 550). """ - outfile = 'outfile1.' + ref.ext + outfile = "outfile1." + ref.ext u = mda.Universe(ref.topology, ref.trajectory) with tmpdir.as_cwd(): u.atoms.write(outfile) - with open(outfile, 'rt') as mda_output: - with mda.lib.util.anyopen(ref.topology, 'rt') as expected_output: + with open(outfile, "rt") as mda_output: + with mda.lib.util.anyopen( + ref.topology, "rt" + ) as expected_output: produced_lines = mda_output.readlines()[1:] expected_lines = expected_output.readlines()[1:] - assert_equal(produced_lines, - expected_lines, - err_msg="Writing GRO file with > 100 000 " - "coords does not truncate properly.") + assert_equal( + produced_lines, + expected_lines, + err_msg="Writing GRO file with > 100 000 " + "coords does not truncate properly.", + ) def test_writer_large_residue_count(self, ref, tmpdir): """ Ensure large residue number truncation for GRO files (Issue 886). """ - outfile = 'outfile2.' + ref.ext + outfile = "outfile2." + ref.ext u = mda.Universe(ref.topology, ref.trajectory) target_resname = u.residues[-1].resname resid_value = 9999999 @@ -407,78 +449,81 @@ def test_writer_large_residue_count(self, ref, tmpdir): with tmpdir.as_cwd(): u.atoms.write(outfile) - with open(outfile, 'rt') as mda_output: + with open(outfile, "rt") as mda_output: output_lines = mda_output.readlines() produced_resid = output_lines[-2].split(target_resname)[0] expected_resid = str(resid_value)[:5] - assert_equal(produced_resid, - expected_resid, - err_msg="Writing GRO file with > 99 999 " - "resids does not truncate properly.") + assert_equal( + produced_resid, + expected_resid, + err_msg="Writing GRO file with > 99 999 " + "resids does not truncate properly.", + ) def test_growriter_resid_truncation(tmpdir): with tmpdir.as_cwd(): - u = make_Universe(extras=['resids'], trajectory=True) + u = make_Universe(extras=["resids"], trajectory=True) u.residues[0].resid = 123456789 - u.atoms.write('out.gro') + u.atoms.write("out.gro") - with open('out.gro', 'r') as grofile: + with open("out.gro", "r") as grofile: grofile.readline() grofile.readline() line = grofile.readline() # larger digits should get truncated - assert line.startswith('56789UNK') + assert line.startswith("56789UNK") + class TestGrowriterReindex(object): @pytest.fixture() def u(self): - gro = '''test + gro = """test 1 2CL CL20850 0.000 0.000 0.000 -7.29748 7.66094 9.82962''' - u = mda.Universe(StringIO(gro), format='gro') +7.29748 7.66094 9.82962""" + u = mda.Universe(StringIO(gro), format="gro") u.atoms[0].id = 3 return u def test_growriter_resid_true(self, u, tmpdir): with tmpdir.as_cwd(): - u.atoms.write('temp.gro', reindex=True) + u.atoms.write("temp.gro", reindex=True) - with open('temp.gro', 'r') as grofile: + with open("temp.gro", "r") as grofile: grofile.readline() grofile.readline() line = grofile.readline() - assert line.startswith(' 2CL CL 1') + assert line.startswith(" 2CL CL 1") def test_growriter_resid_false(self, u, tmpdir): with tmpdir.as_cwd(): - u.atoms.write('temp.gro', reindex=False) - with open('temp.gro', 'r') as grofile: + u.atoms.write("temp.gro", reindex=False) + with open("temp.gro", "r") as grofile: grofile.readline() grofile.readline() line = grofile.readline() - assert line.startswith(' 2CL CL 3') + assert line.startswith(" 2CL CL 3") def test_writer_resid_false(self, u, tmpdir): with tmpdir.as_cwd(): - with mda.Writer('temp.gro', reindex=False) as w: + with mda.Writer("temp.gro", reindex=False) as w: w.write(u.atoms) - with open('temp.gro', 'r') as grofile: + with open("temp.gro", "r") as grofile: grofile.readline() grofile.readline() line = grofile.readline() - assert line.startswith(' 2CL CL 3') + assert line.startswith(" 2CL CL 3") def test_writer_resid_true(self, u, tmpdir): with tmpdir.as_cwd(): - with mda.Writer('temp.gro', reindex=True) as w: + with mda.Writer("temp.gro", reindex=True) as w: w.write(u.atoms) - with open('temp.gro', 'r') as grofile: + with open("temp.gro", "r") as grofile: grofile.readline() grofile.readline() line = grofile.readline() - assert line.startswith(' 2CL CL 1') + assert line.startswith(" 2CL CL 1") def test_multiframe_gro(): @@ -486,14 +531,18 @@ def test_multiframe_gro(): # for now, single frame read assert len(u.trajectory) == 1 - assert_equal(u.dimensions, np.array([100, 100, 100, 90, 90, 90], dtype=np.float32)) + assert_equal( + u.dimensions, np.array([100, 100, 100, 90, 90, 90], dtype=np.float32) + ) def test_huge_box_gro(): u = mda.Universe(GRO_huge_box) - assert_equal(u.dimensions, np.array([4.e+05, 4.e+05, 4.e+05, 90, 90, 90], - dtype=np.float32)) + assert_equal( + u.dimensions, + np.array([4.0e05, 4.0e05, 4.0e05, 90, 90, 90], dtype=np.float32), + ) gro_no_dims = """\ @@ -503,14 +552,14 @@ def test_huge_box_gro(): """ -@pytest.mark.parametrize('dims', [1, 2, 4, 5, 6, 7, 8]) +@pytest.mark.parametrize("dims", [1, 2, 4, 5, 6, 7, 8]) def test_bad_box(dims): - cell = ' '.join([str(float(i)) for i in range(dims)]) + cell = " ".join([str(float(i)) for i in range(dims)]) grofile = gro_no_dims + cell errmsg = "GRO unitcell has neither 3 nor 9 entries." with pytest.raises(ValueError, match=errmsg): - u = mda.Universe(StringIO(grofile), format='gro') + u = mda.Universe(StringIO(grofile), format="gro") def test_gro_empty_box_write_read(tmpdir): @@ -523,9 +572,9 @@ def test_gro_empty_box_write_read(tmpdir): with tmpdir.as_cwd(): wmsg = " setting unit cell to zeroed box" with pytest.warns(UserWarning, match=wmsg): - u.atoms.write('test.gro') + u.atoms.write("test.gro") wmsg = "treating as missing unit cell" with pytest.warns(UserWarning, match=wmsg): - u2 = mda.Universe('test.gro') + u2 = mda.Universe("test.gro") assert u2.dimensions is None diff --git a/testsuite/MDAnalysisTests/coordinates/test_gsd.py b/testsuite/MDAnalysisTests/coordinates/test_gsd.py index e6b6a79ae48..102b8391df9 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gsd.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gsd.py @@ -30,34 +30,36 @@ from MDAnalysisTests.datafiles import GSD -@pytest.mark.skipif(HAS_GSD, reason='gsd is installed') +@pytest.mark.skipif(HAS_GSD, reason="gsd is installed") def test_no_gsd_u_raises(): - with pytest.raises(ImportError, match='please install gsd'): + with pytest.raises(ImportError, match="please install gsd"): _ = mda.Universe(GSD) -@pytest.mark.skipif(HAS_GSD, reason='gsd is installed') +@pytest.mark.skipif(HAS_GSD, reason="gsd is installed") def test_gsd_reader_raises(): - with pytest.raises(ImportError, match='please install gsd'): - _ = GSDReader('foo') + with pytest.raises(ImportError, match="please install gsd"): + _ = GSDReader("foo") -@pytest.mark.skipif(not HAS_GSD, reason='gsd is not installed') +@pytest.mark.skipif(not HAS_GSD, reason="gsd is not installed") class TestGSDReader: - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def GSD_U(self): return mda.Universe(GSD) def test_gsd_positions(self, GSD_U): # first frame first particle GSD_U.trajectory[0] - assert_almost_equal(GSD_U.atoms.positions[0], - [-5.4000001 , -10.19999981, -10.19999981]) + assert_almost_equal( + GSD_U.atoms.positions[0], [-5.4000001, -10.19999981, -10.19999981] + ) # second frame first particle GSD_U.trajectory[1] - assert_almost_equal(GSD_U.atoms.positions[0], - [-5.58348083, -9.98546982, -10.17657185]) - + assert_almost_equal( + GSD_U.atoms.positions[0], [-5.58348083, -9.98546982, -10.17657185] + ) + def test_gsd_n_frames(self, GSD_U): assert len(GSD_U.trajectory) == 2 @@ -65,9 +67,9 @@ def test_gsd_dimensions(self, GSD_U): ts = GSD_U.trajectory[0] assert_almost_equal( ts.dimensions, - [21.60000038, 21.60000038, 21.60000038, 90., 90., 90.] + [21.60000038, 21.60000038, 21.60000038, 90.0, 90.0, 90.0], ) def test_gsd_data_step(self, GSD_U): - assert GSD_U.trajectory[0].data['step'] == 0 - assert GSD_U.trajectory[1].data['step'] == 500 + assert GSD_U.trajectory[0].data["step"] == 0 + assert GSD_U.trajectory[1].data["step"] == 500 diff --git a/testsuite/MDAnalysisTests/coordinates/test_h5md.py b/testsuite/MDAnalysisTests/coordinates/test_h5md.py index 76e80a2a46d..1524ca7f62c 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_h5md.py +++ b/testsuite/MDAnalysisTests/coordinates/test_h5md.py @@ -37,7 +37,7 @@ def __init__(self): self.topology = COORDINATES_TOPOLOGY self.reader = mda.coordinates.H5MD.H5MDReader self.writer = mda.coordinates.H5MD.H5MDWriter - self.ext = 'h5md' + self.ext = "h5md" self.prec = 3 self.changing_dimensions = True @@ -66,6 +66,7 @@ def iter_ts(self, i): @pytest.mark.skipif(not HAS_H5PY, reason="h5py not installed") class TestH5MDReaderBaseAPI(MultiframeReaderTest): """Tests H5MDReader with with synthetic trajectory.""" + @staticmethod @pytest.fixture() def ref(): @@ -73,14 +74,14 @@ def ref(): def test_get_writer_1(self, ref, reader, tmpdir): with tmpdir.as_cwd(): - outfile = 'test-writer.' + ref.ext + outfile = "test-writer." + ref.ext with reader.Writer(outfile) as W: assert_equal(isinstance(W, ref.writer), True) assert_equal(W.n_atoms, reader.n_atoms) def test_get_writer_2(self, ref, reader, tmpdir): with tmpdir.as_cwd(): - outfile = 'test-writer.' + ref.ext + outfile = "test-writer." + ref.ext with reader.Writer(outfile, n_atoms=100) as W: assert_equal(isinstance(W, ref.writer), True) assert_equal(W.n_atoms, 100) @@ -88,25 +89,29 @@ def test_get_writer_2(self, ref, reader, tmpdir): def test_copying(self, ref, reader): # Issue #3664 - test not done in test_copying due to dependencies original = mda.coordinates.H5MD.H5MDReader( - ref.trajectory, convert_units=False, dt=2, - time_offset=10, foo="bar") + ref.trajectory, + convert_units=False, + dt=2, + time_offset=10, + foo="bar", + ) copy = original.copy() - assert original.format not in ('MEMORY', 'CHAIN') + assert original.format not in ("MEMORY", "CHAIN") assert original.convert_units is False assert copy.convert_units is False - assert original._ts_kwargs['time_offset'] == 10 - assert copy._ts_kwargs['time_offset'] == 10 - assert original._ts_kwargs['dt'] == 2 - assert copy._ts_kwargs['dt'] == 2 + assert original._ts_kwargs["time_offset"] == 10 + assert copy._ts_kwargs["time_offset"] == 10 + assert original._ts_kwargs["dt"] == 2 + assert copy._ts_kwargs["dt"] == 2 - assert original.ts.data['time_offset'] == 10 - assert copy.ts.data['time_offset'] == 10 + assert original.ts.data["time_offset"] == 10 + assert copy.ts.data["time_offset"] == 10 - assert original.ts.data['dt'] == 2 - assert copy.ts.data['dt'] == 2 + assert original.ts.data["dt"] == 2 + assert copy.ts.data["dt"] == 2 - assert copy._kwargs['foo'] == 'bar' + assert copy._kwargs["foo"] == "bar" # check coordinates assert original.ts.frame == copy.ts.frame @@ -122,27 +127,32 @@ def test_copying(self, ref, reader): @pytest.mark.skipif(not HAS_H5PY, reason="h5py not installed") class TestH5MDWriterBaseAPI(BaseWriterTest): """Tests H5MDWriter base API with synthetic trajectory""" + @staticmethod @pytest.fixture() def ref(): return H5MDReference() def test_write_trajectory_atomgroup(self, ref, reader, universe, tmpdir): - outfile = 'write-atoms-test.' + ref.ext + outfile = "write-atoms-test." + ref.ext with tmpdir.as_cwd(): - with ref.writer(outfile, universe.atoms.n_atoms, - velocities=True, forces=True) as w: + with ref.writer( + outfile, universe.atoms.n_atoms, velocities=True, forces=True + ) as w: for ts in universe.trajectory: w.write(universe.atoms) self._check_copy(outfile, ref, reader) - @pytest.mark.xfail((os.name == 'nt' and sys.maxsize <= 2**32), - reason="occasional fail on 32-bit windows") + @pytest.mark.xfail( + (os.name == "nt" and sys.maxsize <= 2**32), + reason="occasional fail on 32-bit windows", + ) def test_write_trajectory_universe(self, ref, reader, universe, tmpdir): - outfile = 'write-uni-test.' + ref.ext + outfile = "write-uni-test." + ref.ext with tmpdir.as_cwd(): - with ref.writer(outfile, universe.atoms.n_atoms, - velocities=True, forces=True) as w: + with ref.writer( + outfile, universe.atoms.n_atoms, velocities=True, forces=True + ) as w: for ts in universe.trajectory: w.write(universe) self._check_copy(outfile, ref, reader) @@ -152,101 +162,137 @@ def test_write_trajectory_universe(self, ref, reader, universe, tmpdir): class TestH5MDReaderWithRealTrajectory(object): prec = 3 - ext = 'h5md' + ext = "h5md" - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return mda.Universe(TPR_xvf, H5MD_xvf) @pytest.fixture() def h5md_file(self): - return h5py.File(H5MD_xvf, 'r') + return h5py.File(H5MD_xvf, "r") @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir.join('h5md-reader-test.' + self.ext)) + return str(tmpdir.join("h5md-reader-test." + self.ext)) def test_n_frames(self, universe): assert len(universe.trajectory) == 3 def test_positions(self, universe): universe.trajectory[0] - assert_almost_equal(universe.atoms.positions[0], - [32.309906, 13.77798, 14.372463], - decimal=self.prec) - assert_almost_equal(universe.atoms.positions[42], - [28.116928, 19.405945, 19.647358], - decimal=self.prec) - assert_almost_equal(universe.atoms.positions[10000], - [44.117805, 50.442093, 23.299038], - decimal=self.prec) + assert_almost_equal( + universe.atoms.positions[0], + [32.309906, 13.77798, 14.372463], + decimal=self.prec, + ) + assert_almost_equal( + universe.atoms.positions[42], + [28.116928, 19.405945, 19.647358], + decimal=self.prec, + ) + assert_almost_equal( + universe.atoms.positions[10000], + [44.117805, 50.442093, 23.299038], + decimal=self.prec, + ) universe.trajectory[1] - assert_almost_equal(universe.atoms.positions[0], - [30.891968, 13.678971, 13.6000595], - decimal=self.prec) - assert_almost_equal(universe.atoms.positions[42], - [27.163246, 19.846561, 19.3582], - decimal=self.prec) - assert_almost_equal(universe.atoms.positions[10000], - [45.869278, 5.0342298, 25.460655], - decimal=self.prec) + assert_almost_equal( + universe.atoms.positions[0], + [30.891968, 13.678971, 13.6000595], + decimal=self.prec, + ) + assert_almost_equal( + universe.atoms.positions[42], + [27.163246, 19.846561, 19.3582], + decimal=self.prec, + ) + assert_almost_equal( + universe.atoms.positions[10000], + [45.869278, 5.0342298, 25.460655], + decimal=self.prec, + ) universe.trajectory[2] - assert_almost_equal(universe.atoms.positions[0], - [31.276512, 13.89617, 15.015897], - decimal=self.prec) - assert_almost_equal(universe.atoms.positions[42], - [28.567991, 20.56532, 19.40814], - decimal=self.prec) - assert_almost_equal(universe.atoms.positions[10000], - [39.713223, 6.127234, 18.284992], - decimal=self.prec) + assert_almost_equal( + universe.atoms.positions[0], + [31.276512, 13.89617, 15.015897], + decimal=self.prec, + ) + assert_almost_equal( + universe.atoms.positions[42], + [28.567991, 20.56532, 19.40814], + decimal=self.prec, + ) + assert_almost_equal( + universe.atoms.positions[10000], + [39.713223, 6.127234, 18.284992], + decimal=self.prec, + ) def test_h5md_velocities(self, universe): universe.trajectory[0] - assert_almost_equal(universe.atoms.velocities[0], - [-2.697732, 0.613568, 0.14334752], - decimal=self.prec) + assert_almost_equal( + universe.atoms.velocities[0], + [-2.697732, 0.613568, 0.14334752], + decimal=self.prec, + ) universe.trajectory[1] - assert_almost_equal(universe.atoms.velocities[42], - [-6.8698354, 7.834235, -8.114698], - decimal=self.prec) + assert_almost_equal( + universe.atoms.velocities[42], + [-6.8698354, 7.834235, -8.114698], + decimal=self.prec, + ) universe.trajectory[2] - assert_almost_equal(universe.atoms.velocities[10000], - [9.799492, 5.631466, 6.852126], - decimal=self.prec) + assert_almost_equal( + universe.atoms.velocities[10000], + [9.799492, 5.631466, 6.852126], + decimal=self.prec, + ) def test_h5md_forces(self, universe): universe.trajectory[0] - assert_almost_equal(universe.atoms.forces[0], - [20.071287, -155.2285, -96.72112], - decimal=self.prec) + assert_almost_equal( + universe.atoms.forces[0], + [20.071287, -155.2285, -96.72112], + decimal=self.prec, + ) universe.trajectory[1] - assert_almost_equal(universe.atoms.forces[42], - [-4.1959066, -31.31548, 22.663044], - decimal=self.prec) + assert_almost_equal( + universe.atoms.forces[42], + [-4.1959066, -31.31548, 22.663044], + decimal=self.prec, + ) universe.trajectory[2] - assert_almost_equal(universe.atoms.forces[10000], - [-41.43743, 83.35207, 62.94751], - decimal=self.prec) + assert_almost_equal( + universe.atoms.forces[10000], + [-41.43743, 83.35207, 62.94751], + decimal=self.prec, + ) def test_h5md_dimensions(self, universe): universe.trajectory[0] - assert_almost_equal(universe.trajectory.ts.dimensions, - [52.763, 52.763, 52.763, 90., 90., 90.], - decimal=self.prec) + assert_almost_equal( + universe.trajectory.ts.dimensions, + [52.763, 52.763, 52.763, 90.0, 90.0, 90.0], + decimal=self.prec, + ) universe.trajectory[1] - assert_almost_equal(universe.trajectory.ts.dimensions, - [52.807877, 52.807877, 52.807877, 90., 90., 90.], - decimal=self.prec) + assert_almost_equal( + universe.trajectory.ts.dimensions, + [52.807877, 52.807877, 52.807877, 90.0, 90.0, 90.0], + decimal=self.prec, + ) universe.trajectory[2] - assert_almost_equal(universe.trajectory.ts.dimensions, - [52.839806, 52.839806, 52.839806, 90., 90., 90.], - decimal=self.prec) + assert_almost_equal( + universe.trajectory.ts.dimensions, + [52.839806, 52.839806, 52.839806, 90.0, 90.0, 90.0], + decimal=self.prec, + ) def test_h5md_data_step(self, universe): for ts, step in zip(universe.trajectory, (0, 25000, 50000)): - assert_equal(ts.data['step'], step) + assert_equal(ts.data["step"], step) def test_rewind(self, universe): universe.trajectory[1] @@ -262,155 +308,176 @@ def test_jump_last_frame(self, universe): universe.trajectory[-1] assert universe.trajectory.ts.frame == 2 - @pytest.mark.parametrize("start, stop, step", ((0, 2, 1), - (1, 2, 1))) + @pytest.mark.parametrize("start, stop, step", ((0, 2, 1), (1, 2, 1))) def test_slice(self, universe, start, stop, step): - frames = [universe.trajectory.ts.frame - for ts in universe.trajectory[start:stop:step]] + frames = [ + universe.trajectory.ts.frame + for ts in universe.trajectory[start:stop:step] + ] assert_equal(frames, np.arange(start, stop, step)) @pytest.mark.parametrize("array_like", [list, np.array]) def test_array_like(self, universe, array_like): array = array_like([0, 2]) - frames = [universe.trajectory.ts.frame - for ts in universe.trajectory[array]] + frames = [ + universe.trajectory.ts.frame for ts in universe.trajectory[array] + ] assert_equal(frames, array) def test_list_indices(self, universe): indices = [0, 1, 2, 1, 2, 2, 0] - frames = [universe.trajectory.ts.frame - for ts in universe.trajectory[indices]] + frames = [ + universe.trajectory.ts.frame for ts in universe.trajectory[indices] + ] assert_equal(frames, indices) - @pytest.mark.parametrize('group, attr', (('position', 'positions'), - ('velocity', 'velocities'), - ('force', 'forces'))) + @pytest.mark.parametrize( + "group, attr", + ( + ("position", "positions"), + ("velocity", "velocities"), + ("force", "forces"), + ), + ) def test_no_group(self, h5md_file, outfile, attr, group): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - del g[f'particles/trajectory/{group}'] + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + del g[f"particles/trajectory/{group}"] u = mda.Universe(TPR_xvf, outfile) - with pytest.raises(NoDataError, - match="This Timestep has no"): + with pytest.raises(NoDataError, match="This Timestep has no"): getattr(u.trajectory.ts, attr) - @pytest.mark.parametrize('dset', - ('position/value', 'position/time', 'velocity/value', 'force/value')) + @pytest.mark.parametrize( + "dset", + ("position/value", "position/time", "velocity/value", "force/value"), + ) def test_unknown_unit(self, h5md_file, outfile, dset): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - g['particles' - '/trajectory' - f'/{dset}'].attrs['unit'] = 'random string' - with pytest.raises(RuntimeError, - match=" is not recognized by H5MDReader."): + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + g["particles" "/trajectory" f"/{dset}"].attrs[ + "unit" + ] = "random string" + with pytest.raises( + RuntimeError, match=" is not recognized by H5MDReader." + ): u = mda.Universe(TPR_xvf, outfile) def test_length_unit_from_box(self, h5md_file, universe, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - del g['particles/trajectory/position'] + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + del g["particles/trajectory/position"] ref_u = universe uw = mda.Universe(TPR_xvf, outfile) - assert_equal(ref_u.trajectory.units['length'], - uw.trajectory.units['length']) + assert_equal( + ref_u.trajectory.units["length"], uw.trajectory.units["length"] + ) for ref_ts, new_ts in zip(ref_u.trajectory, uw.trajectory): assert_equal(ref_ts.dimensions, new_ts.dimensions) - assert_equal(ref_ts.triclinic_dimensions, - new_ts.triclinic_dimensions) + assert_equal( + ref_ts.triclinic_dimensions, new_ts.triclinic_dimensions + ) - @pytest.mark.parametrize('group', ('position', 'velocity', 'force')) + @pytest.mark.parametrize("group", ("position", "velocity", "force")) def test_changing_n_atoms(self, h5md_file, outfile, group): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - g[f'particles/trajectory/{group}/value'].resize((3, 10000, 3)) - with pytest.raises(ValueError, - match=" of either the postion, velocity, or force"): + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + g[f"particles/trajectory/{group}/value"].resize((3, 10000, 3)) + with pytest.raises( + ValueError, match=" of either the postion, velocity, or force" + ): u = mda.Universe(TPR_xvf, outfile) def test_2D_box(self, h5md_file, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) new_box = np.ones(shape=(3, 2, 2)) - g['particles/trajectory/box'].attrs['dimension'] = 2 - del g['particles/trajectory/box/edges/value'] - g['particles/trajectory' - '/box/edges'].create_dataset('value', data=new_box) - with pytest.raises(ValueError, - match="MDAnalysis only supports 3-dimensional"): + g["particles/trajectory/box"].attrs["dimension"] = 2 + del g["particles/trajectory/box/edges/value"] + g["particles/trajectory" "/box/edges"].create_dataset( + "value", data=new_box + ) + with pytest.raises( + ValueError, match="MDAnalysis only supports 3-dimensional" + ): u = mda.Universe(TPR_xvf, outfile) def test_box_vector(self, h5md_file, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) vector = [1, 2, 3] - del g['particles/trajectory/box/edges'] - g['particles/trajectory/box/edges/value'] = [vector, vector, vector] + del g["particles/trajectory/box/edges"] + g["particles/trajectory/box/edges/value"] = [ + vector, + vector, + vector, + ] u = mda.Universe(TPR_xvf, outfile) # values in vector are conveted from nm -> Angstrom assert_equal(u.trajectory.ts.dimensions, [10, 20, 30, 90, 90, 90]) def test_no_box(self, h5md_file, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - del g['particles/trajectory/box/edges'] + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + del g["particles/trajectory/box/edges"] u = mda.Universe(TPR_xvf, outfile) assert_equal(u.trajectory.ts.dimensions, None) def test_no_groups(self, h5md_file, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - del g['particles/trajectory/position'] - del g['particles/trajectory/velocity'] - del g['particles/trajectory/force'] - with pytest.raises(NoDataError, - match="Provide at least a position, velocity"): + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + del g["particles/trajectory/position"] + del g["particles/trajectory/velocity"] + del g["particles/trajectory/force"] + with pytest.raises( + NoDataError, match="Provide at least a position, velocity" + ): u = mda.Universe(TPR_xvf, outfile) def test_no_convert_units(self, h5md_file, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - groups = ['position', 'velocity', 'force'] + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + groups = ["position", "velocity", "force"] for name in groups: - del g['particles/trajectory'][name]['value'].attrs['unit'] - del g['particles/trajectory/position/time'].attrs['unit'] + del g["particles/trajectory"][name]["value"].attrs["unit"] + del g["particles/trajectory/position/time"].attrs["unit"] u = mda.Universe(TPR_xvf, outfile, convert_units=False) for unit in u.trajectory.units: assert_equal(u.trajectory.units[unit], None) def test_no_units_but_convert_units_true_error(self, h5md_file, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - groups = ['position', 'velocity', 'force'] + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + groups = ["position", "velocity", "force"] for name in groups: - del g['particles/trajectory'][name]['value'].attrs['unit'] - del g['particles/trajectory/position/time'].attrs['unit'] - del g['particles/trajectory/box/edges/value'].attrs['unit'] - with pytest.raises(ValueError, - match="H5MD file must have readable units if"): + del g["particles/trajectory"][name]["value"].attrs["unit"] + del g["particles/trajectory/position/time"].attrs["unit"] + del g["particles/trajectory/box/edges/value"].attrs["unit"] + with pytest.raises( + ValueError, match="H5MD file must have readable units if" + ): u = mda.Universe(TPR_xvf, outfile, convert_units=True) - @pytest.mark.xfail(reason='Issue #2884') + @pytest.mark.xfail(reason="Issue #2884") def test_open_filestream(self, h5md_file, universe): with h5md_file as f: u = mda.Universe(TPR_xvf, h5md_file) @@ -420,29 +487,41 @@ def test_open_filestream(self, h5md_file, universe): assert_equal(ts1.forces, ts2.forces) def test_wrong_driver(self): - with pytest.raises(ValueError, - match="If MPI communicator object is used to open"): - u = mda.Universe(TPR_xvf, H5MD_xvf, - driver='wrong_driver', - comm="mock MPI.COMM_WORLD") + with pytest.raises( + ValueError, match="If MPI communicator object is used to open" + ): + u = mda.Universe( + TPR_xvf, + H5MD_xvf, + driver="wrong_driver", + comm="mock MPI.COMM_WORLD", + ) def test_open_with_driver(self): u = mda.Universe(TPR_xvf, H5MD_xvf, driver="core") assert_equal(u.trajectory._file.driver, "core") - @pytest.mark.parametrize('group1, group2', (('velocity', 'force'), - ('position', 'force'), - ('position', 'velocity'))) + @pytest.mark.parametrize( + "group1, group2", + ( + ("velocity", "force"), + ("position", "force"), + ("position", "velocity"), + ), + ) def test_parse_n_atoms(self, h5md_file, outfile, group1, group2): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - traj_group = g['particles/trajectory'] + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + traj_group = g["particles/trajectory"] del traj_group[group1] del traj_group[group2] - for dset in ('position/value', 'velocity/value', - 'force/value'): + for dset in ( + "position/value", + "velocity/value", + "force/value", + ): try: n_atoms_in_dset = traj_group[dset].shape[1] break @@ -454,13 +533,13 @@ def test_parse_n_atoms(self, h5md_file, outfile, group1, group2): def test_parse_n_atoms_error(self, h5md_file, outfile): with h5md_file as f: - with h5py.File(outfile, 'w') as g: - f.copy(source='particles', dest=g) - f.copy(source='h5md', dest=g) - traj_group = g['particles/trajectory'] - del traj_group['position'] - del traj_group['velocity'] - del traj_group['force'] + with h5py.File(outfile, "w") as g: + f.copy(source="particles", dest=g) + f.copy(source="h5md", dest=g) + traj_group = g["particles/trajectory"] + del traj_group["position"] + del traj_group["velocity"] + del traj_group["force"] errmsg = "Could not construct minimal topology" with pytest.raises(ValueError, match=errmsg): @@ -479,10 +558,10 @@ def universe(self): @pytest.fixture() def universe_no_units(self): u = mda.Universe(TPR_xvf, H5MD_xvf, convert_units=False) - u.trajectory.units['time'] = None - u.trajectory.units['length'] = None - u.trajectory.units['velocity'] = None - u.trajectory.units['force'] = None + u.trajectory.units["time"] = None + u.trajectory.units["length"] = None + u.trajectory.units["velocity"] = None + u.trajectory.units["force"] = None return u @pytest.fixture() @@ -491,17 +570,22 @@ def Writer(self): @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir) + 'h5md-writer-test.h5md' + return str(tmpdir) + "h5md-writer-test.h5md" @pytest.fixture() def outtop(self, tmpdir): - return str(tmpdir) + 'h5md-writer-top.pdb' - - @pytest.mark.parametrize('scalar, error, match', - ((0, ValueError, "H5MDWriter: no atoms in output trajectory"), - (0.5, IOError, "H5MDWriter: Timestep does not have"))) - def test_n_atoms_errors(self, universe, Writer, outfile, - scalar, error, match): + return str(tmpdir) + "h5md-writer-top.pdb" + + @pytest.mark.parametrize( + "scalar, error, match", + ( + (0, ValueError, "H5MDWriter: no atoms in output trajectory"), + (0.5, IOError, "H5MDWriter: Timestep does not have"), + ), + ) + def test_n_atoms_errors( + self, universe, Writer, outfile, scalar, error, match + ): n_atoms = universe.atoms.n_atoms * scalar with pytest.raises(error, match=match): with Writer(outfile, n_atoms) as W: @@ -515,7 +599,7 @@ def test_chunk_error(self, universe, Writer, outfile): for ts in universe.trajectory: W.write(universe) - @pytest.mark.parametrize('dimensions', (None, 0)) + @pytest.mark.parametrize("dimensions", (None, 0)) def test_no_dimensions(self, universe, Writer, outfile, dimensions): with Writer(outfile, universe.atoms.n_atoms) as W: for ts in universe.trajectory: @@ -523,34 +607,36 @@ def test_no_dimensions(self, universe, Writer, outfile, dimensions): W.write(universe) uw = mda.Universe(TPR_xvf, outfile) - box = uw.trajectory._particle_group['box'] - assert 'edges' not in box - assert_equal(3*['none'], box.attrs['boundary']) - assert_equal(3, box.attrs['dimension']) + box = uw.trajectory._particle_group["box"] + assert "edges" not in box + assert_equal(3 * ["none"], box.attrs["boundary"]) + assert_equal(3, box.attrs["dimension"]) def test_step_not_monotonic(self, universe, Writer, outfile): - with pytest.raises(ValueError, - match="The H5MD standard dictates that the step "): + with pytest.raises( + ValueError, match="The H5MD standard dictates that the step " + ): with Writer(outfile, universe.atoms.n_atoms) as W: for ts in universe.trajectory[[0, 1, 2, 1]]: W.write(universe) - with pytest.raises(ValueError, - match="The H5MD standard dictates that the step "): + with pytest.raises( + ValueError, match="The H5MD standard dictates that the step " + ): with Writer(outfile, universe.atoms.n_atoms) as W: for ts in universe.trajectory: if ts.frame == 2: - ts.data['step'] = 0 + ts.data["step"] = 0 W.write(universe) def test_step_from_frame(self, universe, Writer, outfile): with Writer(outfile, universe.atoms.n_atoms) as W: for ts in universe.trajectory: - del ts.data['step'] + del ts.data["step"] W.write(universe) uw = mda.Universe(TPR_xvf, outfile) - steps = [ts.data['step'] for ts in uw.trajectory] + steps = [ts.data["step"] for ts in uw.trajectory] frames = [ts.frame for ts in universe.trajectory] for step, frame in zip(steps, frames): assert_equal(step, frame) @@ -559,32 +645,39 @@ def test_has_property(self, universe, Writer, outfile): with Writer(outfile, universe.atoms.n_atoms) as W: W.write(universe) # make sure property is pulled from _has dict - assert W.has_positions == W._has['position'] - assert W.has_velocities == W._has['velocity'] - assert W.has_forces == W._has['force'] + assert W.has_positions == W._has["position"] + assert W.has_velocities == W._has["velocity"] + assert W.has_forces == W._has["force"] # make sure the values are correct assert W.has_positions is True assert W.has_velocities is True assert W.has_forces is True - @pytest.mark.parametrize('pos, vel, force', ( + @pytest.mark.parametrize( + "pos, vel, force", + ( (True, False, False), (True, True, False), (True, False, True), (True, True, True), (False, True, True), (False, False, True), - (False, False, False))) - def test_write_trajectory(self, universe, Writer, outfile, - pos, vel, force): + (False, False, False), + ), + ) + def test_write_trajectory( + self, universe, Writer, outfile, pos, vel, force + ): try: - with Writer(outfile, - universe.atoms.n_atoms, - positions=pos, - velocities=vel, - forces=force, - author='My Name', - author_email='my_email@asu.edu') as W: + with Writer( + outfile, + universe.atoms.n_atoms, + positions=pos, + velocities=vel, + forces=force, + author="My Name", + author_email="my_email@asu.edu", + ) as W: for ts in universe.trajectory: W.write(universe) @@ -592,38 +685,47 @@ def test_write_trajectory(self, universe, Writer, outfile, # check the trajectory contents match reference universes for ts, ref_ts in zip(uw.trajectory, universe.trajectory): - assert_almost_equal(ts.dimensions, ref_ts.dimensions, self.prec) + assert_almost_equal( + ts.dimensions, ref_ts.dimensions, self.prec + ) if pos: assert_almost_equal(ts._pos, ref_ts._pos, self.prec) else: - with pytest.raises(NoDataError, - match="This Timestep has no"): - getattr(ts, 'positions') + with pytest.raises( + NoDataError, match="This Timestep has no" + ): + getattr(ts, "positions") if vel: - assert_almost_equal(ts._velocities, ref_ts._velocities, - self.prec) + assert_almost_equal( + ts._velocities, ref_ts._velocities, self.prec + ) else: - with pytest.raises(NoDataError, - match="This Timestep has no"): - getattr(ts, 'velocities') + with pytest.raises( + NoDataError, match="This Timestep has no" + ): + getattr(ts, "velocities") if force: assert_almost_equal(ts._forces, ref_ts._forces, self.prec) else: - with pytest.raises(NoDataError, - match="This Timestep has no"): - getattr(ts, 'forces') + with pytest.raises( + NoDataError, match="This Timestep has no" + ): + getattr(ts, "forces") # when (False, False, False) - except(ValueError): - with pytest.raises(ValueError, - match="At least one of positions, velocities"): - with Writer(outfile, - universe.atoms.n_atoms, - positions=pos, - velocities=vel, - forces=force, - author='My Name', - author_email='my_email@asu.edu') as W: + except ValueError: + with pytest.raises( + ValueError, match="At least one of positions, velocities" + ): + with Writer( + outfile, + universe.atoms.n_atoms, + positions=pos, + velocities=vel, + forces=force, + author="My Name", + author_email="my_email@asu.edu", + ) as W: for ts in universe.trajectory: W.write(universe) @@ -638,90 +740,125 @@ def test_write_AtomGroup_with(self, universe, outfile, outtop, Writer): uw = mda.Universe(outtop, outfile) caw = uw.atoms - for orig_ts, written_ts in zip(universe.trajectory, - uw.trajectory): + for orig_ts, written_ts in zip(universe.trajectory, uw.trajectory): assert_almost_equal(ca.positions, caw.positions, self.prec) assert_almost_equal(orig_ts.time, written_ts.time, self.prec) - assert_almost_equal(written_ts.dimensions, - orig_ts.dimensions, - self.prec) - - @pytest.mark.parametrize('frames, n_frames', ((None, 1), - ('all', 3))) - def test_ag_write(self, universe, outfile, outtop, - Writer, frames, n_frames): + assert_almost_equal( + written_ts.dimensions, orig_ts.dimensions, self.prec + ) + + @pytest.mark.parametrize("frames, n_frames", ((None, 1), ("all", 3))) + def test_ag_write( + self, universe, outfile, outtop, Writer, frames, n_frames + ): """test to write with ag.write()""" ca = universe.select_atoms("protein and name CA") ca.write(outtop) - ca.write(outfile, frames=frames, format='h5md') + ca.write(outfile, frames=frames, format="h5md") uw = mda.Universe(outtop, outfile) caw = uw.atoms assert_equal(n_frames, len(uw.trajectory)) - for orig_ts, written_ts in zip(universe.trajectory, - uw.trajectory): + for orig_ts, written_ts in zip(universe.trajectory, uw.trajectory): assert_almost_equal(ca.positions, caw.positions, self.prec) assert_almost_equal(orig_ts.time, written_ts.time, self.prec) - assert_almost_equal(written_ts.dimensions, - orig_ts.dimensions, - self.prec) - - @pytest.mark.parametrize('timeunit, lengthunit, velocityunit, forceunit', ( - ('fs', 'Angstrom', 'Angstrom/ps', 'kJ/(mol*Angstrom)'), - ('s', 'pm', 'm/s', 'Newton'), - ('ps', 'fm', 'Angstrom/fs', 'kcal/(mol*Angstrom)',), - ('AKMA', 'nm', 'Angstrom/AKMA', 'kcal/(mol*Angstrom)'))) - def test_write_custom_units(self, universe, outfile, Writer, - timeunit, lengthunit, - velocityunit, forceunit): - with Writer(outfile, - universe.atoms.n_atoms, - lengthunit=lengthunit, - velocityunit=velocityunit, - forceunit=forceunit, - timeunit=timeunit) as W: + assert_almost_equal( + written_ts.dimensions, orig_ts.dimensions, self.prec + ) + + @pytest.mark.parametrize( + "timeunit, lengthunit, velocityunit, forceunit", + ( + ("fs", "Angstrom", "Angstrom/ps", "kJ/(mol*Angstrom)"), + ("s", "pm", "m/s", "Newton"), + ( + "ps", + "fm", + "Angstrom/fs", + "kcal/(mol*Angstrom)", + ), + ("AKMA", "nm", "Angstrom/AKMA", "kcal/(mol*Angstrom)"), + ), + ) + def test_write_custom_units( + self, + universe, + outfile, + Writer, + timeunit, + lengthunit, + velocityunit, + forceunit, + ): + with Writer( + outfile, + universe.atoms.n_atoms, + lengthunit=lengthunit, + velocityunit=velocityunit, + forceunit=forceunit, + timeunit=timeunit, + ) as W: for ts in universe.trajectory: W.write(universe) u = mda.Universe(TPR_xvf, outfile) - for u_unit, custom_unit in zip(u.trajectory.units.values(), - (timeunit, lengthunit, - velocityunit, forceunit)): + for u_unit, custom_unit in zip( + u.trajectory.units.values(), + (timeunit, lengthunit, velocityunit, forceunit), + ): assert_equal(u_unit, custom_unit) - @pytest.mark.parametrize('timeunit, lengthunit, velocityunit, forceunit', ( - ('imaginary time', None, None, None), - (None, None, 'c', None), - (None, None, None, 'HUGE FORCE',), - (None, 'lightyear', None, None),)) - def test_write_bad_units(self, universe, outfile, Writer, - timeunit, lengthunit, - velocityunit, forceunit): + @pytest.mark.parametrize( + "timeunit, lengthunit, velocityunit, forceunit", + ( + ("imaginary time", None, None, None), + (None, None, "c", None), + ( + None, + None, + None, + "HUGE FORCE", + ), + (None, "lightyear", None, None), + ), + ) + def test_write_bad_units( + self, + universe, + outfile, + Writer, + timeunit, + lengthunit, + velocityunit, + forceunit, + ): with pytest.raises(ValueError, match=" is not a unit recognized by"): - with Writer(outfile, - universe.atoms.n_atoms, - lengthunit=lengthunit, - velocityunit=velocityunit, - forceunit=forceunit, - timeunit=timeunit) as W: + with Writer( + outfile, + universe.atoms.n_atoms, + lengthunit=lengthunit, + velocityunit=velocityunit, + forceunit=forceunit, + timeunit=timeunit, + ) as W: for ts in universe.trajectory: W.write(universe) def test_no_units_w_convert_true(self, universe_no_units, outfile, Writer): # no units + convert_units = ValueError with pytest.raises(ValueError, match="The trajectory has no units,"): - with Writer(outfile, - universe_no_units.atoms.n_atoms) as W: + with Writer(outfile, universe_no_units.atoms.n_atoms) as W: for ts in universe_no_units.trajectory: W.write(universe_no_units) - def test_no_units_w_convert_false(self, universe_no_units, - outfile, Writer): - with Writer(outfile, - universe_no_units.atoms.n_atoms, - convert_units=False) as W: + def test_no_units_w_convert_false( + self, universe_no_units, outfile, Writer + ): + with Writer( + outfile, universe_no_units.atoms.n_atoms, convert_units=False + ) as W: for ts in universe_no_units.trajectory: W.write(universe_no_units) @@ -729,12 +866,11 @@ def test_no_units_w_convert_false(self, universe_no_units, for unit in uw.trajectory.units.values(): assert_equal(unit, None) - @pytest.mark.parametrize('convert_units', (True, False)) - def test_convert_units(self, universe, outfile, Writer, - convert_units): - with Writer(outfile, - universe.atoms.n_atoms, - convert_units=convert_units) as W: + @pytest.mark.parametrize("convert_units", (True, False)) + def test_convert_units(self, universe, outfile, Writer, convert_units): + with Writer( + outfile, universe.atoms.n_atoms, convert_units=convert_units + ) as W: for ts in universe.trajectory: W.write(universe) @@ -744,20 +880,20 @@ def test_convert_units(self, universe, outfile, Writer, for u1, u2 in zip(ref_units, uw_units): assert_equal(u1, u2) - @pytest.mark.parametrize('chunks', ((3, 1000, 1), - (1, 1000, 3), - (100, 100, 3))) + @pytest.mark.parametrize( + "chunks", ((3, 1000, 1), (1, 1000, 3), (100, 100, 3)) + ) def test_write_chunks(self, universe, outfile, Writer, chunks): - with Writer(outfile, - universe.atoms.n_atoms, - chunks=chunks) as W: + with Writer(outfile, universe.atoms.n_atoms, chunks=chunks) as W: for ts in universe.trajectory: W.write(universe) uw = mda.Universe(TPR_xvf, outfile) - for dset in (uw.trajectory._particle_group['position/value'], - uw.trajectory._particle_group['velocity/value'], - uw.trajectory._particle_group['force/value']): + for dset in ( + uw.trajectory._particle_group["position/value"], + uw.trajectory._particle_group["velocity/value"], + uw.trajectory._particle_group["force/value"], + ): assert_equal(dset.chunks, chunks) for ts1, ts2 in zip(universe.trajectory, uw.trajectory): @@ -768,16 +904,16 @@ def test_write_chunks(self, universe, outfile, Writer, chunks): def test_write_chunks_with_nframes(self, universe, outfile, Writer): n_atoms = universe.atoms.n_atoms n_frames = universe.trajectory.n_frames - with Writer(outfile, - n_atoms=n_atoms, - n_frames=n_frames) as W: + with Writer(outfile, n_atoms=n_atoms, n_frames=n_frames) as W: for ts in universe.trajectory: W.write(universe) uw = mda.Universe(TPR_xvf, outfile) - for dset in (uw.trajectory._particle_group['position/value'], - uw.trajectory._particle_group['velocity/value'], - uw.trajectory._particle_group['force/value']): + for dset in ( + uw.trajectory._particle_group["position/value"], + uw.trajectory._particle_group["velocity/value"], + uw.trajectory._particle_group["force/value"], + ): assert_equal(dset.chunks, (1, n_atoms, 3)) for ts1, ts2 in zip(universe.trajectory, uw.trajectory): @@ -788,55 +924,59 @@ def test_write_chunks_with_nframes(self, universe, outfile, Writer): def test_write_contiguous1(self, universe, Writer, outfile): n_atoms = universe.atoms.n_atoms n_frames = len(universe.trajectory) - with Writer(outfile, - n_atoms=n_atoms, - n_frames=n_frames, - chunks=False) as W: + with Writer( + outfile, n_atoms=n_atoms, n_frames=n_frames, chunks=False + ) as W: for ts in universe.trajectory: W.write(universe) uw = mda.Universe(TPR_xvf, outfile) - for dset in (uw.trajectory._particle_group['position/value'], - uw.trajectory._particle_group['velocity/value'], - uw.trajectory._particle_group['force/value']): + for dset in ( + uw.trajectory._particle_group["position/value"], + uw.trajectory._particle_group["velocity/value"], + uw.trajectory._particle_group["force/value"], + ): assert_equal(dset.chunks, None) def test_write_contiguous2(self, universe, Writer, outfile): - ag = universe.select_atoms('all') + ag = universe.select_atoms("all") n_frames = len(ag.universe.trajectory) - ag.write(outfile, frames='all', n_frames=n_frames, chunks=False) + ag.write(outfile, frames="all", n_frames=n_frames, chunks=False) uw = mda.Universe(TPR_xvf, outfile) - for dset in (uw.trajectory._particle_group['position/value'], - uw.trajectory._particle_group['velocity/value'], - uw.trajectory._particle_group['force/value']): + for dset in ( + uw.trajectory._particle_group["position/value"], + uw.trajectory._particle_group["velocity/value"], + uw.trajectory._particle_group["force/value"], + ): assert_equal(dset.chunks, None) - @pytest.mark.parametrize('filter, opts', (('gzip', 1), - ('gzip', 9), - ('lzf', None))) - def test_write_with_compression(self, universe, - outfile, Writer, - filter, opts): - with Writer(outfile, - universe.atoms.n_atoms, - compression=filter, - compression_opts=opts) as W: + @pytest.mark.parametrize( + "filter, opts", (("gzip", 1), ("gzip", 9), ("lzf", None)) + ) + def test_write_with_compression( + self, universe, outfile, Writer, filter, opts + ): + with Writer( + outfile, + universe.atoms.n_atoms, + compression=filter, + compression_opts=opts, + ) as W: for ts in universe.trajectory: W.write(universe) uw = mda.Universe(TPR_xvf, outfile) - dset = uw.trajectory._particle_group['position/value'] + dset = uw.trajectory._particle_group["position/value"] assert_equal(dset.compression, filter) assert_equal(dset.compression_opts, opts) - @pytest.mark.xfail(os.name == 'nt', - reason="occasional PermissionError on windows") - @pytest.mark.parametrize('driver', ('core', 'stdio')) + @pytest.mark.xfail( + os.name == "nt", reason="occasional PermissionError on windows" + ) + @pytest.mark.parametrize("driver", ("core", "stdio")) def test_write_with_drivers(self, universe, outfile, Writer, driver): - with Writer(outfile, - universe.atoms.n_atoms, - driver=driver) as W: + with Writer(outfile, universe.atoms.n_atoms, driver=driver) as W: for ts in universe.trajectory: W.write(universe) @@ -844,13 +984,11 @@ def test_write_with_drivers(self, universe, outfile, Writer, driver): file = uw.trajectory._file assert_equal(file.driver, driver) - def test_parallel_disabled(self, universe, Writer, outfile, - driver='mpio'): - with pytest.raises(ValueError, - match="H5MDWriter: parallel writing with MPI I/O "): - with Writer(outfile, - universe.atoms.n_atoms, - driver=driver) as W: + def test_parallel_disabled(self, universe, Writer, outfile, driver="mpio"): + with pytest.raises( + ValueError, match="H5MDWriter: parallel writing with MPI I/O " + ): + with Writer(outfile, universe.atoms.n_atoms, driver=driver) as W: for ts in universe.trajectory: W.write(universe) @@ -870,9 +1008,11 @@ def test_timestep_not_modified_by_writer(self, universe, Writer, outfile): assert_equal( ts._pos, x, - err_msg="Positions in Timestep were modified by writer.") + err_msg="Positions in Timestep were modified by writer.", + ) assert_equal( - ts.time, time, err_msg="Time in Timestep was modified by writer.") + ts.time, time, err_msg="Time in Timestep was modified by writer." + ) class TestH5PYNotInstalled(object): @@ -881,7 +1021,8 @@ class TestH5PYNotInstalled(object): @pytest.fixture(autouse=True) def block_h5py(self, monkeypatch): monkeypatch.setattr( - sys.modules['MDAnalysis.coordinates.H5MD'], 'HAS_H5PY', False) + sys.modules["MDAnalysis.coordinates.H5MD"], "HAS_H5PY", False + ) @pytest.fixture() def Writer(self): @@ -889,7 +1030,7 @@ def Writer(self): @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir) + 'h5md-writer-test.h5md' + return str(tmpdir) + "h5md-writer-test.h5md" def test_reader_no_h5py(self): with pytest.raises(RuntimeError, match="Please install h5py"): @@ -897,10 +1038,10 @@ def test_reader_no_h5py(self): def test_writer_no_h5py(self, Writer, outfile): u = mda.Universe(TPR_xvf, TRR_xvf) - with pytest.raises(RuntimeError, - match="H5MDWriter: Please install h5py"): - with Writer(outfile, - u.atoms.n_atoms) as W: + with pytest.raises( + RuntimeError, match="H5MDWriter: Please install h5py" + ): + with Writer(outfile, u.atoms.n_atoms) as W: for ts in u.trajectory: W.write(universe) @@ -910,7 +1051,7 @@ class TestH5MDReaderWithObservables(object): """Read H5MD file with 'observables/atoms/energy'.""" prec = 3 - ext = 'h5md' + ext = "h5md" def test_read_h5md_issue4598(self): """Read a H5MD file with observables. diff --git a/testsuite/MDAnalysisTests/coordinates/test_lammps.py b/testsuite/MDAnalysisTests/coordinates/test_lammps.py index 39f693c75ca..3b203f5bae2 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_lammps.py +++ b/testsuite/MDAnalysisTests/coordinates/test_lammps.py @@ -29,25 +29,37 @@ import MDAnalysis as mda from MDAnalysis import NoDataError -from numpy.testing import (assert_equal, assert_allclose) +from numpy.testing import assert_equal, assert_allclose from MDAnalysisTests import make_Universe from MDAnalysisTests.coordinates.reference import ( - RefLAMMPSData, RefLAMMPSDataMini, RefLAMMPSDataDCD, - RefLAMMPSDataAdditionalColumns + RefLAMMPSData, + RefLAMMPSDataMini, + RefLAMMPSDataDCD, + RefLAMMPSDataAdditionalColumns, ) from MDAnalysisTests.datafiles import ( - LAMMPScnt, LAMMPShyd, LAMMPSdata, LAMMPSdata_mini, LAMMPSdata_triclinic, - LAMMPSDUMP, LAMMPSDUMP_allcoords, LAMMPSDUMP_nocoords, LAMMPSDUMP_triclinic, - LAMMPSDUMP_image_vf, LAMMPS_image_vf, LAMMPSdata_additional_columns, - LAMMPSDUMP_additional_columns + LAMMPScnt, + LAMMPShyd, + LAMMPSdata, + LAMMPSdata_mini, + LAMMPSdata_triclinic, + LAMMPSDUMP, + LAMMPSDUMP_allcoords, + LAMMPSDUMP_nocoords, + LAMMPSDUMP_triclinic, + LAMMPSDUMP_image_vf, + LAMMPS_image_vf, + LAMMPSdata_additional_columns, + LAMMPSDUMP_additional_columns, ) def test_datareader_ValueError(): from MDAnalysis.coordinates.LAMMPS import DATAReader + with pytest.raises(ValueError): - DATAReader('filename') + DATAReader("filename") class _TestLammpsData_Coords(object): @@ -56,7 +68,7 @@ class _TestLammpsData_Coords(object): All topology loading from MDAnalysisTests.data is done in test_topology """ - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(self): return mda.Universe(self.filename) @@ -97,17 +109,20 @@ class TestLammpsDataMini_Coords(_TestLammpsData_Coords, RefLAMMPSDataMini): pass -@pytest.fixture(params=[ - LAMMPSdata, - LAMMPSdata_mini, - LAMMPScnt, - LAMMPShyd, -], scope='module') +@pytest.fixture( + params=[ + LAMMPSdata, + LAMMPSdata_mini, + LAMMPScnt, + LAMMPShyd, + ], + scope="module", +) def LAMMPSDATAWriter(request, tmpdir_factory): filename = request.param u = mda.Universe(filename) fn = os.path.split(filename)[1] - outfile = str(tmpdir_factory.mktemp('data').join(fn)) + outfile = str(tmpdir_factory.mktemp("data").join(fn)) with mda.Writer(outfile, n_atoms=u.atoms.n_atoms) as w: w.write(u.atoms) @@ -117,23 +132,26 @@ def LAMMPSDATAWriter(request, tmpdir_factory): return u, u_new -@pytest.fixture(params=[ - [LAMMPSdata, True], - [LAMMPSdata_mini, True], - [LAMMPScnt, True], - [LAMMPShyd, True], - [LAMMPSdata, False] -], scope='module') +@pytest.fixture( + params=[ + [LAMMPSdata, True], + [LAMMPSdata_mini, True], + [LAMMPScnt, True], + [LAMMPShyd, True], + [LAMMPSdata, False], + ], + scope="module", +) def LAMMPSDATAWriter_molecule_tag(request, tmpdir_factory): filename, charges = request.param u = mda.Universe(filename) if not charges: - u.del_TopologyAttr('charges') + u.del_TopologyAttr("charges") - u.trajectory.ts.data['molecule_tag'] = u.atoms.resids + u.trajectory.ts.data["molecule_tag"] = u.atoms.resids fn = os.path.split(filename)[1] - outfile = str(tmpdir_factory.mktemp('data').join(fn)) + outfile = str(tmpdir_factory.mktemp("data").join(fn)) with mda.Writer(outfile, n_atoms=u.atoms.n_atoms) as w: w.write(u.atoms) @@ -145,84 +163,106 @@ def LAMMPSDATAWriter_molecule_tag(request, tmpdir_factory): def test_unwrap_vel_force(): - u_wrapped = mda.Universe(LAMMPS_image_vf, [LAMMPSDUMP_image_vf], - format="LAMMPSDUMP") + u_wrapped = mda.Universe( + LAMMPS_image_vf, [LAMMPSDUMP_image_vf], format="LAMMPSDUMP" + ) u_wrapped.trajectory[-1] - - assert_allclose(u_wrapped.atoms.positions[0], - np.array([2.56616, 6.11565, 7.37956]), - atol=1e-5) + + assert_allclose( + u_wrapped.atoms.positions[0], + np.array([2.56616, 6.11565, 7.37956]), + atol=1e-5, + ) assert hasattr(u_wrapped.atoms, "velocities") assert hasattr(u_wrapped.atoms, "forces") def test_unwrap_image_wrap(): - u_unwrapped = mda.Universe(LAMMPS_image_vf, LAMMPSDUMP_image_vf, - format="LAMMPSDUMP", unwrap_images=True) + u_unwrapped = mda.Universe( + LAMMPS_image_vf, + LAMMPSDUMP_image_vf, + format="LAMMPSDUMP", + unwrap_images=True, + ) u_unwrapped.trajectory[-1] - pos = (np.array([2.56616, 6.11565, 7.37956]) + - np.array([3, 1, -4])*u_unwrapped.dimensions[:3]) - assert_allclose(u_unwrapped.atoms.positions[0], - pos, - atol=1e-5, - ) + pos = ( + np.array([2.56616, 6.11565, 7.37956]) + + np.array([3, 1, -4]) * u_unwrapped.dimensions[:3] + ) + assert_allclose( + u_unwrapped.atoms.positions[0], + pos, + atol=1e-5, + ) def test_unwrap_no_image(): with pytest.raises(ValueError, match="must have image flag"): - u_unwrapped = mda.Universe( - LAMMPSDUMP_allcoords, - format="LAMMPSDUMP", - unwrap_images=True) + u_unwrapped = mda.Universe( + LAMMPSDUMP_allcoords, format="LAMMPSDUMP", unwrap_images=True + ) class TestLAMMPSDATAWriter(object): def test_Writer_dimensions(self, LAMMPSDATAWriter): u_ref, u_new = LAMMPSDATAWriter - assert_allclose(u_ref.dimensions, u_new.dimensions, - err_msg="attributes different after writing", - atol=1e-6) + assert_allclose( + u_ref.dimensions, + u_new.dimensions, + err_msg="attributes different after writing", + atol=1e-6, + ) def test_Writer_atoms_types(self, LAMMPSDATAWriter): u_ref, u_new = LAMMPSDATAWriter - assert_equal(u_ref.atoms.types, u_new.atoms.types, - err_msg="attributes different after writing",) - - @pytest.mark.parametrize('attr', [ - 'bonds', 'angles', 'dihedrals', 'impropers' - ]) + assert_equal( + u_ref.atoms.types, + u_new.atoms.types, + err_msg="attributes different after writing", + ) + + @pytest.mark.parametrize( + "attr", ["bonds", "angles", "dihedrals", "impropers"] + ) def test_Writer_atoms(self, attr, LAMMPSDATAWriter): u_ref, u_new = LAMMPSDATAWriter ref = getattr(u_ref.atoms, attr) new = getattr(u_new.atoms, attr) assert ref == new, "attributes different after writing" - @pytest.mark.parametrize('attr', [ - 'masses', 'charges', 'velocities', 'positions' - ]) + @pytest.mark.parametrize( + "attr", ["masses", "charges", "velocities", "positions"] + ) def test_Writer_numerical_attrs(self, attr, LAMMPSDATAWriter): u_ref, u_new = LAMMPSDATAWriter try: refvals = getattr(u_ref, attr) - except (AttributeError): + except AttributeError: with pytest.raises(AttributeError): getattr(u_new, attr) else: - assert_allclose(refvals, - getattr(u_new.atoms, attr), - err_msg="attributes different after writing", - atol=1e-6) + assert_allclose( + refvals, + getattr(u_new.atoms, attr), + err_msg="attributes different after writing", + atol=1e-6, + ) class TestLAMMPSDATAWriter_molecule_tag(object): def test_molecule_tag(self, LAMMPSDATAWriter_molecule_tag): u_ref, u_new = LAMMPSDATAWriter_molecule_tag - assert_equal(u_ref.atoms.resids, u_new.atoms.resids, - err_msg="resids different after writing",) + assert_equal( + u_ref.atoms.resids, + u_new.atoms.resids, + err_msg="resids different after writing", + ) -@pytest.mark.parametrize('filename', ['out.data', 'out.data.bz2', 'out.data.gz']) +@pytest.mark.parametrize( + "filename", ["out.data", "out.data.bz2", "out.data.gz"] +) def test_datawriter_universe(filename, tmpdir): """ Test roundtrip on datawriter, and also checks compressed files @@ -251,7 +291,7 @@ def LAMMPSDATA_partial(tmpdir): N_kept = 5 u = mda.Universe(filename) ext = os.path.splitext(filename)[1] - outfile = str(tmpdir.join('lammps-data-writer-test' + ext)) + outfile = str(tmpdir.join("lammps-data-writer-test" + ext)) with mda.Writer(outfile, n_atoms=N_kept) as w: w.write(u.atoms[:N_kept]) @@ -260,16 +300,18 @@ def LAMMPSDATA_partial(tmpdir): return u, u_new - @pytest.mark.parametrize('attr', [ - 'masses', 'charges', 'velocities', 'positions' - ]) + @pytest.mark.parametrize( + "attr", ["masses", "charges", "velocities", "positions"] + ) def test_Writer_atoms(self, attr, LAMMPSDATA_partial): u_ref, u_new = LAMMPSDATA_partial if hasattr(u_ref.atoms, attr): - assert_allclose(getattr(u_ref.atoms[:self.N_kept], attr), - getattr(u_new.atoms, attr), - err_msg="attributes different after writing", - atol=1e-6) + assert_allclose( + getattr(u_ref.atoms[: self.N_kept], attr), + getattr(u_new.atoms, attr), + err_msg="attributes different after writing", + atol=1e-6, + ) else: with pytest.raises(AttributeError): getattr(u_new, attr) @@ -285,13 +327,13 @@ def test_n_angles(self, LAMMPSDATA_partial): # need more tests of the LAMMPS DCDReader + class TestLAMMPSDCDReader(RefLAMMPSDataDCD): - flavor = 'LAMMPS' + flavor = "LAMMPS" - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(self): - return mda.Universe(self.topology, self.trajectory, - format=self.format) + return mda.Universe(self.topology, self.trajectory, format=self.format) def test_Reader_is_LAMMPS(self, u): assert u.trajectory.flavor, self.flavor @@ -308,91 +350,113 @@ def test_n_frames(self, u): assert_equal(u.trajectory.n_frames, self.n_frames) def test_dimensions(self, u): - mean_dimensions = np.mean([ts.dimensions.copy() for ts in u.trajectory], - axis=0) + mean_dimensions = np.mean( + [ts.dimensions.copy() for ts in u.trajectory], axis=0 + ) assert_allclose(mean_dimensions, self.mean_dimensions) def test_dt(self, u): - assert_allclose(u.trajectory.dt, self.dt, - err_msg="Time between frames dt is wrong.") + assert_allclose( + u.trajectory.dt, + self.dt, + err_msg="Time between frames dt is wrong.", + ) def test_Timestep_time(self, u): iframe = self.get_frame_from_end(1, u) - assert_allclose(u.trajectory[iframe].time, - iframe * self.dt, - err_msg="Time for frame {0} (dt={1}) is wrong.".format( - iframe, self.dt)) - - def test_LAMMPSDCDReader_set_dt(self, u, dt=1500.): - u = mda.Universe(self.topology, self.trajectory, format=self.format, - dt=dt) + assert_allclose( + u.trajectory[iframe].time, + iframe * self.dt, + err_msg="Time for frame {0} (dt={1}) is wrong.".format( + iframe, self.dt + ), + ) + + def test_LAMMPSDCDReader_set_dt(self, u, dt=1500.0): + u = mda.Universe( + self.topology, self.trajectory, format=self.format, dt=dt + ) iframe = self.get_frame_from_end(1, u) - assert_allclose(u.trajectory[iframe].time, iframe * dt, - err_msg="setting time step dt={0} failed: " - "actually used dt={1}".format( - dt, u.trajectory._ts_kwargs['dt'])) + assert_allclose( + u.trajectory[iframe].time, + iframe * dt, + err_msg="setting time step dt={0} failed: " + "actually used dt={1}".format(dt, u.trajectory._ts_kwargs["dt"]), + ) def test_wrong_time_unit(self): def wrong_load(unit="nm"): - return mda.Universe(self.topology, self.trajectory, - format=self.format, - timeunit=unit) + return mda.Universe( + self.topology, + self.trajectory, + format=self.format, + timeunit=unit, + ) with pytest.raises(TypeError): wrong_load() def test_wrong_unit(self): def wrong_load(unit="GARBAGE"): - return mda.Universe(self.topology, self.trajectory, - format=self.format, - timeunit=unit) + return mda.Universe( + self.topology, + self.trajectory, + format=self.format, + timeunit=unit, + ) with pytest.raises(ValueError): wrong_load() class TestLAMMPSDCDWriter(RefLAMMPSDataDCD): - flavor = 'LAMMPS' + flavor = "LAMMPS" - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(self): return mda.Universe(self.topology, self.trajectory, format=self.format) def test_Writer_is_LAMMPS(self, u, tmpdir): ext = os.path.splitext(self.trajectory)[1] - outfile = str(tmpdir.join('lammps-writer-test' + ext)) - with mda.Writer(outfile, n_atoms=u.atoms.n_atoms, - format=self.format) as W: + outfile = str(tmpdir.join("lammps-writer-test" + ext)) + with mda.Writer( + outfile, n_atoms=u.atoms.n_atoms, format=self.format + ) as W: assert W.flavor, self.flavor def test_Writer(self, u, tmpdir, n_frames=3): ext = os.path.splitext(self.trajectory)[1] - outfile = str(tmpdir.join('lammps-writer-test' + ext)) + outfile = str(tmpdir.join("lammps-writer-test" + ext)) - with mda.Writer(outfile, - n_atoms=u.atoms.n_atoms, - format=self.format) as w: + with mda.Writer( + outfile, n_atoms=u.atoms.n_atoms, format=self.format + ) as w: for ts in u.trajectory[:n_frames]: w.write(u) short = mda.Universe(self.topology, outfile) - assert_equal(short.trajectory.n_frames, n_frames, - err_msg="number of frames mismatch") - assert_allclose(short.trajectory[n_frames - 1].positions, - u.trajectory[n_frames - 1].positions, - 6, - err_msg="coordinate mismatch between corresponding frames") + assert_equal( + short.trajectory.n_frames, + n_frames, + err_msg="number of frames mismatch", + ) + assert_allclose( + short.trajectory[n_frames - 1].positions, + u.trajectory[n_frames - 1].positions, + 6, + err_msg="coordinate mismatch between corresponding frames", + ) def test_OtherWriter_is_LAMMPS(self, u, tmpdir): ext = os.path.splitext(self.trajectory)[1] - outfile = str(tmpdir.join('lammps-writer-test' + ext)) + outfile = str(tmpdir.join("lammps-writer-test" + ext)) with u.trajectory.OtherWriter(outfile) as W: assert W.flavor, self.flavor def test_OtherWriter(self, u, tmpdir): times = [] ext = os.path.splitext(self.trajectory)[1] - outfile = str(tmpdir.join('lammps-writer-test' + ext)) + outfile = str(tmpdir.join("lammps-writer-test" + ext)) with u.trajectory.OtherWriter(outfile) as w: for ts in u.trajectory[::-1]: times.append(ts.time) @@ -403,27 +467,36 @@ def test_OtherWriter(self, u, tmpdir): # but DCD has no way to store timestamps. Right now, we'll simply # test that this is the case and pass. reversed = mda.Universe(self.topology, outfile) - assert_equal(reversed.trajectory.n_frames, u.trajectory.n_frames, - err_msg="number of frames mismatch") + assert_equal( + reversed.trajectory.n_frames, + u.trajectory.n_frames, + err_msg="number of frames mismatch", + ) rev_times = [ts.time for ts in reversed.trajectory] - assert_allclose(rev_times, times[::-1], 6, - err_msg="time steps of written DCD mismatch") - assert_allclose(reversed.trajectory[-1].positions, - u.trajectory[0].positions, - 6, - err_msg="coordinate mismatch between corresponding frames") + assert_allclose( + rev_times, + times[::-1], + 6, + err_msg="time steps of written DCD mismatch", + ) + assert_allclose( + reversed.trajectory[-1].positions, + u.trajectory[0].positions, + 6, + err_msg="coordinate mismatch between corresponding frames", + ) class TestLAMMPSDCDWriterClass(object): - flavor = 'LAMMPS' + flavor = "LAMMPS" def test_Writer_is_LAMMPS(self, tmpdir): - outfile = str(tmpdir.join('lammps-writer-test.dcd')) + outfile = str(tmpdir.join("lammps-writer-test.dcd")) with mda.coordinates.LAMMPS.DCDWriter(outfile, n_atoms=10) as W: assert W.flavor, self.flavor def test_open(self, tmpdir): - outfile = str(tmpdir.join('lammps-writer-test.dcd')) + outfile = str(tmpdir.join("lammps-writer-test.dcd")) try: with mda.coordinates.LAMMPS.DCDWriter(outfile, n_atoms=10): pass @@ -431,127 +504,148 @@ def test_open(self, tmpdir): pytest.fail() def test_wrong_time_unit(self, tmpdir): - outfile = str(tmpdir.join('lammps-writer-test.dcd')) + outfile = str(tmpdir.join("lammps-writer-test.dcd")) with pytest.raises(TypeError): - with mda.coordinates.LAMMPS.DCDWriter(outfile, n_atoms=10, - timeunit='nm'): + with mda.coordinates.LAMMPS.DCDWriter( + outfile, n_atoms=10, timeunit="nm" + ): pass def test_wrong_unit(self, tmpdir): - outfile = str(tmpdir.join('lammps-writer-test.dcd')) + outfile = str(tmpdir.join("lammps-writer-test.dcd")) with pytest.raises(ValueError): - with mda.coordinates.LAMMPS.DCDWriter(outfile, n_atoms=10, - timeunit='GARBAGE'): + with mda.coordinates.LAMMPS.DCDWriter( + outfile, n_atoms=10, timeunit="GARBAGE" + ): pass def test_triclinicness(): u = mda.Universe(LAMMPScnt) - assert u.dimensions[3] == 90. - assert u.dimensions[4] == 90. - assert u.dimensions[5] == 120. + assert u.dimensions[3] == 90.0 + assert u.dimensions[4] == 90.0 + assert u.dimensions[5] == 120.0 @pytest.fixture def tmpout(tmpdir): - return str(tmpdir.join('out.data')) + return str(tmpdir.join("out.data")) class TestDataWriterErrors(object): def test_write_no_masses(self, tmpout): - u = make_Universe(('types',), trajectory=True) + u = make_Universe(("types",), trajectory=True) try: u.atoms.write(tmpout) except NoDataError as e: - assert 'masses' in e.args[0] + assert "masses" in e.args[0] else: pytest.fail() def test_write_no_types(self, tmpout): - u = make_Universe(('masses',), trajectory=True) + u = make_Universe(("masses",), trajectory=True) try: u.atoms.write(tmpout) except NoDataError as e: - assert 'types' in e.args[0] + assert "types" in e.args[0] else: pytest.fail() def test_write_non_numerical_types(self, tmpout): - u = make_Universe(('types', 'masses'), trajectory=True) + u = make_Universe(("types", "masses"), trajectory=True) try: u.atoms.write(tmpout) except ValueError as e: - assert 'must be convertible to integers' in e.args[0] + assert "must be convertible to integers" in e.args[0] else: raise pytest.fail() class TestLammpsDumpReader(object): - @pytest.fixture( - params=['ascii', 'bz2', 'gzip'] - ) + @pytest.fixture(params=["ascii", "bz2", "gzip"]) def u(self, tmpdir, request): trjtype = request.param - if trjtype == 'bz2': + if trjtype == "bz2": # no conversion needed f = LAMMPSDUMP else: - f = str(tmpdir.join('lammps.' + trjtype)) - with bz2.BZ2File(LAMMPSDUMP, 'rb') as datain: + f = str(tmpdir.join("lammps." + trjtype)) + with bz2.BZ2File(LAMMPSDUMP, "rb") as datain: data = datain.read() - if trjtype == 'ascii': - with open(f, 'wb') as fout: + if trjtype == "ascii": + with open(f, "wb") as fout: fout.write(data) - elif trjtype == 'gzip': - with gzip.GzipFile(f, 'wb') as fout: + elif trjtype == "gzip": + with gzip.GzipFile(f, "wb") as fout: fout.write(data) - yield mda.Universe(f, format='LAMMPSDUMP', - lammps_coordinate_convention="auto") + yield mda.Universe( + f, format="LAMMPSDUMP", lammps_coordinate_convention="auto" + ) @pytest.fixture() def u_additional_columns_true(self): f = LAMMPSDUMP_additional_columns top = LAMMPSdata_additional_columns - return mda.Universe(top, f, format='LAMMPSDUMP', - lammps_coordinate_convention="auto", - additional_columns=True) + return mda.Universe( + top, + f, + format="LAMMPSDUMP", + lammps_coordinate_convention="auto", + additional_columns=True, + ) @pytest.fixture() def u_additional_columns_single(self): f = LAMMPSDUMP_additional_columns top = LAMMPSdata_additional_columns - return mda.Universe(top, f, format='LAMMPSDUMP', - lammps_coordinate_convention="auto", - additional_columns=['q']) + return mda.Universe( + top, + f, + format="LAMMPSDUMP", + lammps_coordinate_convention="auto", + additional_columns=["q"], + ) @pytest.fixture() def u_additional_columns_multiple(self): f = LAMMPSDUMP_additional_columns top = LAMMPSdata_additional_columns - return mda.Universe(top, f, format='LAMMPSDUMP', - lammps_coordinate_convention="auto", - additional_columns=['q', 'p']) + return mda.Universe( + top, + f, + format="LAMMPSDUMP", + lammps_coordinate_convention="auto", + additional_columns=["q", "p"], + ) @pytest.fixture() def u_additional_columns_wrong_format(self): f = LAMMPSDUMP_additional_columns top = LAMMPSdata_additional_columns - return mda.Universe(top, f, format='LAMMPSDUMP', - lammps_coordinate_convention="auto", - additional_columns='q') + return mda.Universe( + top, + f, + format="LAMMPSDUMP", + lammps_coordinate_convention="auto", + additional_columns="q", + ) @pytest.fixture() def u_additional_columns_not_present(self): f = LAMMPSDUMP_additional_columns top = LAMMPSdata_additional_columns - return mda.Universe(top, f, format='LAMMPSDUMP', - lammps_coordinate_convention="auto", - additional_columns=['q', 'w']) + return mda.Universe( + top, + f, + format="LAMMPSDUMP", + lammps_coordinate_convention="auto", + additional_columns=["q", "w"], + ) @pytest.fixture() def reference_positions(self): @@ -559,19 +653,19 @@ def reference_positions(self): data = {} # at timestep 500 - lo, hi = float(2.1427867124774069e-01), float(5.9857213287522608e+00) + lo, hi = float(2.1427867124774069e-01), float(5.9857213287522608e00) length1 = hi - lo # at timestep 1000 - lo, hi = float(-5.4458069063278991e-03), float(6.2054458069063330e+00) + lo, hi = float(-5.4458069063278991e-03), float(6.2054458069063330e00) length2 = hi - lo boxes = [ - np.array([6.2, 6.2, 6.2, 90., 90., 90.]), - np.array([length1, length1, length1, 90., 90., 90.]), - np.array([length2, length2, length2, 90., 90., 90.]), + np.array([6.2, 6.2, 6.2, 90.0, 90.0, 90.0]), + np.array([length1, length1, length1, 90.0, 90.0, 90.0]), + np.array([length2, length2, length2, 90.0, 90.0, 90.0]), ] - data['box'] = boxes + data["box"] = boxes box_mins = [ - np.array([0., 0., 0.]), + np.array([0.0, 0.0, 0.0]), np.array([0.21427867, 0.21427867, 0.21427867]), np.array([-0.00544581, -0.00544581, -0.00544581]), ] @@ -582,14 +676,14 @@ def reference_positions(self): atom1_pos1 = np.array([0.25, 0.25, 0.241936]) * boxes[0][:3] atom1_pos2 = np.array([0.278215, 0.12611, 0.322087]) * boxes[1][:3] atom1_pos3 = np.array([0.507123, 1.00424, 0.280972]) * boxes[2][:3] - data['atom1_pos'] = [atom1_pos1, atom1_pos2, atom1_pos3] + data["atom1_pos"] = [atom1_pos1, atom1_pos2, atom1_pos3] # data for atom id 13 # *is* sensitive to reordering of positions # normally appears 4th in traj data atom13_pos1 = np.array([0.25, 0.25, 0.741936]) * boxes[0][:3] atom13_pos2 = np.array([0.394618, 0.263115, 0.798295]) * boxes[1][:3] atom13_pos3 = np.array([0.332363, 0.30544, 0.641589]) * boxes[2][:3] - data['atom13_pos'] = [atom13_pos1, atom13_pos2, atom13_pos3] + data["atom13_pos"] = [atom13_pos1, atom13_pos2, atom13_pos3] return data @@ -600,106 +694,146 @@ def test_length(self, u): assert len(u.trajectory) == 3 for i, ts in enumerate(u.trajectory): assert ts.frame == i - assert ts.data['step'] == i * 500 + assert ts.data["step"] == i * 500 for i, ts in enumerate(u.trajectory): assert ts.frame == i - assert ts.data['step'] == i * 500 + assert ts.data["step"] == i * 500 def test_seeking(self, u, reference_positions): u.trajectory[1] - assert_allclose(u.dimensions, reference_positions['box'][1], - atol=1e-5) - pos = (reference_positions['atom1_pos'][1] - - reference_positions['mins'][1]) - assert_allclose(u.atoms[0].position, pos, - atol=1e-5) - pos = (reference_positions['atom13_pos'][1] - - reference_positions['mins'][1]) - assert_allclose(u.atoms[12].position, pos, - atol=1e-5) + assert_allclose(u.dimensions, reference_positions["box"][1], atol=1e-5) + pos = ( + reference_positions["atom1_pos"][1] + - reference_positions["mins"][1] + ) + assert_allclose(u.atoms[0].position, pos, atol=1e-5) + pos = ( + reference_positions["atom13_pos"][1] + - reference_positions["mins"][1] + ) + assert_allclose(u.atoms[12].position, pos, atol=1e-5) def test_boxsize(self, u, reference_positions): - for ts, box in zip(u.trajectory, - reference_positions['box']): + for ts, box in zip(u.trajectory, reference_positions["box"]): assert_allclose(ts.dimensions, box, atol=1e-5) def test_atom_reordering(self, u, reference_positions): atom1 = u.atoms[0] atom13 = u.atoms[12] - for ts, atom1_pos, atom13_pos, bmin in zip(u.trajectory, - reference_positions['atom1_pos'], - reference_positions['atom13_pos'], - reference_positions['mins']): - assert_allclose(atom1.position, atom1_pos-bmin, atol=1e-5) - assert_allclose(atom13.position, atom13_pos-bmin, atol=1e-5) - - @pytest.mark.parametrize("system, fields", [ - ('u_additional_columns_true', ['q', 'p']), - ('u_additional_columns_single', ['q']), - ('u_additional_columns_multiple', ['q', 'p']), - ]) + for ts, atom1_pos, atom13_pos, bmin in zip( + u.trajectory, + reference_positions["atom1_pos"], + reference_positions["atom13_pos"], + reference_positions["mins"], + ): + assert_allclose(atom1.position, atom1_pos - bmin, atol=1e-5) + assert_allclose(atom13.position, atom13_pos - bmin, atol=1e-5) + + @pytest.mark.parametrize( + "system, fields", + [ + ("u_additional_columns_true", ["q", "p"]), + ("u_additional_columns_single", ["q"]), + ("u_additional_columns_multiple", ["q", "p"]), + ], + ) def test_additional_columns(self, system, fields, request): u = request.getfixturevalue(system) for field in fields: data = u.trajectory[0].data[field] - assert_allclose(data, - getattr(RefLAMMPSDataAdditionalColumns, field)) - - @pytest.mark.parametrize("system", [ - ('u_additional_columns_wrong_format'), - ]) + assert_allclose( + data, getattr(RefLAMMPSDataAdditionalColumns, field) + ) + + @pytest.mark.parametrize( + "system", + [ + ("u_additional_columns_wrong_format"), + ], + ) def test_wrong_format_additional_colums(self, system, request): - with pytest.raises(ValueError, - match="Please provide an iterable containing"): + with pytest.raises( + ValueError, match="Please provide an iterable containing" + ): request.getfixturevalue(system) - @pytest.mark.parametrize("system", [ - ('u_additional_columns_not_present'), - ]) + @pytest.mark.parametrize( + "system", + [ + ("u_additional_columns_not_present"), + ], + ) def test_warning(self, system, request): with pytest.warns(match="Some of the additional"): request.getfixturevalue(system) -@pytest.mark.parametrize("convention", - ["unscaled", "unwrapped", "scaled_unwrapped"]) + +@pytest.mark.parametrize( + "convention", ["unscaled", "unwrapped", "scaled_unwrapped"] +) def test_open_absent_convention_fails(convention): with pytest.raises(ValueError, match="No coordinates following"): - mda.Universe(LAMMPSDUMP, format='LAMMPSDUMP', - lammps_coordinate_convention=convention) + mda.Universe( + LAMMPSDUMP, + format="LAMMPSDUMP", + lammps_coordinate_convention=convention, + ) def test_open_incorrect_convention_fails(): - with pytest.raises(ValueError, - match="is not a valid option"): - mda.Universe(LAMMPSDUMP, format='LAMMPSDUMP', - lammps_coordinate_convention="42") - - -@pytest.mark.parametrize("convention,result", - [("auto", "unscaled"), ("unscaled", "unscaled"), - ("scaled", "scaled"), ("unwrapped", "unwrapped"), - ("scaled_unwrapped", "scaled_unwrapped")]) + with pytest.raises(ValueError, match="is not a valid option"): + mda.Universe( + LAMMPSDUMP, format="LAMMPSDUMP", lammps_coordinate_convention="42" + ) + + +@pytest.mark.parametrize( + "convention,result", + [ + ("auto", "unscaled"), + ("unscaled", "unscaled"), + ("scaled", "scaled"), + ("unwrapped", "unwrapped"), + ("scaled_unwrapped", "scaled_unwrapped"), + ], +) def test_open_all_convention(convention, result): - u = mda.Universe(LAMMPSDUMP_allcoords, format='LAMMPSDUMP', - lammps_coordinate_convention=convention) - assert(u.trajectory.lammps_coordinate_convention == result) + u = mda.Universe( + LAMMPSDUMP_allcoords, + format="LAMMPSDUMP", + lammps_coordinate_convention=convention, + ) + assert u.trajectory.lammps_coordinate_convention == result def test_no_coordinate_info(): with pytest.raises(ValueError, match="No coordinate information detected"): - u = mda.Universe(LAMMPSDUMP_nocoords, format='LAMMPSDUMP', - lammps_coordinate_convention="auto") + u = mda.Universe( + LAMMPSDUMP_nocoords, + format="LAMMPSDUMP", + lammps_coordinate_convention="auto", + ) class TestCoordinateMatches(object): @pytest.fixture() def universes(self): - coordinate_conventions = ["auto", "unscaled", "scaled", "unwrapped", - "scaled_unwrapped"] - universes = {i: mda.Universe(LAMMPSDUMP_allcoords, format='LAMMPSDUMP', - lammps_coordinate_convention=i) - for i in coordinate_conventions} + coordinate_conventions = [ + "auto", + "unscaled", + "scaled", + "unwrapped", + "scaled_unwrapped", + ] + universes = { + i: mda.Universe( + LAMMPSDUMP_allcoords, + format="LAMMPSDUMP", + lammps_coordinate_convention=i, + ) + for i in coordinate_conventions + } return universes @pytest.fixture() @@ -710,21 +844,32 @@ def reference_unscaled_positions(self): atom340_pos1_unscaled = np.array([4.48355, 0.331422, 1.59231]) - bmin atom340_pos2_unscaled = np.array([4.41947, 35.4403, 2.25115]) - bmin atom340_pos3_unscaled = np.array([4.48989, 0.360633, 2.63623]) - bmin - return np.asarray([atom340_pos1_unscaled, atom340_pos2_unscaled, - atom340_pos3_unscaled]) + return np.asarray( + [ + atom340_pos1_unscaled, + atom340_pos2_unscaled, + atom340_pos3_unscaled, + ] + ) def test_unscaled_reference(self, universes, reference_unscaled_positions): atom_340 = universes["unscaled"].atoms[339] for i, ts_u in enumerate(universes["unscaled"].trajectory[0:3]): - assert_allclose(atom_340.position, - reference_unscaled_positions[i, :], atol=1e-5) + assert_allclose( + atom_340.position, + reference_unscaled_positions[i, :], + atol=1e-5, + ) def test_scaled_reference(self, universes, reference_unscaled_positions): # NOTE use of unscaled positions here due to S->R transform atom_340 = universes["scaled"].atoms[339] for i, ts_u in enumerate(universes["scaled"].trajectory[0:3]): - assert_allclose(atom_340.position, - reference_unscaled_positions[i, :], atol=1e-1) + assert_allclose( + atom_340.position, + reference_unscaled_positions[i, :], + atol=1e-1, + ) # NOTE this seems a bit inaccurate? @pytest.fixture() @@ -734,60 +879,85 @@ def reference_unwrapped_positions(self): atom340_pos1_unwrapped = [4.48355, 35.8378, 1.59231] atom340_pos2_unwrapped = [4.41947, 35.4403, 2.25115] atom340_pos3_unwrapped = [4.48989, 35.867, 2.63623] - return np.asarray([atom340_pos1_unwrapped, atom340_pos2_unwrapped, - atom340_pos3_unwrapped]) - - def test_unwrapped_scaled_reference(self, universes, - reference_unwrapped_positions): + return np.asarray( + [ + atom340_pos1_unwrapped, + atom340_pos2_unwrapped, + atom340_pos3_unwrapped, + ] + ) + + def test_unwrapped_scaled_reference( + self, universes, reference_unwrapped_positions + ): atom_340 = universes["unwrapped"].atoms[339] for i, ts_u in enumerate(universes["unwrapped"].trajectory[0:3]): - assert_allclose(atom_340.position, - reference_unwrapped_positions[i, :], atol=1e-5) - - def test_unwrapped_scaled_reference(self, universes, - reference_unwrapped_positions): + assert_allclose( + atom_340.position, + reference_unwrapped_positions[i, :], + atol=1e-5, + ) + + def test_unwrapped_scaled_reference( + self, universes, reference_unwrapped_positions + ): # NOTE use of unscaled positions here due to S->R transform atom_340 = universes["scaled_unwrapped"].atoms[339] for i, ts_u in enumerate( - universes["scaled_unwrapped"].trajectory[0:3]): - assert_allclose(atom_340.position, - reference_unwrapped_positions[i, :], atol=1e-1) + universes["scaled_unwrapped"].trajectory[0:3] + ): + assert_allclose( + atom_340.position, + reference_unwrapped_positions[i, :], + atol=1e-1, + ) # NOTE this seems a bit inaccurate? def test_scaled_unscaled_match(self, universes): - assert(len(universes["unscaled"].trajectory) - == len(universes["scaled"].trajectory)) - for ts_u, ts_s in zip(universes["unscaled"].trajectory, - universes["scaled"].trajectory): + assert len(universes["unscaled"].trajectory) == len( + universes["scaled"].trajectory + ) + for ts_u, ts_s in zip( + universes["unscaled"].trajectory, universes["scaled"].trajectory + ): assert_allclose(ts_u.positions, ts_s.positions, atol=1e-1) # NOTE this seems a bit inaccurate? def test_unwrapped_scaled_unwrapped_match(self, universes): - assert(len(universes["unwrapped"].trajectory) == - len(universes["scaled_unwrapped"].trajectory)) - for ts_u, ts_s in zip(universes["unwrapped"].trajectory, - universes["scaled_unwrapped"].trajectory): + assert len(universes["unwrapped"].trajectory) == len( + universes["scaled_unwrapped"].trajectory + ) + for ts_u, ts_s in zip( + universes["unwrapped"].trajectory, + universes["scaled_unwrapped"].trajectory, + ): assert_allclose(ts_u.positions, ts_s.positions, atol=1e-1) # NOTE this seems a bit inaccurate? def test_auto_is_unscaled_match(self, universes): - assert(len(universes["auto"].trajectory) == - len(universes["unscaled"].trajectory)) - for ts_a, ts_s in zip(universes["auto"].trajectory, - universes["unscaled"].trajectory): + assert len(universes["auto"].trajectory) == len( + universes["unscaled"].trajectory + ) + for ts_a, ts_s in zip( + universes["auto"].trajectory, universes["unscaled"].trajectory + ): assert_allclose(ts_a.positions, ts_s.positions, atol=1e-5) class TestLammpsTriclinic(object): @pytest.fixture() def u_dump(self): - return mda.Universe(LAMMPSDUMP_triclinic, format='LAMMPSDUMP', - lammps_coordinate_convention="auto") + return mda.Universe( + LAMMPSDUMP_triclinic, + format="LAMMPSDUMP", + lammps_coordinate_convention="auto", + ) @pytest.fixture() def u_data(self): - return mda.Universe(LAMMPSdata_triclinic, format='data', - atom_style='id type x y z') + return mda.Universe( + LAMMPSdata_triclinic, format="data", atom_style="id type x y z" + ) @pytest.fixture() def reference_box(self): @@ -815,7 +985,8 @@ def test_box(self, u_dump, u_data, reference_box): assert_allclose(ts.dimensions, reference_box, rtol=1e-5, atol=0) for ts in u_dump.trajectory: - assert_allclose(ts.dimensions, u_data.dimensions, - rtol=1e-5, atol=0) + assert_allclose( + ts.dimensions, u_data.dimensions, rtol=1e-5, atol=0 + ) assert_allclose(u_data.dimensions, reference_box, rtol=1e-5, atol=0) diff --git a/testsuite/MDAnalysisTests/coordinates/test_memory.py b/testsuite/MDAnalysisTests/coordinates/test_memory.py index 223345de155..5d020d43d2e 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_memory.py +++ b/testsuite/MDAnalysisTests/coordinates/test_memory.py @@ -26,8 +26,10 @@ import pytest from MDAnalysis.coordinates.memory import MemoryReader from MDAnalysisTests.datafiles import DCD, PSF -from MDAnalysisTests.coordinates.base import (BaseReference, - MultiframeReaderTest) +from MDAnalysisTests.coordinates.base import ( + BaseReference, + MultiframeReaderTest, +) from MDAnalysis.coordinates.memory import Timestep from numpy.testing import assert_equal, assert_almost_equal @@ -51,27 +53,29 @@ def __init__(self): self.first_frame = Timestep(self.n_atoms) self.first_frame.positions = np.array(self.universe.trajectory[0]) self.first_frame.frame = 0 - self.first_frame.time = self.first_frame.frame*self.dt + self.first_frame.time = self.first_frame.frame * self.dt self.second_frame = Timestep(self.n_atoms) self.second_frame.positions = np.array(self.universe.trajectory[1]) self.second_frame.frame = 1 - self.second_frame.time = self.second_frame.frame*self.dt + self.second_frame.time = self.second_frame.frame * self.dt self.last_frame = Timestep(self.n_atoms) - self.last_frame.positions = \ - np.array(self.universe.trajectory[self.n_frames - 1]) + self.last_frame.positions = np.array( + self.universe.trajectory[self.n_frames - 1] + ) self.last_frame.frame = self.n_frames - 1 - self.last_frame.time = self.last_frame.frame*self.dt + self.last_frame.time = self.last_frame.frame * self.dt self.jump_to_frame = self.first_frame.copy() self.jump_to_frame.positions = np.array(self.universe.trajectory[3]) self.jump_to_frame.frame = 3 - self.jump_to_frame.time = self.jump_to_frame.frame*self.dt + self.jump_to_frame.time = self.jump_to_frame.frame * self.dt def reader(self, trajectory): - return mda.Universe(self.topology, - trajectory, in_memory=True).trajectory + return mda.Universe( + self.topology, trajectory, in_memory=True + ).trajectory def iter_ts(self, i): ts = self.universe.trajectory[i] @@ -82,7 +86,7 @@ def iter_ts(self, i): class TestMemoryReader(MultiframeReaderTest): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ref(): return MemoryReference() @@ -96,14 +100,18 @@ def test_filename_array(self): # filename attribute of MemoryReader should be None when generated from an array universe = mda.Universe(PSF, DCD) coordinates = universe.trajectory.timeseries(universe.atoms) - universe2 = mda.Universe(PSF, coordinates, format=MemoryReader, order='afc') + universe2 = mda.Universe( + PSF, coordinates, format=MemoryReader, order="afc" + ) assert universe2.trajectory.filename is None def test_default_memory_layout(self): universe1 = mda.Universe(PSF, DCD, in_memory=True) - universe2 = mda.Universe(PSF, DCD, in_memory=True, order='fac') - assert_equal(universe1.trajectory.get_array().shape, - universe2.trajectory.get_array().shape) + universe2 = mda.Universe(PSF, DCD, in_memory=True, order="fac") + assert_equal( + universe1.trajectory.get_array().shape, + universe2.trajectory.get_array().shape, + ) def test_iteration(self, ref, reader): frames = 0 @@ -111,35 +119,36 @@ def test_iteration(self, ref, reader): frames += 1 assert frames == ref.n_frames - def test_extract_array_afc(self,reader): - assert_equal(reader.timeseries(order='afc').shape, (3341, 98, 3)) + def test_extract_array_afc(self, reader): + assert_equal(reader.timeseries(order="afc").shape, (3341, 98, 3)) def test_extract_array_afc(self, reader): - assert_equal(reader.timeseries(order='afc').shape, (3341, 98, 3)) + assert_equal(reader.timeseries(order="afc").shape, (3341, 98, 3)) def test_extract_array_fac(self, reader): - assert_equal(reader.timeseries(order='fac').shape, (98, 3341, 3)) + assert_equal(reader.timeseries(order="fac").shape, (98, 3341, 3)) def test_extract_array_cfa(self, reader): - assert_equal(reader.timeseries(order='cfa').shape, (3, 98, 3341)) + assert_equal(reader.timeseries(order="cfa").shape, (3, 98, 3341)) def test_extract_array_acf(self, reader): - assert_equal(reader.timeseries(order='acf').shape, (3341, 3, 98)) + assert_equal(reader.timeseries(order="acf").shape, (3341, 3, 98)) def test_extract_array_fca(self, reader): - assert_equal(reader.timeseries(order='fca').shape, (98, 3, 3341)) + assert_equal(reader.timeseries(order="fca").shape, (98, 3, 3341)) def test_extract_array_caf(self, reader): - assert_equal(reader.timeseries(order='caf').shape, (3, 3341, 98)) + assert_equal(reader.timeseries(order="caf").shape, (3, 3341, 98)) def test_timeseries_skip1(self, ref, reader): - assert_equal(reader.timeseries(ref.universe.atoms).shape, - (3341, 98, 3)) + assert_equal( + reader.timeseries(ref.universe.atoms).shape, (3341, 98, 3) + ) def test_timeseries_skip10(self, reader): # Check that timeseries skip works similar to numpy slicing array1 = reader.timeseries(step=10) - array2 = reader.timeseries()[:,::10,:] + array2 = reader.timeseries()[:, ::10, :] assert_equal(array1, array2) def test_timeseries_view(self, reader): @@ -150,8 +159,10 @@ def test_timeseries_subarray_view(self, reader): # timeseries() is expected to provide a view of the underlying array # also in the case where we slice the array using the start, stop and # step options. - assert reader.timeseries(start=5,stop=15,step=2,order='fac').base is\ - reader.get_array() + assert ( + reader.timeseries(start=5, stop=15, step=2, order="fac").base + is reader.get_array() + ) def test_timeseries_view_from_universe_atoms(self, ref, reader): # timeseries() is expected to provide a view of the underlying array @@ -163,9 +174,9 @@ def test_timeseries_view_from_select_all(self, ref, reader): # timeseries() is expected to provide a view of the underlying array # also in the special case when using "all" in selections. selection = ref.universe.select_atoms("all") - assert_equal(reader.timeseries( - asel=selection).base is reader.get_array(), - True) + assert_equal( + reader.timeseries(asel=selection).base is reader.get_array(), True + ) def test_timeseries_noview(self, ref, reader): # timeseries() is expected NOT to provide a view of the underlying array @@ -186,9 +197,15 @@ def test_get_writer_2(self): def test_float32(self, ref): # Check that we get float32 positions even when initializing with float64 - coordinates = np.random.uniform(size=(100, ref.universe.atoms.n_atoms, 3)).cumsum(0) - universe = mda.Universe(ref.universe.filename, coordinates, format=MemoryReader) - assert_equal(universe.trajectory.get_array().dtype, np.dtype('float32')) + coordinates = np.random.uniform( + size=(100, ref.universe.atoms.n_atoms, 3) + ).cumsum(0) + universe = mda.Universe( + ref.universe.filename, coordinates, format=MemoryReader + ) + assert_equal( + universe.trajectory.get_array().dtype, np.dtype("float32") + ) def test_position_assignation(self, reader): # When coordinates are assigned to a timestep, is the change persistent? @@ -198,19 +215,20 @@ def test_position_assignation(self, reader): assert_almost_equal(reader.ts.positions, new_positions) def test_timeseries_warns_deprecation(self, reader): - with pytest.warns(DeprecationWarning, match="MemoryReader.timeseries " - "inclusive"): + with pytest.warns( + DeprecationWarning, match="MemoryReader.timeseries " "inclusive" + ): reader.timeseries(start=0, stop=3, step=1) def test_timeseries_asel_warns_deprecation(self, ref, reader): selection = ref.universe.atoms with pytest.warns(DeprecationWarning, match="asel argument to"): reader.timeseries(asel=selection) - + def test_timeseries_atomgroup(self, ref, reader): selection = ref.universe.atoms reader.timeseries(atomgroup=selection) - + def test_timeseries_atomgroup_asel_mutex(self, ref, reader): selection = ref.universe.atoms with pytest.raises(ValueError, match="Cannot provide both"): @@ -219,27 +237,27 @@ def test_timeseries_atomgroup_asel_mutex(self, ref, reader): class TestMemoryReaderVelsForces(object): @staticmethod - @pytest.fixture(params=['2d', '3d']) + @pytest.fixture(params=["2d", "3d"]) def ref_pos(request): - if request.param == '2d': + if request.param == "2d": return np.arange(30).reshape(10, 3) - elif request.param == '3d': + elif request.param == "3d": return np.arange(30).reshape(1, 10, 3) @staticmethod - @pytest.fixture(params=['2d', '3d']) + @pytest.fixture(params=["2d", "3d"]) def ref_vels(request): - if request.param == '2d': + if request.param == "2d": return np.arange(30).reshape(10, 3) + 100 - elif request.param == '3d': + elif request.param == "3d": return np.arange(30).reshape(1, 10, 3) + 100 @staticmethod - @pytest.fixture(params=['2d', '3d']) + @pytest.fixture(params=["2d", "3d"]) def ref_forces(request): - if request.param == '2d': + if request.param == "2d": return np.arange(30).reshape(10, 3) + 1000 - elif request.param == '3d': + elif request.param == "3d": return np.arange(30).reshape(1, 10, 3) + 1000 @staticmethod @@ -250,31 +268,27 @@ def assert_equal_dims(arr1, arr2): assert_equal(arr1, arr2) def test_velocities(self, ref_pos, ref_vels): - mr = MemoryReader(ref_pos, - velocities=ref_vels) + mr = MemoryReader(ref_pos, velocities=ref_vels) assert mr.ts.has_velocities self.assert_equal_dims(mr.ts.velocities, ref_vels) assert not mr.ts.has_forces def test_forces(self, ref_pos, ref_forces): - mr = MemoryReader(ref_pos, - forces=ref_forces) + mr = MemoryReader(ref_pos, forces=ref_forces) assert not mr.ts.has_velocities assert mr.ts.has_forces self.assert_equal_dims(mr.ts.forces, ref_forces) def test_both(self, ref_pos, ref_vels, ref_forces): - mr = MemoryReader(ref_pos, - velocities=ref_vels, - forces=ref_forces) + mr = MemoryReader(ref_pos, velocities=ref_vels, forces=ref_forces) assert mr.ts.has_velocities self.assert_equal_dims(mr.ts.velocities, ref_vels) assert mr.ts.has_forces self.assert_equal_dims(mr.ts.forces, ref_forces) - @pytest.mark.parametrize('param', ['velocities', 'forces']) + @pytest.mark.parametrize("param", ["velocities", "forces"]) def test_wrongshape(self, ref_pos, param): with pytest.raises(ValueError): mr = MemoryReader(ref_pos, **{param: np.zeros((3, 2, 1))}) @@ -328,17 +342,23 @@ class TestMemoryReaderModifications(object): @pytest.fixture() def mr_reader(self): pos = np.arange(self.n_frames * self.n_atoms * 3).reshape( - self.n_frames, self.n_atoms, 3) - vel = np.arange(self.n_frames * self.n_atoms * 3).reshape( - self.n_frames, self.n_atoms, 3) + 200 - frc = np.arange(self.n_frames * self.n_atoms * 3).reshape( - self.n_frames, self.n_atoms, 3) + 400 + self.n_frames, self.n_atoms, 3 + ) + vel = ( + np.arange(self.n_frames * self.n_atoms * 3).reshape( + self.n_frames, self.n_atoms, 3 + ) + + 200 + ) + frc = ( + np.arange(self.n_frames * self.n_atoms * 3).reshape( + self.n_frames, self.n_atoms, 3 + ) + + 400 + ) box = np.arange(self.n_frames * 6).reshape(self.n_frames, 6) + 600 - return MemoryReader(pos, - velocities=vel, - forces=frc, - dimensions=box) + return MemoryReader(pos, velocities=vel, forces=frc, dimensions=box) @pytest.fixture() def mr_universe(self, mr_reader): @@ -347,7 +367,9 @@ def mr_universe(self, mr_reader): return u - @pytest.mark.parametrize('attr', ['positions', 'velocities', 'forces', 'dimensions']) + @pytest.mark.parametrize( + "attr", ["positions", "velocities", "forces", "dimensions"] + ) def test_copying(self, mr_reader, attr): mr2 = mr_reader.copy() # update the attribute @@ -365,7 +387,9 @@ def test_copying(self, mr_reader, attr): # check our old change is still there assert_almost_equal(getattr(ts, attr), 7) - @pytest.mark.parametrize('attr', ['positions', 'velocities', 'forces', 'dimensions']) + @pytest.mark.parametrize( + "attr", ["positions", "velocities", "forces", "dimensions"] + ) def test_attr_set(self, mr_universe, attr): # same as above, but via a Universe/AtomGroup u = mr_universe @@ -384,8 +408,7 @@ def test_attr_set(self, mr_universe, attr): assert u.atoms.forces.shape == (self.n_atoms, 3) assert u.atoms.dimensions.shape == (6,) - @pytest.mark.parametrize('attr', ['velocities', 'forces', 'dimensions']) + @pytest.mark.parametrize("attr", ["velocities", "forces", "dimensions"]) def test_non_numpy_arr(self, attr): with pytest.raises(TypeError): - mr = MemoryReader(np.zeros((10, 30, 3)), - **{attr: 'not an array'}) + mr = MemoryReader(np.zeros((10, 30, 3)), **{attr: "not an array"}) diff --git a/testsuite/MDAnalysisTests/coordinates/test_mmtf.py b/testsuite/MDAnalysisTests/coordinates/test_mmtf.py index a8a3b6037a0..8fa7d6a767f 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_mmtf.py +++ b/testsuite/MDAnalysisTests/coordinates/test_mmtf.py @@ -32,7 +32,7 @@ class TestMMTFReader(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def r(self): return MMTFReader(MMTF) @@ -40,12 +40,12 @@ def test_read_frame_size(self, r): assert r.ts.n_atoms == 512 def test_read_positions(self, r): - assert_almost_equal(r.ts.positions[0], - np.array([-0.798, 12.632, 23.231]), - decimal=4) - assert_almost_equal(r.ts.positions[-1], - np.array([10.677, 15.517, 11.1]), - decimal=4) + assert_almost_equal( + r.ts.positions[0], np.array([-0.798, 12.632, 23.231]), decimal=4 + ) + assert_almost_equal( + r.ts.positions[-1], np.array([10.677, 15.517, 11.1]), decimal=4 + ) def test_velocities(self, r): assert not r.ts.has_velocities @@ -59,7 +59,7 @@ def test_len(self, r): class TestMMTFReaderGZ(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def r(self): return MMTFReader(MMTF_gz) @@ -67,12 +67,12 @@ def test_read_frame_size(self, r): assert r.ts.n_atoms == 1140 def test_read_positions(self, r): - assert_almost_equal(r.ts.positions[0], - np.array([38.428, 16.440, 28.841]), - decimal=4) - assert_almost_equal(r.ts.positions[-1], - np.array([36.684, 27.024, 20.468]), - decimal=4) + assert_almost_equal( + r.ts.positions[0], np.array([38.428, 16.440, 28.841]), decimal=4 + ) + assert_almost_equal( + r.ts.positions[-1], np.array([36.684, 27.024, 20.468]), decimal=4 + ) def test_velocities(self, r): assert not r.ts.has_velocities @@ -84,6 +84,7 @@ def test_len(self, r): # should be single frame assert len(r) == 1 + def test_dimensionless(): r = MMTFReader(MMTF_skinny2) diff --git a/testsuite/MDAnalysisTests/coordinates/test_mol2.py b/testsuite/MDAnalysisTests/coordinates/test_mol2.py index 450a972eca5..494b2ace2af 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_mol2.py +++ b/testsuite/MDAnalysisTests/coordinates/test_mol2.py @@ -24,14 +24,21 @@ import os from numpy.testing import ( - assert_equal, assert_array_equal, - assert_array_almost_equal, TestCase, - assert_almost_equal + assert_equal, + assert_array_equal, + assert_array_almost_equal, + TestCase, + assert_almost_equal, ) from MDAnalysisTests.datafiles import ( - mol2_molecules, mol2_molecule, mol2_broken_molecule, - mol2_zinc, mol2_comments_header, mol2_ligand, mol2_sodium_ion + mol2_molecules, + mol2_molecule, + mol2_broken_molecule, + mol2_zinc, + mol2_comments_header, + mol2_ligand, + mol2_sodium_ion, ) from MDAnalysis import Universe import MDAnalysis as mda @@ -40,7 +47,7 @@ class TestMol2(object): def setup_method(self): - self.outfile = 'test.mol2' + self.outfile = "test.mol2" def test_read(self): u = Universe(mol2_molecules) @@ -48,7 +55,9 @@ def test_read(self): assert_equal(u.trajectory.n_frames, 200) u.trajectory[199] - assert_array_almost_equal(u.atoms.positions[0], [1.7240, 11.2730, 14.1200]) + assert_array_almost_equal( + u.atoms.positions[0], [1.7240, 11.2730, 14.1200] + ) def test_read_statusbit(self): u = Universe(mol2_ligand) @@ -94,9 +103,9 @@ def test_broken_molecule(self): # This doesn't work with 2.6 # Checks the text of the error message, so it low priority - #with self.assertRaises(Exception) as context: + # with self.assertRaises(Exception) as context: # u = Universe(mol2_broken_molecule) - #self.assertEqual("The mol2 block (BrokenMolecule.mol2:0) has no atoms" in context.exception.message, + # self.assertEqual("The mol2 block (BrokenMolecule.mol2:0) has no atoms" in context.exception.message, # True) def test_comments_header(self): @@ -104,7 +113,9 @@ def test_comments_header(self): assert_equal(len(u.atoms), 9) assert_equal(u.trajectory.n_frames, 2) u.trajectory[1] - assert_array_almost_equal(u.atoms.positions[2], [-12.2710, -1.9540, -16.0480]) + assert_array_almost_equal( + u.atoms.positions[2], [-12.2710, -1.9540, -16.0480] + ) def test_no_bonds(self, tmpdir): # Issue #3057 @@ -112,7 +123,7 @@ def test_no_bonds(self, tmpdir): ag = u.atoms assert not hasattr(ag, "bonds") with tmpdir.as_cwd(): - outfile = 'test.mol2' + outfile = "test.mol2" ag.write(outfile) u2 = Universe(outfile) assert not hasattr(u2.atoms, "bonds") @@ -152,17 +163,21 @@ def test_slice_traj(self): def test_reverse_traj(self): frames = [ts.frame for ts in self.traj[20:5:-1]] - assert_equal(frames, list(range(20, 5, -1)), - "reversing traj [20:5:-1]") + assert_equal( + frames, list(range(20, 5, -1)), "reversing traj [20:5:-1]" + ) def test_n_frames(self): - assert_equal(self.universe.trajectory.n_frames, 200, "wrong number of frames in traj") + assert_equal( + self.universe.trajectory.n_frames, + 200, + "wrong number of frames in traj", + ) class TestMOL2NoSubstructure(object): - """MOL2 file without substructure + """MOL2 file without substructure""" - """ n_atoms = 45 def test_load(self): @@ -175,7 +190,7 @@ def test_universe(self): def test_write_nostructure(self, tmpdir): with tmpdir.as_cwd(): - outfile = 'test.mol2' + outfile = "test.mol2" u = mda.Universe(mol2_zinc) with mda.Writer(outfile) as W: @@ -188,23 +203,24 @@ def test_write_nostructure(self, tmpdir): def test_mol2_write_NIE(tmpdir): with tmpdir.as_cwd(): - outfile = os.path.join('test.mol2') + outfile = os.path.join("test.mol2") u = make_Universe(trajectory=True) with pytest.raises(NotImplementedError): u.atoms.write(outfile) + def test_mol2_multi_write(tmpdir): # see: gh-2678 with tmpdir.as_cwd(): u = mda.Universe(mol2_molecules) - u.atoms[:4].write('group1.mol2') - u.atoms[:4].write('group1.mol2') + u.atoms[:4].write("group1.mol2") + u.atoms[:4].write("group1.mol2") def test_mol2_universe_write(tmpdir): # see Issue 2717 with tmpdir.as_cwd(): - outfile = 'test.mol2' + outfile = "test.mol2" u = mda.Universe(mol2_comments_header) diff --git a/testsuite/MDAnalysisTests/coordinates/test_namdbin.py b/testsuite/MDAnalysisTests/coordinates/test_namdbin.py index 9cbce77f2ee..cb07b28c1ba 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_namdbin.py +++ b/testsuite/MDAnalysisTests/coordinates/test_namdbin.py @@ -24,19 +24,20 @@ import os import pytest -from numpy.testing import (assert_allclose, - assert_equal, - assert_almost_equal) +from numpy.testing import assert_allclose, assert_equal, assert_almost_equal import MDAnalysis as mda from MDAnalysisTests.datafiles import NAMDBIN, PDB_small -from MDAnalysisTests.coordinates.base import (_SingleFrameReader, - BaseReference, - BaseWriterTest) +from MDAnalysisTests.coordinates.base import ( + _SingleFrameReader, + BaseReference, + BaseWriterTest, +) class TestNAMDBINReader(_SingleFrameReader): """Test reading namd binary coordinate file""" + __test__ = True def setUp(self): @@ -52,9 +53,8 @@ def test_parse_n_atoms(self): def test_get_writer_from_reader(self): universe = mda.Universe(PDB_small, NAMDBIN) - writer = universe.trajectory.Writer('NAMDBIN-test') - assert isinstance(writer, - mda.coordinates.NAMDBIN.NAMDBINWriter) + writer = universe.trajectory.Writer("NAMDBIN-test") + assert isinstance(writer, mda.coordinates.NAMDBIN.NAMDBINWriter) class NAMDBINReference(BaseReference): @@ -64,7 +64,7 @@ def __init__(self): self.topology = PDB_small self.reader = mda.coordinates.NAMDBIN.NAMDBINReader self.writer = mda.coordinates.NAMDBIN.NAMDBINWriter - self.ext = 'coor' + self.ext = "coor" self.volume = 0 self.dimensions = np.zeros(6) self.container_format = True @@ -72,6 +72,7 @@ def __init__(self): class NAMDBINWriter(BaseWriterTest): __test__ = True + @staticmethod @pytest.fixture() def ref(): diff --git a/testsuite/MDAnalysisTests/coordinates/test_netcdf.py b/testsuite/MDAnalysisTests/coordinates/test_netcdf.py index dc3456addfa..1ebad11665c 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_netcdf.py +++ b/testsuite/MDAnalysisTests/coordinates/test_netcdf.py @@ -27,18 +27,23 @@ from scipy.io import netcdf_file import pytest -from numpy.testing import ( - assert_equal, - assert_almost_equal -) +from numpy.testing import assert_equal, assert_almost_equal from MDAnalysis.coordinates.TRJ import NCDFReader, NCDFWriter -from MDAnalysisTests.datafiles import (PFncdf_Top, PFncdf_Trj, - GRO, TRR, XYZ_mini, - PRM_NCBOX, TRJ_NCBOX, DLP_CONFIG, - CPPTRAJ_TRAJ_TOP, CPPTRAJ_TRAJ) +from MDAnalysisTests.datafiles import ( + PFncdf_Top, + PFncdf_Trj, + GRO, + TRR, + XYZ_mini, + PRM_NCBOX, + TRJ_NCBOX, + DLP_CONFIG, + CPPTRAJ_TRAJ_TOP, + CPPTRAJ_TRAJ, +) from MDAnalysisTests.coordinates.test_trj import _TRJReaderTest -from MDAnalysisTests.coordinates.reference import (RefVGV, RefTZ2) +from MDAnalysisTests.coordinates.reference import RefVGV, RefTZ2 from MDAnalysisTests import make_Universe from MDAnalysisTests.util import block_import @@ -52,14 +57,16 @@ def universe(self): def test_slice_iteration(self, universe): frames = [ts.frame for ts in universe.trajectory[4:-2:4]] - assert_equal(frames, - np.arange(universe.trajectory.n_frames)[4:-2:4], - err_msg="slicing did not produce the expected frames") + assert_equal( + frames, + np.arange(universe.trajectory.n_frames)[4:-2:4], + err_msg="slicing did not produce the expected frames", + ) def test_metadata(self, universe): data = universe.trajectory.trjfile - assert_equal(data.Conventions.decode('utf-8'), 'AMBER') - assert_equal(data.ConventionVersion.decode('utf-8'), '1.0') + assert_equal(data.Conventions.decode("utf-8"), "AMBER") + assert_equal(data.ConventionVersion.decode("utf-8"), "1.0") def test_dt(self, universe): ref = 0.0 @@ -67,15 +74,16 @@ def test_dt(self, universe): assert_almost_equal(ref, universe.trajectory.ts.dt, self.prec) def test_get_writer(self, universe): - with universe.trajectory.Writer('out.ncdf') as w: + with universe.trajectory.Writer("out.ncdf") as w: assert w.n_atoms == len(universe.atoms) - assert w.remarks.startswith('AMBER NetCDF format') + assert w.remarks.startswith("AMBER NetCDF format") def test_get_writer_custom_n_atoms(self, universe): - with universe.trajectory.Writer('out.ncdf', n_atoms=42, - remarks='Hi!') as w: + with universe.trajectory.Writer( + "out.ncdf", n_atoms=42, remarks="Hi!" + ) as w: assert w.n_atoms == 42 - assert w.remarks == 'Hi!' + assert w.remarks == "Hi!" def test_wrong_natoms(self): with pytest.raises(ValueError): @@ -94,6 +102,7 @@ def test_mmap_kwarg(self, universe): # Ugly way to create the tests for mmap + class _NCDFReaderTest_mmap_None(_NCDFReaderTest): @pytest.fixture() def universe(self): @@ -144,46 +153,63 @@ class TestNCDFReader2(object): Contributed by Albert Solernou """ + prec = 3 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(self): return mda.Universe(PFncdf_Top, PFncdf_Trj) def test_positions_1(self, u): """Check positions on first frame""" u.trajectory[0] - ref_1 = np.array([[-0.11980818, 18.70524979, 11.6477766 - ], [-0.44717646, 18.61727142, 12.59919548], - [-0.60952115, 19.47885513, 11.22137547]], - dtype=np.float32) + ref_1 = np.array( + [ + [-0.11980818, 18.70524979, 11.6477766], + [-0.44717646, 18.61727142, 12.59919548], + [-0.60952115, 19.47885513, 11.22137547], + ], + dtype=np.float32, + ) assert_almost_equal(ref_1, u.atoms.positions[:3], self.prec) def test_positions_2(self, u): """Check positions on second frame""" u.trajectory[1] - ref_2 = np.array([[-0.13042036, 18.6671524, 11.69647026 - ], [-0.46643803, 18.60186768, 12.646698], - [-0.46567637, 19.49173927, 11.21922874]], - dtype=np.float32) + ref_2 = np.array( + [ + [-0.13042036, 18.6671524, 11.69647026], + [-0.46643803, 18.60186768, 12.646698], + [-0.46567637, 19.49173927, 11.21922874], + ], + dtype=np.float32, + ) assert_almost_equal(ref_2, u.atoms.positions[:3], self.prec) def test_forces_1(self, u): """Check forces on first frame""" u.trajectory[0] - ref_1 = np.array([[49.23017883, -97.05565643, -86.09863281 - ], [2.97547197, 29.84169388, 11.12069607], - [-15.93093777, 14.43616867, 30.25889015]], - dtype=np.float32) + ref_1 = np.array( + [ + [49.23017883, -97.05565643, -86.09863281], + [2.97547197, 29.84169388, 11.12069607], + [-15.93093777, 14.43616867, 30.25889015], + ], + dtype=np.float32, + ) assert_almost_equal(ref_1, u.atoms.forces[:3], self.prec) def test_forces_2(self, u): """Check forces on second frame""" u.trajectory[1] - ref_2 = np.array([[116.39096832, -145.44448853, -151.3155365 - ], [-18.90058327, 27.20145798, 1.95245135], - [-31.08556366, 14.95863628, 41.10367966]], - dtype=np.float32) + ref_2 = np.array( + [ + [116.39096832, -145.44448853, -151.3155365], + [-18.90058327, 27.20145798, 1.95245135], + [-31.08556366, 14.95863628, 41.10367966], + ], + dtype=np.float32, + ) assert_almost_equal(ref_2, u.atoms.forces[:3], self.prec) def test_time_1(self, u): @@ -211,80 +237,113 @@ class TestNCDFReader3(object): Added to address Issue #2323 """ + prec = 3 # Expected coordinates as stored in Angstrom units - coord_refs = np.array([ - [[15.249873, 12.578178, 15.191731], - [14.925511, 13.58888, 14.944009], - [15.285703, 14.3409605, 15.645962]], - [[14.799454, 15.214347, 14.714555], - [15.001984, 15.870884, 13.868363], - [16.03358, 16.183628, 14.02995]] - ], dtype=np.float32) + coord_refs = np.array( + [ + [ + [15.249873, 12.578178, 15.191731], + [14.925511, 13.58888, 14.944009], + [15.285703, 14.3409605, 15.645962], + ], + [ + [14.799454, 15.214347, 14.714555], + [15.001984, 15.870884, 13.868363], + [16.03358, 16.183628, 14.02995], + ], + ], + dtype=np.float32, + ) # Expected forces as stored in kcal/(mol*Angstrom) - frc_refs = np.array([ - [[8.583388, 1.8023694, -15.0033455], - [-21.594835, 39.09166, 6.567963], - [4.363016, -12.135163, 4.4775457]], - [[-10.106646, -7.870829, -10.385734], - [7.23599, -12.366022, -9.106191], - [-4.637955, 11.597565, -6.463743]] - ], dtype=np.float32) + frc_refs = np.array( + [ + [ + [8.583388, 1.8023694, -15.0033455], + [-21.594835, 39.09166, 6.567963], + [4.363016, -12.135163, 4.4775457], + ], + [ + [-10.106646, -7.870829, -10.385734], + [7.23599, -12.366022, -9.106191], + [-4.637955, 11.597565, -6.463743], + ], + ], + dtype=np.float32, + ) # Expected velocities as stored in Angstrom per AKMA time unit # These are usually associated with a scale_factor of 20.455 - vel_refs = np.array([ - [[-0.5301689, -0.16311595, -0.31390688], - [0.00188578, 0.02513031, -0.2687525], - [0.84072256, 0.09402391, -0.7457009]], - [[-1.7773226, 1.2307, 0.50276583], - [-0.13532305, 0.1355039, -0.05567304], - [-0.6182481, 1.6396415, 0.46686798]] - ], dtype=np.float32) + vel_refs = np.array( + [ + [ + [-0.5301689, -0.16311595, -0.31390688], + [0.00188578, 0.02513031, -0.2687525], + [0.84072256, 0.09402391, -0.7457009], + ], + [ + [-1.7773226, 1.2307, 0.50276583], + [-0.13532305, 0.1355039, -0.05567304], + [-0.6182481, 1.6396415, 0.46686798], + ], + ], + dtype=np.float32, + ) # Expected box values as stored in cell_lengths ([:3]) of Angstrom # and cell_angles ([:3]) of degree - box_refs = np.array([ - [28.81876287, 28.27875261, 27.72616397, 90., 90., 90.], - [27.06266081, 26.55555665, 26.03664058, 90., 90., 90.] - ], dtype=np.float32) - - @pytest.fixture(scope='class') + box_refs = np.array( + [ + [28.81876287, 28.27875261, 27.72616397, 90.0, 90.0, 90.0], + [27.06266081, 26.55555665, 26.03664058, 90.0, 90.0, 90.0], + ], + dtype=np.float32, + ) + + @pytest.fixture(scope="class") def universe(self): return mda.Universe(PRM_NCBOX, TRJ_NCBOX) - @pytest.mark.parametrize('index,expected', ((0, 0), (8, 1))) + @pytest.mark.parametrize("index,expected", ((0, 0), (8, 1))) def test_positions(self, universe, index, expected): universe.trajectory[index] - assert_almost_equal(self.coord_refs[expected], - universe.atoms.positions[:3], self.prec) + assert_almost_equal( + self.coord_refs[expected], universe.atoms.positions[:3], self.prec + ) - @pytest.mark.parametrize('index,expected', ((0, 0), (8, 1))) + @pytest.mark.parametrize("index,expected", ((0, 0), (8, 1))) def test_forces(self, universe, index, expected): """Here we multiply the forces by 4.184 to convert from kcal to kj in order to verify that MDA has correctly read and converted the units from those stored in the NetCDF file. """ universe.trajectory[index] - assert_almost_equal(self.frc_refs[expected] * 4.184, - universe.atoms.forces[:3], self.prec) + assert_almost_equal( + self.frc_refs[expected] * 4.184, + universe.atoms.forces[:3], + self.prec, + ) - @pytest.mark.parametrize('index,expected', ((0, 0), (8, 1))) + @pytest.mark.parametrize("index,expected", ((0, 0), (8, 1))) def test_velocities(self, universe, index, expected): """Here we multiply the velocities by 20.455 to match the value of `scale_factor` which has been declared in the NetCDF file, which should change the values from Angstrom/AKMA time unit to Angstrom/ps. """ universe.trajectory[index] - assert_almost_equal(self.vel_refs[expected] * 20.455, - universe.atoms.velocities[:3], self.prec) + assert_almost_equal( + self.vel_refs[expected] * 20.455, + universe.atoms.velocities[:3], + self.prec, + ) - @pytest.mark.parametrize('index,expected', ((0, 1.0), (8, 9.0))) + @pytest.mark.parametrize("index,expected", ((0, 1.0), (8, 9.0))) def test_time(self, universe, index, expected): - assert_almost_equal(expected, universe.trajectory[index].time, - self.prec) + assert_almost_equal( + expected, universe.trajectory[index].time, self.prec + ) def test_nframes(self, universe): assert_equal(10, universe.trajectory.n_frames) @@ -294,7 +353,7 @@ def test_dt(self, universe): assert_almost_equal(ref, universe.trajectory.dt, self.prec) assert_almost_equal(ref, universe.trajectory.ts.dt, self.prec) - @pytest.mark.parametrize('index,expected', ((0, 0), (8, 1))) + @pytest.mark.parametrize("index,expected", ((0, 0), (8, 1))) def test_box(self, universe, index, expected): universe.trajectory[index] assert_almost_equal(self.box_refs[expected], universe.dimensions) @@ -302,12 +361,12 @@ def test_box(self, universe, index, expected): class TestNCDFReader4(object): """NCDF Trajectory exported by cpptaj, without `time` variable.""" + prec = 3 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(self): - return mda.Universe(CPPTRAJ_TRAJ_TOP, - [CPPTRAJ_TRAJ, CPPTRAJ_TRAJ]) + return mda.Universe(CPPTRAJ_TRAJ_TOP, [CPPTRAJ_TRAJ, CPPTRAJ_TRAJ]) def test_chain_times(self, u): """Check times entries for a chain of trajectories without @@ -322,8 +381,10 @@ def test_dt(self, u): assert u.trajectory.ts.dt == pytest.approx(ref) def test_warn_user_no_time_information(self, u): - wmsg = ("NCDF trajectory does not contain `time` information;" - " `time` will be set as an increasing index") + wmsg = ( + "NCDF trajectory does not contain `time` information;" + " `time` will be set as an increasing index" + ) with pytest.warns(UserWarning, match=wmsg): u2 = mda.Universe(CPPTRAJ_TRAJ_TOP, CPPTRAJ_TRAJ) @@ -335,116 +396,139 @@ class _NCDFGenerator(object): def create_ncdf(self, params): """A basic modular ncdf writer based on :class:`NCDFWriter`""" # Create under context manager - with netcdf_file(params['filename'], mode='w', - version=params['version_byte']) as ncdf: + with netcdf_file( + params["filename"], mode="w", version=params["version_byte"] + ) as ncdf: # Top level attributes - if params['Conventions']: - setattr(ncdf, 'Conventions', params['Conventions']) - if params['ConventionVersion']: - setattr(ncdf, 'ConventionVersion', - params['ConventionVersion']) - if params['program']: - setattr(ncdf, 'program', params['program']) - if params['programVersion']: - setattr(ncdf, 'programVersion', params['programVersion']) + if params["Conventions"]: + setattr(ncdf, "Conventions", params["Conventions"]) + if params["ConventionVersion"]: + setattr(ncdf, "ConventionVersion", params["ConventionVersion"]) + if params["program"]: + setattr(ncdf, "program", params["program"]) + if params["programVersion"]: + setattr(ncdf, "programVersion", params["programVersion"]) # Dimensions - if params['frame']: - ncdf.createDimension('frame', None) - if params['n_atoms']: - ncdf.createDimension('atom', params['n_atoms']) - if params['spatial']: - ncdf.createDimension('spatial', params['spatial']) - if params['time']: - ncdf.createDimension('time', 1) - ncdf.createDimension('label', 5) - ncdf.createDimension('cell_spatial', 3) - ncdf.createDimension('cell_angular', 3) + if params["frame"]: + ncdf.createDimension("frame", None) + if params["n_atoms"]: + ncdf.createDimension("atom", params["n_atoms"]) + if params["spatial"]: + ncdf.createDimension("spatial", params["spatial"]) + if params["time"]: + ncdf.createDimension("time", 1) + ncdf.createDimension("label", 5) + ncdf.createDimension("cell_spatial", 3) + ncdf.createDimension("cell_angular", 3) # Variables - if params['time']: - time = ncdf.createVariable('time', 'd', ('time',)) - setattr(time, 'units', params['time']) + if params["time"]: + time = ncdf.createVariable("time", "d", ("time",)) + setattr(time, "units", params["time"]) time[:] = 1.0 - cell_spatial = ncdf.createVariable('cell_spatial', 'c', - ('cell_spatial', )) - cell_spatial[:] = np.asarray(list('abc')) - cell_angular = ncdf.createVariable('cell_angular', 'c', - ('cell_angular', 'label')) - cell_angular[:] = np.asarray([list('alpha'), list('beta '), - list('gamma')]) + cell_spatial = ncdf.createVariable( + "cell_spatial", "c", ("cell_spatial",) + ) + cell_spatial[:] = np.asarray(list("abc")) + cell_angular = ncdf.createVariable( + "cell_angular", "c", ("cell_angular", "label") + ) + cell_angular[:] = np.asarray( + [list("alpha"), list("beta "), list("gamma")] + ) # Spatial or atom dependent variables - if (params['spatial']) and (params['n_atoms']): - spatial = ncdf.createVariable('spatial', 'c', ('spatial',)) - spatial[:] = np.asarray(list('xyz')[:params['spatial']]) - if params['frame']: - if params['coordinates']: - coords = ncdf.createVariable('coordinates', 'f4', - ('frame', 'atom', - 'spatial')) - velocs = ncdf.createVariable('velocities', 'f4', - ('frame', 'atom', 'spatial')) - forces = ncdf.createVariable('forces', 'f4', - ('frame', 'atom', 'spatial')) - cell_lengths = ncdf.createVariable('cell_lengths', 'f8', - ('frame', - 'cell_spatial')) - cell_angles = ncdf.createVariable('cell_angles', 'f8', - ('frame', - 'cell_angular')) + if (params["spatial"]) and (params["n_atoms"]): + spatial = ncdf.createVariable("spatial", "c", ("spatial",)) + spatial[:] = np.asarray(list("xyz")[: params["spatial"]]) + if params["frame"]: + if params["coordinates"]: + coords = ncdf.createVariable( + "coordinates", "f4", ("frame", "atom", "spatial") + ) + velocs = ncdf.createVariable( + "velocities", "f4", ("frame", "atom", "spatial") + ) + forces = ncdf.createVariable( + "forces", "f4", ("frame", "atom", "spatial") + ) + cell_lengths = ncdf.createVariable( + "cell_lengths", "f8", ("frame", "cell_spatial") + ) + cell_angles = ncdf.createVariable( + "cell_angles", "f8", ("frame", "cell_angular") + ) else: - if params['coordinates']: - coords = ncdf.createVariable('coordinates', 'f8', - ('atom', 'spatial')) - cell_lengths = ncdf.createVariable('cell_lengths', 'f8', - ('cell_spatial',)) - cell_angles = ncdf.createVariable('cell_angles', 'f8', - ('cell_angular',)) - velocs = ncdf.createVariable('velocities', 'f8', - ('atom', 'spatial')) - forces = ncdf.createVariable('forces', 'f8', - ('atom', 'spatial')) + if params["coordinates"]: + coords = ncdf.createVariable( + "coordinates", "f8", ("atom", "spatial") + ) + cell_lengths = ncdf.createVariable( + "cell_lengths", "f8", ("cell_spatial",) + ) + cell_angles = ncdf.createVariable( + "cell_angles", "f8", ("cell_angular",) + ) + velocs = ncdf.createVariable( + "velocities", "f8", ("atom", "spatial") + ) + forces = ncdf.createVariable( + "forces", "f8", ("atom", "spatial") + ) # Set units - if params['coordinates']: - setattr(coords, 'units', params['coordinates']) - setattr(velocs, 'units', params['velocities']) - setattr(forces, 'units', params['forces']) - setattr(cell_lengths, 'units', params['cell_lengths']) - setattr(cell_angles, 'units', params['cell_angles']) + if params["coordinates"]: + setattr(coords, "units", params["coordinates"]) + setattr(velocs, "units", params["velocities"]) + setattr(forces, "units", params["forces"]) + setattr(cell_lengths, "units", params["cell_lengths"]) + setattr(cell_angles, "units", params["cell_angles"]) # Assign value - if params['frame']: - for index in range(params['frame']): - if params['coordinates']: + if params["frame"]: + for index in range(params["frame"]): + if params["coordinates"]: coords[index, :] = np.asarray( - range(params['spatial']), dtype=np.float32) - cell_lengths[index, :] = np.array([20., 20., 20.], - dtype=np.float32) - cell_angles[index, :] = np.array([90., 90., 90.], - dtype=np.float32) - velocs[index, :] = np.asarray(range(params['spatial']), - dtype=np.float32) - forces[index, :] = np.asarray(range(params['spatial']), - dtype=np.float32) + range(params["spatial"]), dtype=np.float32 + ) + cell_lengths[index, :] = np.array( + [20.0, 20.0, 20.0], dtype=np.float32 + ) + cell_angles[index, :] = np.array( + [90.0, 90.0, 90.0], dtype=np.float32 + ) + velocs[index, :] = np.asarray( + range(params["spatial"]), dtype=np.float32 + ) + forces[index, :] = np.asarray( + range(params["spatial"]), dtype=np.float32 + ) else: - if params['coordinates']: - coords[:] = np.asarray(range(params['spatial']), - dtype=np.float32) - cell_lengths[:] = np.array([20., 20., 20.], - dtype=np.float32) - cell_angles[:] = np.array([90., 90., 90.], - dtype=np.float32) - velocs[:] = np.asarray(range(params['spatial']), - dtype=np.float32) - forces[:] = np.asarray(range(params['spatial']), - dtype=np.float32) + if params["coordinates"]: + coords[:] = np.asarray( + range(params["spatial"]), dtype=np.float32 + ) + cell_lengths[:] = np.array( + [20.0, 20.0, 20.0], dtype=np.float32 + ) + cell_angles[:] = np.array( + [90.0, 90.0, 90.0], dtype=np.float32 + ) + velocs[:] = np.asarray( + range(params["spatial"]), dtype=np.float32 + ) + forces[:] = np.asarray( + range(params["spatial"]), dtype=np.float32 + ) # self.scale_factor overrides which variable gets a scale_factor - if params['scale_factor']: - setattr(ncdf.variables[params['scale_factor']], - 'scale_factor', params['scale_factor_value']) + if params["scale_factor"]: + setattr( + ncdf.variables[params["scale_factor"]], + "scale_factor", + params["scale_factor_value"], + ) def gen_params(self, keypair=None, restart=False): """Generate writer parameters, keypair can be used to overwrite @@ -452,28 +536,28 @@ def gen_params(self, keypair=None, restart=False): """ params = { - 'filename': 'test.nc', - 'version_byte': 2, - 'Conventions': 'AMBER', - 'ConventionVersion': '1.0', - 'program': 'mda test_writer', - 'programVersion': 'V42', - 'n_atoms': 1, - 'spatial': 3, - 'coordinates': 'angstrom', - 'velocities': 'angstrom/picosecond', - 'forces': 'kilocalorie/mole/angstrom', - 'cell_lengths': 'angstrom', - 'cell_angles': 'degree', - 'time': 'picosecond', - 'scale_factor': None, - 'scale_factor_value': 2.0, - 'frame': 2 + "filename": "test.nc", + "version_byte": 2, + "Conventions": "AMBER", + "ConventionVersion": "1.0", + "program": "mda test_writer", + "programVersion": "V42", + "n_atoms": 1, + "spatial": 3, + "coordinates": "angstrom", + "velocities": "angstrom/picosecond", + "forces": "kilocalorie/mole/angstrom", + "cell_lengths": "angstrom", + "cell_angles": "degree", + "time": "picosecond", + "scale_factor": None, + "scale_factor_value": 2.0, + "frame": 2, } if restart: - params['filename'] = 'test.ncrst' - params['frame'] = None + params["filename"] = "test.ncrst" + params["frame"] = None if keypair: for entry in keypair: @@ -487,150 +571,166 @@ class TestScaleFactorImplementation(_NCDFGenerator): prec = 5 def test_scale_factor_coordinates(self, tmpdir): - mutation = {'scale_factor': 'coordinates'} + mutation = {"scale_factor": "coordinates"} params = self.gen_params(keypair=mutation, restart=False) expected = np.asarray(range(3), dtype=np.float32) * 2.0 with tmpdir.as_cwd(): self.create_ncdf(params) - u = mda.Universe(params['filename'], to_guess=()) + u = mda.Universe(params["filename"], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.positions[0], expected, self.prec) def test_scale_factor_velocities(self, tmpdir): - mutation = {'scale_factor': 'velocities', 'scale_factor_value': 3.0} + mutation = {"scale_factor": "velocities", "scale_factor_value": 3.0} params = self.gen_params(keypair=mutation, restart=False) expected = np.asarray(range(3), dtype=np.float32) * 3.0 with tmpdir.as_cwd(): self.create_ncdf(params) - u = mda.Universe(params['filename'], to_guess=()) + u = mda.Universe(params["filename"], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.velocities[0], expected, self.prec) def test_scale_factor_forces(self, tmpdir): - mutation = {'scale_factor': 'forces', 'scale_factor_value': 10.0} + mutation = {"scale_factor": "forces", "scale_factor_value": 10.0} params = self.gen_params(keypair=mutation, restart=False) 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'], to_guess=()) + u = mda.Universe(params["filename"], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.forces[0], expected, self.prec) - @pytest.mark.parametrize('mutation,expected', ( - ({'scale_factor': 'cell_lengths', 'scale_factor_value': 0.75}, - np.array([15., 15., 15., 90., 90., 90.])), - ({'scale_factor': 'cell_angles', 'scale_factor_value': 0.5}, - np.array([20., 20., 20., 45., 45., 45.])) - )) + @pytest.mark.parametrize( + "mutation,expected", + ( + ( + {"scale_factor": "cell_lengths", "scale_factor_value": 0.75}, + np.array([15.0, 15.0, 15.0, 90.0, 90.0, 90.0]), + ), + ( + {"scale_factor": "cell_angles", "scale_factor_value": 0.5}, + np.array([20.0, 20.0, 20.0, 45.0, 45.0, 45.0]), + ), + ), + ) 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'], to_guess=()) + u = mda.Universe(params["filename"], to_guess=()) for ts in u.trajectory: assert_almost_equal(ts.dimensions, expected, self.prec) def test_scale_factor_not_float(self, tmpdir): - mutation = {'scale_factor': 'coordinates', - 'scale_factor_value': 'parsnips'} + mutation = { + "scale_factor": "coordinates", + "scale_factor_value": "parsnips", + } params = self.gen_params(keypair=mutation, restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) errmsg = "b'parsnips' is not a float" with pytest.raises(TypeError, match=errmsg): - u = mda.Universe(params['filename']) + u = mda.Universe(params["filename"]) class TestNCDFReaderExceptionsWarnings(_NCDFGenerator): - @pytest.mark.parametrize('mutation', [ - {'Conventions': 'Foo'}, - {'version_byte': 1}, - {'spatial': 2} - ]) + @pytest.mark.parametrize( + "mutation", + [{"Conventions": "Foo"}, {"version_byte": 1}, {"spatial": 2}], + ) def test_type_errors(self, tmpdir, mutation): params = self.gen_params(keypair=mutation, restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) with pytest.raises(TypeError): - NCDFReader(params['filename']) - - @pytest.mark.parametrize('mutation', [ - {'Conventions': None}, - {'ConventionVersion': None}, - {'spatial': None}, - {'n_atoms': None}, - {'frame': None} - ]) + NCDFReader(params["filename"]) + + @pytest.mark.parametrize( + "mutation", + [ + {"Conventions": None}, + {"ConventionVersion": None}, + {"spatial": None}, + {"n_atoms": None}, + {"frame": None}, + ], + ) def test_value_errors(self, tmpdir, mutation): params = self.gen_params(keypair=mutation, restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) with pytest.raises(ValueError): - NCDFReader(params['filename']) - - @pytest.mark.parametrize('mutation', [ - {'scale_factor': 'cell_spatial'}, - {'time': 'femtosecond'}, - {'coordinates': 'nanometer'}, - {'velocities': 'angstrom/akma'}, - {'forces': 'kilojoule/mole/angstrom'}, - {'cell_lengths': 'nanometer'}, - {'cell_angles': 'radians'} - ]) + NCDFReader(params["filename"]) + + @pytest.mark.parametrize( + "mutation", + [ + {"scale_factor": "cell_spatial"}, + {"time": "femtosecond"}, + {"coordinates": "nanometer"}, + {"velocities": "angstrom/akma"}, + {"forces": "kilojoule/mole/angstrom"}, + {"cell_lengths": "nanometer"}, + {"cell_angles": "radians"}, + ], + ) def test_notimplemented_errors(self, tmpdir, mutation): params = self.gen_params(keypair=mutation, restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) with pytest.raises(NotImplementedError): - NCDFReader(params['filename']) + NCDFReader(params["filename"]) - @pytest.mark.parametrize('evaluate,expected', ( - ('yard', 'foot'), - ('second', 'minute') - )) + @pytest.mark.parametrize( + "evaluate,expected", (("yard", "foot"), ("second", "minute")) + ) def test_verify_units_errors(self, evaluate, expected): """Directly tests expected failures of _verify_units""" with pytest.raises(NotImplementedError): - NCDFReader._verify_units(evaluate.encode('utf-8'), expected) + NCDFReader._verify_units(evaluate.encode("utf-8"), expected) def test_ioerror(self, tmpdir): params = self.gen_params(restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) with pytest.raises(IOError): - u = mda.Universe(params['filename'], to_guess=()) + u = mda.Universe(params["filename"], to_guess=()) u.trajectory.close() u.trajectory[-1] def test_conventionversion_warn(self, tmpdir): - mutation = {'ConventionVersion': '2.0'} + mutation = {"ConventionVersion": "2.0"} params = self.gen_params(keypair=mutation, restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) with pytest.warns(UserWarning) as record: - NCDFReader(params['filename']) + NCDFReader(params["filename"]) assert len(record) == 1 - wmsg = ("NCDF trajectory format is 2.0 but the reader " - "implements format 1.0") + wmsg = ( + "NCDF trajectory format is 2.0 but the reader " + "implements format 1.0" + ) assert str(record[0].message.args[0]) == wmsg - @pytest.mark.parametrize('mutation', [ - {'program': None}, - {'programVersion': None} - ]) + @pytest.mark.parametrize( + "mutation", [{"program": None}, {"programVersion": None}] + ) def test_program_warn(self, tmpdir, mutation): params = self.gen_params(keypair=mutation, restart=False) with tmpdir.as_cwd(): self.create_ncdf(params) with pytest.warns(UserWarning) as record: - NCDFReader(params['filename']) + NCDFReader(params["filename"]) assert len(record) == 1 - wmsg = ("NCDF trajectory test.nc may not fully adhere to AMBER " - "standards as either the `program` or `programVersion` " - "attributes are missing") + wmsg = ( + "NCDF trajectory test.nc may not fully adhere to AMBER " + "standards as either the `program` or `programVersion` " + "attributes are missing" + ) assert str(record[0].message.args[0]) == wmsg def test_no_dt_warning(self, tmpdir): @@ -638,13 +738,13 @@ def test_no_dt_warning(self, tmpdir): Also at the same time checks that single frame chain reading works""" u = mda.Universe(PFncdf_Top, PFncdf_Trj) with tmpdir.as_cwd(): - with NCDFWriter('single_frame.nc', u.atoms.n_atoms) as W: + with NCDFWriter("single_frame.nc", u.atoms.n_atoms) as W: W.write(u) # Using the ChainReader implicitly calls dt() and thus _get_dt() wmsg = "Reader has no dt information, set to 1.0 ps" with pytest.warns(UserWarning, match=wmsg): - u2 = mda.Universe(PFncdf_Top, [PFncdf_Trj, 'single_frame.nc']) + u2 = mda.Universe(PFncdf_Top, [PFncdf_Trj, "single_frame.nc"]) class _NCDFWriterTest(object): @@ -654,19 +754,19 @@ class _NCDFWriterTest(object): def universe(self): return mda.Universe(self.topology, self.filename) - @pytest.fixture(params=['nc', 'ncdf']) + @pytest.fixture(params=["nc", "ncdf"]) def outfile_extensions(self, tmpdir, request): # Issue 3030, test all extensions of NCDFWriter ext = request.param - return str(tmpdir) + f'ncdf-writer-1.{ext}' + return str(tmpdir) + f"ncdf-writer-1.{ext}" @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir) + 'ncdf-writer-1.ncdf' + return str(tmpdir) + "ncdf-writer-1.ncdf" @pytest.fixture() def outtop(self, tmpdir): - return str(tmpdir) + 'ncdf-writer-top.pdb' + return str(tmpdir) + "ncdf-writer-top.pdb" def _test_write_trajectory(self, universe, outfile): # explicit import so that we can artifically remove netCDF4 @@ -685,15 +785,21 @@ def _test_write_trajectory(self, universe, outfile): # which should be "float32". # See http://docs.scipy.org/doc/numpy-1.10.0/reference/arrays.dtypes.html # and https://github.com/MDAnalysis/mdanalysis/pull/503 - dataset = netcdf_file(outfile, 'r') - coords = dataset.variables['coordinates'] - time = dataset.variables['time'] - assert_equal(coords[:].dtype.name, np.dtype(np.float32).name, - err_msg='ncdf coord output not float32 ' - 'but {}'.format(coords[:].dtype)) - assert_equal(time[:].dtype.name, np.dtype(np.float32).name, - err_msg='ncdf time output not float32 ' - 'but {}'.format(time[:].dtype)) + dataset = netcdf_file(outfile, "r") + coords = dataset.variables["coordinates"] + time = dataset.variables["time"] + assert_equal( + coords[:].dtype.name, + np.dtype(np.float32).name, + err_msg="ncdf coord output not float32 " + "but {}".format(coords[:].dtype), + ) + assert_equal( + time[:].dtype.name, + np.dtype(np.float32).name, + err_msg="ncdf time output not float32 " + "but {}".format(time[:].dtype), + ) def test_write_trajectory_netCDF4(self, universe, outfile): pytest.importorskip("netCDF4") @@ -701,16 +807,19 @@ def test_write_trajectory_netCDF4(self, universe, outfile): def test_write_trajectory_netcdf(self, universe, outfile): import MDAnalysis.coordinates.TRJ - loaded_netCDF4 = sys.modules['MDAnalysis.coordinates.TRJ'].netCDF4 + + loaded_netCDF4 = sys.modules["MDAnalysis.coordinates.TRJ"].netCDF4 try: # cannot use @block_import('netCDF4') because TRJ was already imported # during setup() and already sits in the global module list so we just # set it to None because that is what TRJ does if it cannot find netCDF4 - sys.modules['MDAnalysis.coordinates.TRJ'].netCDF4 = None - assert MDAnalysis.coordinates.TRJ.netCDF4 is None # should happen if netCDF4 not found + sys.modules["MDAnalysis.coordinates.TRJ"].netCDF4 = None + assert ( + MDAnalysis.coordinates.TRJ.netCDF4 is None + ) # should happen if netCDF4 not found return self._test_write_trajectory(universe, outfile) finally: - sys.modules['MDAnalysis.coordinates.TRJ'].netCDF4 = loaded_netCDF4 + sys.modules["MDAnalysis.coordinates.TRJ"].netCDF4 = loaded_netCDF4 def test_OtherWriter(self, universe, outfile_extensions): t = universe.trajectory @@ -726,22 +835,30 @@ def _check_new_traj(self, universe, outfile): uw = mda.Universe(self.topology, outfile) # check that the trajectories are identical for each time step - for orig_ts, written_ts in zip(universe.trajectory, - uw.trajectory): - assert_almost_equal(written_ts._pos, orig_ts._pos, self.prec, - err_msg="coordinate mismatch between " - "original and written trajectory at " - "frame %d (orig) vs %d (written)" % ( - orig_ts.frame, - written_ts.frame)) + for orig_ts, written_ts in zip(universe.trajectory, uw.trajectory): + assert_almost_equal( + written_ts._pos, + orig_ts._pos, + self.prec, + err_msg="coordinate mismatch between " + "original and written trajectory at " + "frame %d (orig) vs %d (written)" + % (orig_ts.frame, written_ts.frame), + ) # not a good test because in the example trajectory all times are 0 - assert_almost_equal(orig_ts.time, written_ts.time, self.prec, - err_msg="Time for step {0} are not the " - "same.".format(orig_ts.frame)) - assert_almost_equal(written_ts.dimensions, - orig_ts.dimensions, - self.prec, - err_msg="unitcells are not identical") + assert_almost_equal( + orig_ts.time, + written_ts.time, + self.prec, + err_msg="Time for step {0} are not the " + "same.".format(orig_ts.frame), + ) + assert_almost_equal( + written_ts.dimensions, + orig_ts.dimensions, + self.prec, + err_msg="unitcells are not identical", + ) # check that the NCDF data structures are the same nc_orig = universe.trajectory.trjfile nc_copy = uw.trajectory.trjfile @@ -752,58 +869,84 @@ def _check_new_traj(self, universe, outfile): try: dim_new = nc_copy.dimensions[k] except KeyError: - raise AssertionError("NCDFWriter did not write " - "dimension '{0}'".format(k)) + raise AssertionError( + "NCDFWriter did not write " "dimension '{0}'".format(k) + ) else: - assert_equal(dim, dim_new, - err_msg="Dimension '{0}' size mismatch".format(k)) + assert_equal( + dim, + dim_new, + err_msg="Dimension '{0}' size mismatch".format(k), + ) for k, v in nc_orig.variables.items(): try: v_new = nc_copy.variables[k] except KeyError: - raise AssertionError("NCDFWriter did not write " - "variable '{0}'".format(k)) + raise AssertionError( + "NCDFWriter did not write " "variable '{0}'".format(k) + ) else: try: - assert_almost_equal(v[:], v_new[:], self.prec, - err_msg="Variable '{0}' not " - "written correctly".format( - k)) + assert_almost_equal( + v[:], + v_new[:], + self.prec, + err_msg="Variable '{0}' not " + "written correctly".format(k), + ) except TypeError: - assert_equal(v[:], v_new[:], - err_msg="Variable {0} not written " - "correctly".format(k)) + assert_equal( + v[:], + v_new[:], + err_msg="Variable {0} not written " + "correctly".format(k), + ) def test_TRR2NCDF(self, outfile): trr = mda.Universe(GRO, TRR) - with mda.Writer(outfile, trr.trajectory.n_atoms, - velocities=True, format="ncdf") as W: + with mda.Writer( + outfile, trr.trajectory.n_atoms, velocities=True, format="ncdf" + ) as W: for ts in trr.trajectory: W.write(trr) uw = mda.Universe(GRO, outfile) - for orig_ts, written_ts in zip(trr.trajectory, - uw.trajectory): - assert_almost_equal(written_ts._pos, orig_ts._pos, self.prec, - err_msg="coordinate mismatch between " - "original and written trajectory at " - "frame {0} (orig) vs {1} (written)".format( - orig_ts.frame, written_ts.frame)) - assert_almost_equal(written_ts._velocities, - orig_ts._velocities, self.prec, - err_msg="velocity mismatch between " - "original and written trajectory at " - "frame {0} (orig) vs {1} (written)".format( - orig_ts.frame, written_ts.frame)) - assert_almost_equal(orig_ts.time, written_ts.time, self.prec, - err_msg="Time for step {0} are not the " - "same.".format(orig_ts.frame)) - assert_almost_equal(written_ts.dimensions, - orig_ts.dimensions, - self.prec, - err_msg="unitcells are not identical") + for orig_ts, written_ts in zip(trr.trajectory, uw.trajectory): + assert_almost_equal( + written_ts._pos, + orig_ts._pos, + self.prec, + err_msg="coordinate mismatch between " + "original and written trajectory at " + "frame {0} (orig) vs {1} (written)".format( + orig_ts.frame, written_ts.frame + ), + ) + assert_almost_equal( + written_ts._velocities, + orig_ts._velocities, + self.prec, + err_msg="velocity mismatch between " + "original and written trajectory at " + "frame {0} (orig) vs {1} (written)".format( + orig_ts.frame, written_ts.frame + ), + ) + assert_almost_equal( + orig_ts.time, + written_ts.time, + self.prec, + err_msg="Time for step {0} are not the " + "same.".format(orig_ts.frame), + ) + assert_almost_equal( + written_ts.dimensions, + orig_ts.dimensions, + self.prec, + err_msg="unitcells are not identical", + ) del trr def test_write_AtomGroup(self, universe, outfile, outtop): @@ -817,21 +960,29 @@ def test_write_AtomGroup(self, universe, outfile, outtop): uw = mda.Universe(outtop, outfile) pw = uw.atoms - for orig_ts, written_ts in zip(universe.trajectory, - uw.trajectory): - assert_almost_equal(p.positions, pw.positions, self.prec, - err_msg="coordinate mismatch between " - "original and written trajectory at " - "frame %d (orig) vs %d (written)" % ( - orig_ts.frame, - written_ts.frame)) - assert_almost_equal(orig_ts.time, written_ts.time, self.prec, - err_msg="Time for step {0} are not the " - "same.".format(orig_ts.frame)) - assert_almost_equal(written_ts.dimensions, - orig_ts.dimensions, - self.prec, - err_msg="unitcells are not identical") + for orig_ts, written_ts in zip(universe.trajectory, uw.trajectory): + assert_almost_equal( + p.positions, + pw.positions, + self.prec, + err_msg="coordinate mismatch between " + "original and written trajectory at " + "frame %d (orig) vs %d (written)" + % (orig_ts.frame, written_ts.frame), + ) + assert_almost_equal( + orig_ts.time, + written_ts.time, + self.prec, + err_msg="Time for step {0} are not the " + "same.".format(orig_ts.frame), + ) + assert_almost_equal( + written_ts.dimensions, + orig_ts.dimensions, + self.prec, + err_msg="unitcells are not identical", + ) class TestNCDFWriter(_NCDFWriterTest, RefVGV): @@ -844,14 +995,19 @@ class TestNCDFWriterTZ2(_NCDFWriterTest, RefTZ2): class TestNCDFWriterVelsForces(object): """Test writing NCDF trajectories with a mixture of options""" + prec = 3 top = XYZ_mini n_atoms = 3 @pytest.fixture() def u1(self): - u = make_Universe(size=(self.n_atoms, 1, 1), trajectory=True, - velocities=True, forces=True) + u = make_Universe( + size=(self.n_atoms, 1, 1), + trajectory=True, + velocities=True, + forces=True, + ) # Memory reader so changes should be in-place u.atoms.velocities += 100 u.atoms.forces += 200 @@ -859,31 +1015,37 @@ def u1(self): @pytest.fixture() def u2(self): - u = make_Universe(size=(self.n_atoms, 1, 1), trajectory=True, - velocities=True, forces=True) + u = make_Universe( + size=(self.n_atoms, 1, 1), + trajectory=True, + velocities=True, + forces=True, + ) # Memory reader so changes should be in-place u.atoms.positions += 300 u.atoms.velocities += 400 u.atoms.forces += 500 return u - @pytest.mark.parametrize('pos, vel, force', ( + @pytest.mark.parametrize( + "pos, vel, force", + ( (True, False, False), (True, True, False), (True, False, True), (True, True, True), - )) + ), + ) def test_write_u(self, pos, vel, force, tmpdir, u1, u2): """Write the two reference universes, then open them up and check values pos vel and force are bools which define whether these properties should be in universe """ - outfile = str(tmpdir) + 'ncdf-write-vels-force.ncdf' - with NCDFWriter(outfile, - n_atoms=self.n_atoms, - velocities=vel, - forces=force) as w: + outfile = str(tmpdir) + "ncdf-write-vels-force.ncdf" + with NCDFWriter( + outfile, n_atoms=self.n_atoms, velocities=vel, forces=force + ) as w: w.write(u1) w.write(u2) @@ -895,23 +1057,26 @@ def test_write_u(self, pos, vel, force, tmpdir, u1, u2): u = mda.Universe(self.top, outfile) # check the trajectory contents match reference universes - for ts, ref_ts in zip(u.trajectory, [u1.trajectory.ts, u2.trajectory.ts]): + for ts, ref_ts in zip( + u.trajectory, [u1.trajectory.ts, u2.trajectory.ts] + ): if pos: assert_almost_equal(ts._pos, ref_ts._pos, self.prec) else: with pytest.raises(mda.NoDataError): - getattr(ts, 'positions') + getattr(ts, "positions") if vel: - assert_almost_equal(ts._velocities, ref_ts._velocities, - self.prec) + assert_almost_equal( + ts._velocities, ref_ts._velocities, self.prec + ) else: with pytest.raises(mda.NoDataError): - getattr(ts, 'velocities') + getattr(ts, "velocities") if force: assert_almost_equal(ts._forces, ref_ts._forces, self.prec) else: with pytest.raises(mda.NoDataError): - getattr(ts, 'forces') + getattr(ts, "forces") u.trajectory.close() @@ -922,11 +1087,11 @@ class TestNCDFWriterScaleFactors: @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir) + 'ncdf-write-scale.ncdf' + return str(tmpdir) + "ncdf-write-scale.ncdf" @pytest.fixture() def outfile2(self, tmpdir): - return str(tmpdir) + 'ncdf-write-scale2.ncdf' + return str(tmpdir) + "ncdf-write-scale2.ncdf" @pytest.fixture() def universe(self): @@ -939,7 +1104,7 @@ def get_scale_factors(self, ncdfile): # be faster & ok to set it to True with netcdf_file(ncdfile, mmap=False) as f: for var in f.variables: - if hasattr(f.variables[var], 'scale_factor'): + if hasattr(f.variables[var], "scale_factor"): sfactors[var] = f.variables[var].scale_factor return sfactors @@ -956,30 +1121,53 @@ def test_write_read_factors_default(self, outfile, universe): # check scale_factors sfactors = self.get_scale_factors(outfile) assert len(sfactors) == 1 - assert sfactors['velocities'] == 20.455 + assert sfactors["velocities"] == 20.455 def test_write_bad_scale_factor(self, outfile, universe): errmsg = "scale_factor parsnips is not a float" with pytest.raises(TypeError, match=errmsg): - NCDFWriter(outfile, n_atoms=len(universe.atoms), - scale_velocities="parsnips") - - @pytest.mark.parametrize('stime, slengths, sangles, scoords, svels, sfrcs', ( + NCDFWriter( + outfile, + n_atoms=len(universe.atoms), + scale_velocities="parsnips", + ) + + @pytest.mark.parametrize( + "stime, slengths, sangles, scoords, svels, sfrcs", + ( (-2.0, -2.0, -2.0, -2.0, -2.0, -2.0), (1.0, 1.0, 1.0, 1.0, 1.0, 1.0), - (2.0, 4.0, 8.0, 16.0, 32.0, 64.0) - )) - def test_write_read_write(self, outfile, outfile2, universe, stime, - slengths, sangles, scoords, svels, sfrcs): + (2.0, 4.0, 8.0, 16.0, 32.0, 64.0), + ), + ) + def test_write_read_write( + self, + outfile, + outfile2, + universe, + stime, + slengths, + sangles, + scoords, + svels, + sfrcs, + ): """Write out a file with assorted scale_factors, then read it back in, then write it out to make sure that the new assorted scale_factors have been retained by Write""" - with NCDFWriter(outfile, n_atoms=len(universe.atoms), velocities=True, - forces=True, scale_time=stime, - scale_cell_lengths=slengths, scale_cell_angles=sangles, - scale_coordinates=scoords, scale_velocities=svels, - scale_forces=sfrcs) as W: + with NCDFWriter( + outfile, + n_atoms=len(universe.atoms), + velocities=True, + forces=True, + scale_time=stime, + scale_cell_lengths=slengths, + scale_cell_angles=sangles, + scale_coordinates=scoords, + scale_velocities=svels, + scale_forces=sfrcs, + ) as W: for ts in universe.trajectory: W.write(universe.atoms) @@ -997,46 +1185,70 @@ def test_write_read_write(self, outfile, outfile2, universe, stime, assert sfactors1 == sfactors2 assert len(sfactors1) == 6 - assert sfactors1['time'] == stime - assert sfactors1['cell_lengths'] == slengths - assert sfactors1['cell_angles'] == sangles - assert sfactors1['coordinates'] == scoords - assert sfactors1['velocities'] == svels - assert sfactors1['forces'] == sfrcs + assert sfactors1["time"] == stime + assert sfactors1["cell_lengths"] == slengths + assert sfactors1["cell_angles"] == sangles + assert sfactors1["coordinates"] == scoords + assert sfactors1["velocities"] == svels + assert sfactors1["forces"] == sfrcs # check that the stored values are indeed scaled - assert_almost_equal(universe.trajectory.time / stime, - self.get_variable(outfile, 'time', 0), 4) - assert_almost_equal(universe.dimensions[:3] / slengths, - self.get_variable(outfile, 'cell_lengths', 0), 4) - assert_almost_equal(universe.dimensions[3:] / sangles, - self.get_variable(outfile, 'cell_angles', 0), 4) - assert_almost_equal(universe.atoms.positions / scoords, - self.get_variable(outfile, 'coordinates', 0), 4) - assert_almost_equal(universe.atoms.velocities / svels, - self.get_variable(outfile, 'velocities', 0), 4) + assert_almost_equal( + universe.trajectory.time / stime, + self.get_variable(outfile, "time", 0), + 4, + ) + assert_almost_equal( + universe.dimensions[:3] / slengths, + self.get_variable(outfile, "cell_lengths", 0), + 4, + ) + assert_almost_equal( + universe.dimensions[3:] / sangles, + self.get_variable(outfile, "cell_angles", 0), + 4, + ) + assert_almost_equal( + universe.atoms.positions / scoords, + self.get_variable(outfile, "coordinates", 0), + 4, + ) + assert_almost_equal( + universe.atoms.velocities / svels, + self.get_variable(outfile, "velocities", 0), + 4, + ) # note: kJ/mol -> kcal/mol = 4.184 conversion - assert_almost_equal(universe.atoms.forces / (sfrcs * 4.184), - self.get_variable(outfile, 'forces', 0), 4) + assert_almost_equal( + universe.atoms.forces / (sfrcs * 4.184), + self.get_variable(outfile, "forces", 0), + 4, + ) # check that the individual components were saved/read properly for ts1, ts3 in zip(universe.trajectory, universe3.trajectory): assert_almost_equal(ts1.time, ts3.time) assert_almost_equal(ts1.dimensions, ts3.dimensions) - assert_almost_equal(universe.atoms.positions, - universe3.atoms.positions, 4) - assert_almost_equal(universe.atoms.velocities, - universe3.atoms.velocities, 4) - assert_almost_equal(universe.atoms.forces, - universe3.atoms.forces, 4) + assert_almost_equal( + universe.atoms.positions, universe3.atoms.positions, 4 + ) + assert_almost_equal( + universe.atoms.velocities, universe3.atoms.velocities, 4 + ) + assert_almost_equal( + universe.atoms.forces, universe3.atoms.forces, 4 + ) class TestScipyScaleFactors(TestNCDFWriterScaleFactors): """As above, but netCDF4 is disabled since scaleandmask is different between the two libraries""" + @pytest.fixture(autouse=True) def block_netcdf4(self, monkeypatch): - monkeypatch.setattr(sys.modules['MDAnalysis.coordinates.TRJ'], 'netCDF4', None) + monkeypatch.setattr( + sys.modules["MDAnalysis.coordinates.TRJ"], "netCDF4", None + ) def test_ncdf4_not_present(self, outfile, universe): # whilst we're here, let's also test this warning @@ -1047,35 +1259,44 @@ def test_ncdf4_not_present(self, outfile, universe): class TestNCDFWriterUnits(object): """Tests that the writer adheres to AMBER convention units""" + @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir) + 'ncdf-writer-1.ncdf' - - @pytest.mark.parametrize('var, expected', ( - ('coordinates', 'angstrom'), - ('time', 'picosecond'), - ('cell_lengths', 'angstrom'), - ('cell_angles', 'degree'), - ('velocities', 'angstrom/picosecond'), - ('forces', 'kilocalorie/mole/angstrom') - )) + return str(tmpdir) + "ncdf-writer-1.ncdf" + + @pytest.mark.parametrize( + "var, expected", + ( + ("coordinates", "angstrom"), + ("time", "picosecond"), + ("cell_lengths", "angstrom"), + ("cell_angles", "degree"), + ("velocities", "angstrom/picosecond"), + ("forces", "kilocalorie/mole/angstrom"), + ), + ) def test_writer_units(self, outfile, var, expected): - trr = mda.Universe(DLP_CONFIG, format='CONFIG') - - with mda.Writer(outfile, trr.trajectory.n_atoms, velocities=True, - forces=True, format='ncdf') as W: + trr = mda.Universe(DLP_CONFIG, format="CONFIG") + + with mda.Writer( + outfile, + trr.trajectory.n_atoms, + velocities=True, + forces=True, + format="ncdf", + ) as W: for ts in trr.trajectory: W.write(trr) - with netcdf_file(outfile, mode='r') as ncdf: - unit = ncdf.variables[var].units.decode('utf-8') + with netcdf_file(outfile, mode="r") as ncdf: + unit = ncdf.variables[var].units.decode("utf-8") assert_equal(unit, expected) class TestNCDFWriterErrorsWarnings(object): @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir) + 'out.ncdf' + return str(tmpdir) + "out.ncdf" def test_zero_atoms_VE(self, outfile): with pytest.raises(ValueError): diff --git a/testsuite/MDAnalysisTests/coordinates/test_null.py b/testsuite/MDAnalysisTests/coordinates/test_null.py index fc23803258d..2990930e67a 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_null.py +++ b/testsuite/MDAnalysisTests/coordinates/test_null.py @@ -23,7 +23,7 @@ import MDAnalysis as mda import pytest -from MDAnalysisTests.datafiles import (TPR, XTC) +from MDAnalysisTests.datafiles import TPR, XTC @pytest.fixture() diff --git a/testsuite/MDAnalysisTests/coordinates/test_pdb.py b/testsuite/MDAnalysisTests/coordinates/test_pdb.py index 441a91864d5..50219d7b039 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_pdb.py +++ b/testsuite/MDAnalysisTests/coordinates/test_pdb.py @@ -28,31 +28,53 @@ import pytest from MDAnalysisTests import make_Universe from MDAnalysisTests.coordinates.base import _SingleFrameReader -from MDAnalysisTests.coordinates.reference import (RefAdKSmall, - RefAdK) -from MDAnalysisTests.datafiles import (PDB, PDB_small, PDB_multiframe, - PDB_full, PDB_varying, - XPDB_small, PSF, DCD, CONECT, CRD, - INC_PDB, PDB_xlserial, ALIGN, ENT, - PDB_cm, PDB_cm_gz, PDB_cm_bz2, - PDB_mc, PDB_mc_gz, PDB_mc_bz2, - PDB_CRYOEM_BOX, MMTF_NOCRYST, - PDB_HOLE, mol2_molecule, PDB_charges, - CONECT_ERROR,) -from numpy.testing import (assert_equal, - assert_array_almost_equal, - assert_almost_equal, - assert_allclose) - -IGNORE_NO_INFORMATION_WARNING = 'ignore:Found no information for attr:UserWarning' +from MDAnalysisTests.coordinates.reference import RefAdKSmall, RefAdK +from MDAnalysisTests.datafiles import ( + PDB, + PDB_small, + PDB_multiframe, + PDB_full, + PDB_varying, + XPDB_small, + PSF, + DCD, + CONECT, + CRD, + INC_PDB, + PDB_xlserial, + ALIGN, + ENT, + PDB_cm, + PDB_cm_gz, + PDB_cm_bz2, + PDB_mc, + PDB_mc_gz, + PDB_mc_bz2, + PDB_CRYOEM_BOX, + MMTF_NOCRYST, + PDB_HOLE, + mol2_molecule, + PDB_charges, + CONECT_ERROR, +) +from numpy.testing import ( + assert_equal, + assert_array_almost_equal, + assert_almost_equal, + assert_allclose, +) + +IGNORE_NO_INFORMATION_WARNING = ( + "ignore:Found no information for attr:UserWarning" +) @pytest.fixture def dummy_universe_without_elements(): n_atoms = 5 u = make_Universe(size=(n_atoms, 1, 1), trajectory=True) - u.add_TopologyAttr('resnames', ['RES']) - u.add_TopologyAttr('names', ['C1', 'O2', 'N3', 'S4', 'NA']) + u.add_TopologyAttr("resnames", ["RES"]) + u.add_TopologyAttr("names", ["C1", "O2", "N3", "S4", "NA"]) u.dimensions = [42, 42, 42, 90, 90, 90] return u @@ -70,109 +92,140 @@ def setUp(self): def test_uses_PDBReader(self): from MDAnalysis.coordinates.PDB import PDBReader - assert isinstance(self.universe.trajectory, PDBReader), "failed to choose PDBReader" + assert isinstance( + self.universe.trajectory, PDBReader + ), "failed to choose PDBReader" def test_dimensions(self): assert_almost_equal( - self.universe.trajectory.ts.dimensions, RefAdKSmall.ref_unitcell, + self.universe.trajectory.ts.dimensions, + RefAdKSmall.ref_unitcell, self.prec, - "PDBReader failed to get unitcell dimensions from CRYST1") + "PDBReader failed to get unitcell dimensions from CRYST1", + ) def test_ENT(self): from MDAnalysis.coordinates.PDB import PDBReader + self.universe = mda.Universe(ENT) - assert isinstance(self.universe.trajectory, PDBReader), "failed to choose PDBReader" + assert isinstance( + self.universe.trajectory, PDBReader + ), "failed to choose PDBReader" class TestPDBMetadata(object): - header = 'HYDROLASE 11-MAR-12 4E43' - title = ['HIV PROTEASE (PR) DIMER WITH ACETATE IN EXO SITE AND PEPTIDE ' - 'IN ACTIVE', '2 SITE'] - compnd = ['MOL_ID: 1;', - '2 MOLECULE: PROTEASE;', - '3 CHAIN: A, B;', - '4 ENGINEERED: YES;', - '5 MUTATION: YES;', - '6 MOL_ID: 2;', - '7 MOLECULE: RANDOM PEPTIDE;', - '8 CHAIN: C;', - '9 ENGINEERED: YES;', - '10 OTHER_DETAILS: UNKNOWN IMPURITY', ] + header = "HYDROLASE 11-MAR-12 4E43" + title = [ + "HIV PROTEASE (PR) DIMER WITH ACETATE IN EXO SITE AND PEPTIDE " + "IN ACTIVE", + "2 SITE", + ] + compnd = [ + "MOL_ID: 1;", + "2 MOLECULE: PROTEASE;", + "3 CHAIN: A, B;", + "4 ENGINEERED: YES;", + "5 MUTATION: YES;", + "6 MOL_ID: 2;", + "7 MOLECULE: RANDOM PEPTIDE;", + "8 CHAIN: C;", + "9 ENGINEERED: YES;", + "10 OTHER_DETAILS: UNKNOWN IMPURITY", + ] num_remarks = 333 # only first 5 remarks for comparison nmax_remarks = 5 remarks = [ - '2', - '2 RESOLUTION. 1.54 ANGSTROMS.', - '3', - '3 REFINEMENT.', - '3 PROGRAM : REFMAC 5.5.0110', + "2", + "2 RESOLUTION. 1.54 ANGSTROMS.", + "3", + "3 REFINEMENT.", + "3 PROGRAM : REFMAC 5.5.0110", ] @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return mda.Universe(PDB_full) def test_HEADER(self, universe): - assert_equal(universe.trajectory.header, - self.header, - err_msg="HEADER record not correctly parsed") + assert_equal( + universe.trajectory.header, + self.header, + err_msg="HEADER record not correctly parsed", + ) def test_TITLE(self, universe): try: title = universe.trajectory.title except AttributeError: raise AssertionError("Reader does not have a 'title' attribute.") - assert_equal(len(title), - len(self.title), - err_msg="TITLE does not contain same number of lines") - for lineno, (parsed, reference) in enumerate(zip(title, self.title), - start=1): - assert_equal(parsed, - reference, - err_msg="TITLE line {0} do not match".format(lineno)) + assert_equal( + len(title), + len(self.title), + err_msg="TITLE does not contain same number of lines", + ) + for lineno, (parsed, reference) in enumerate( + zip(title, self.title), start=1 + ): + assert_equal( + parsed, + reference, + err_msg="TITLE line {0} do not match".format(lineno), + ) def test_COMPND(self, universe): try: compound = universe.trajectory.compound except AttributeError: raise AssertionError( - "Reader does not have a 'compound' attribute.") - assert_equal(len(compound), - len(self.compnd), - err_msg="COMPND does not contain same number of lines") - for lineno, (parsed, reference) in enumerate(zip(compound, - self.compnd), - start=1): - assert_equal(parsed, - reference, - err_msg="COMPND line {0} do not match".format(lineno)) + "Reader does not have a 'compound' attribute." + ) + assert_equal( + len(compound), + len(self.compnd), + err_msg="COMPND does not contain same number of lines", + ) + for lineno, (parsed, reference) in enumerate( + zip(compound, self.compnd), start=1 + ): + assert_equal( + parsed, + reference, + err_msg="COMPND line {0} do not match".format(lineno), + ) def test_REMARK(self, universe): try: remarks = universe.trajectory.remarks except AttributeError: raise AssertionError("Reader does not have a 'remarks' attribute.") - assert_equal(len(remarks), - self.num_remarks, - err_msg="REMARK does not contain same number of lines") + assert_equal( + len(remarks), + self.num_remarks, + err_msg="REMARK does not contain same number of lines", + ) # only look at the first 5 entries for lineno, (parsed, reference) in enumerate( - zip(remarks[:self.nmax_remarks], - self.remarks[:self.nmax_remarks]), - start=1): - assert_equal(parsed, - reference, - err_msg="REMARK line {0} do not match".format(lineno)) + zip( + remarks[: self.nmax_remarks], self.remarks[: self.nmax_remarks] + ), + start=1, + ): + assert_equal( + parsed, + reference, + err_msg="REMARK line {0} do not match".format(lineno), + ) class TestExtendedPDBReader(_SingleFrameReader): __test__ = True + def setUp(self): - self.universe = mda.Universe(PDB_small, - topology_format="XPDB", - format="XPDB") + self.universe = mda.Universe( + PDB_small, topology_format="XPDB", format="XPDB" + ) # 3 decimals in PDB spec # http://www.wwpdb.org/documentation/format32/sect9.html#ATOM self.prec = 3 @@ -181,7 +234,8 @@ def test_long_resSeq(self): # it checks that it can read a 5-digit resid self.universe = mda.Universe(XPDB_small, topology_format="XPDB") u = self.universe.select_atoms( - 'resid 1 or resid 10 or resid 100 or resid 1000 or resid 10000') + "resid 1 or resid 10 or resid 100 or resid 1000 or resid 10000" + ) assert_equal(u[4].resid, 10000, "can't read a five digit resid") @@ -211,10 +265,7 @@ def universe4(self): def universe5(self): return mda.Universe(mol2_molecule) - @pytest.fixture(params=[ - [PDB_CRYOEM_BOX, None], - [MMTF_NOCRYST, None] - ]) + @pytest.fixture(params=[[PDB_CRYOEM_BOX, None], [MMTF_NOCRYST, None]]) def universe_and_expected_dims(self, request): """ File with meaningless CRYST1 record and expected dimensions. @@ -226,7 +277,9 @@ def universe_and_expected_dims(self, request): @pytest.fixture def outfile(self, tmpdir): - return str(tmpdir.mkdir("PDBWriter").join('primitive-pdb-writer' + self.ext)) + return str( + tmpdir.mkdir("PDBWriter").join("primitive-pdb-writer" + self.ext) + ) @pytest.fixture def u_no_ids(self): @@ -234,41 +287,51 @@ def u_no_ids(self): # else the PDB writer expects to avoid issuing warnings. universe = make_Universe( [ - 'names', 'resids', 'resnames', 'altLocs', - 'segids', 'occupancies', 'tempfactors', + "names", + "resids", + "resnames", + "altLocs", + "segids", + "occupancies", + "tempfactors", ], trajectory=True, ) - universe.add_TopologyAttr('icodes', [' '] * len(universe.residues)) - universe.add_TopologyAttr('record_types', ['ATOM'] * len(universe.atoms)) + universe.add_TopologyAttr("icodes", [" "] * len(universe.residues)) + universe.add_TopologyAttr( + "record_types", ["ATOM"] * len(universe.atoms) + ) universe.dimensions = [10, 10, 10, 90, 90, 90] return universe @pytest.fixture def u_no_resnames(self): - return make_Universe(['names', 'resids'], trajectory=True) + return make_Universe(["names", "resids"], trajectory=True) @pytest.fixture def u_no_resids(self): - return make_Universe(['names', 'resnames'], trajectory=True) + return make_Universe(["names", "resnames"], trajectory=True) @pytest.fixture def u_no_names(self): - return make_Universe(['resids', 'resnames'], trajectory=True) + return make_Universe(["resids", "resnames"], trajectory=True) def test_writer(self, universe, outfile): "Test writing from a single frame PDB file to a PDB file." "" universe.atoms.write(outfile) u = mda.Universe(PSF, outfile) - assert_almost_equal(u.atoms.positions, - universe.atoms.positions, self.prec, - err_msg="Writing PDB file with PDBWriter " - "does not reproduce original coordinates") + assert_almost_equal( + u.atoms.positions, + universe.atoms.positions, + self.prec, + err_msg="Writing PDB file with PDBWriter " + "does not reproduce original coordinates", + ) def test_writer_no_resnames(self, u_no_resnames, outfile): u_no_resnames.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array(['UNK'] * u_no_resnames.atoms.n_atoms) + expected = np.array(["UNK"] * u_no_resnames.atoms.n_atoms) assert_equal(u.atoms.resnames, expected) def test_writer_no_resids(self, u_no_resids, outfile): @@ -280,25 +343,25 @@ def test_writer_no_resids(self, u_no_resids, outfile): def test_writer_no_atom_names(self, u_no_names, outfile): u_no_names.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array(['X'] * u_no_names.atoms.n_atoms) + expected = np.array(["X"] * u_no_names.atoms.n_atoms) assert_equal(u.atoms.names, expected) def test_writer_no_altlocs(self, u_no_names, outfile): u_no_names.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array([''] * u_no_names.atoms.n_atoms) + expected = np.array([""] * u_no_names.atoms.n_atoms) assert_equal(u.atoms.altLocs, expected) def test_writer_no_icodes(self, u_no_names, outfile): u_no_names.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array([''] * u_no_names.atoms.n_atoms) + expected = np.array([""] * u_no_names.atoms.n_atoms) assert_equal(u.atoms.icodes, expected) def test_writer_no_segids(self, u_no_names, outfile): u_no_names.atoms.write(outfile) u = mda.Universe(outfile) - expected = np.array(['X'] * u_no_names.atoms.n_atoms) + expected = np.array(["X"] * u_no_names.atoms.n_atoms) assert_equal([atom.segid for atom in u.atoms], expected) def test_writer_no_occupancies(self, u_no_names, outfile): @@ -318,12 +381,14 @@ def test_write_single_frame_Writer(self, universe2, outfile): MDAnalysis.Writer (Issue 105)""" u = universe2 u.trajectory[50] - with mda.Writer(outfile) as W: - W.write(u.select_atoms('all')) + with mda.Writer(outfile) as W: + W.write(u.select_atoms("all")) u2 = mda.Universe(outfile) - assert_equal(u2.trajectory.n_frames, - 1, - err_msg="The number of frames should be 1.") + assert_equal( + u2.trajectory.n_frames, + 1, + err_msg="The number of frames should be 1.", + ) def test_write_single_frame_AtomGroup(self, universe2, outfile): """Test writing a single frame from a DCD trajectory to a PDB using @@ -332,13 +397,19 @@ def test_write_single_frame_AtomGroup(self, universe2, outfile): u.trajectory[50] u.atoms.write(outfile) u2 = mda.Universe(PSF, outfile) - assert_equal(u2.trajectory.n_frames, - 1, - err_msg="Output PDB should only contain a single frame") - assert_almost_equal(u2.atoms.positions, u.atoms.positions, - self.prec, err_msg="Written coordinates do not " - "agree with original coordinates from frame %d" % - u.trajectory.frame) + assert_equal( + u2.trajectory.n_frames, + 1, + err_msg="Output PDB should only contain a single frame", + ) + assert_almost_equal( + u2.atoms.positions, + u.atoms.positions, + self.prec, + err_msg="Written coordinates do not " + "agree with original coordinates from frame %d" + % u.trajectory.frame, + ) def test_write_nodims(self, universe_and_expected_dims, outfile): """ @@ -362,25 +433,28 @@ def test_write_nodims(self, universe_and_expected_dims, outfile): with pytest.warns(UserWarning, match=expected_msg): u.atoms.write(outfile) - with pytest.warns(UserWarning, match="Unit cell dimensions will be set to None."): + with pytest.warns( + UserWarning, match="Unit cell dimensions will be set to None." + ): uout = mda.Universe(outfile) assert uout.dimensions is None, "Problem with default box." assert_equal( - uout.trajectory.n_frames, 1, - err_msg="Output PDB should only contain a single frame" + uout.trajectory.n_frames, + 1, + err_msg="Output PDB should only contain a single frame", ) assert_almost_equal( - u.atoms.positions, uout.atoms.positions, + u.atoms.positions, + uout.atoms.positions, self.prec, err_msg="Written coordinates do not " - "agree with original coordinates from frame %d" % - u.trajectory.frame + "agree with original coordinates from frame %d" + % u.trajectory.frame, ) - def test_check_coordinate_limits_min(self, universe, outfile): """Test that illegal PDB coordinates (x <= -999.9995 A) are caught with ValueError (Issue 57)""" @@ -413,16 +487,17 @@ def test_check_HEADER_TITLE_multiframe(self, universe2, outfile): got_header = 0 got_title = 0 for line in f: - if line.startswith('HEADER'): + if line.startswith("HEADER"): got_header += 1 assert got_header <= 1, "There should be only one HEADER." - elif line.startswith('TITLE'): + elif line.startswith("TITLE"): got_title += 1 assert got_title <= 1, "There should be only one TITLE." - @pytest.mark.parametrize("startframe,maxframes", - [(0, 12), (9997, 12)]) - def test_check_MODEL_multiframe(self, universe2, outfile, startframe, maxframes): + @pytest.mark.parametrize("startframe,maxframes", [(0, 12), (9997, 12)]) + def test_check_MODEL_multiframe( + self, universe2, outfile, startframe, maxframes + ): """Check whether MODEL number is in the right column (Issue #1950)""" u = universe2 protein = u.select_atoms("protein and name CA") @@ -439,7 +514,7 @@ def get_MODEL_lines(filename): MODEL_lines = list(get_MODEL_lines(outfile)) assert len(MODEL_lines) == maxframes - for model, line in enumerate(MODEL_lines, start=startframe+1): + for model, line in enumerate(MODEL_lines, start=startframe + 1): # test that only the right-most 4 digits are stored (rest must be space) # line[10:14] == '9999' or ' 1' @@ -449,14 +524,13 @@ def get_MODEL_lines(filename): # test number (only last 4 digits) assert int(line[10:14]) == model % 10000 - @pytest.mark.parametrize("bad_chainid", - ['@', '', 'AA']) + @pytest.mark.parametrize("bad_chainid", ["@", "", "AA"]) def test_chainid_validated(self, universe3, outfile, bad_chainid): """ Check that an atom's chainID is set to 'X' if the chainID does not confirm to standards (issue #2224) """ - default_id = 'X' + default_id = "X" u = universe3 u.atoms.chainIDs = bad_chainid u.atoms.write(outfile) @@ -493,16 +567,18 @@ def test_hetatm_written(self, universe4, tmpdir, outfile): u.atoms.write(outfile) written = mda.Universe(outfile) - written_atoms = written.select_atoms("resname ETA and " - "record_type HETATM") + written_atoms = written.select_atoms( + "resname ETA and " "record_type HETATM" + ) - assert len(u_hetatms) == len(written_atoms), \ - "mismatched HETATM number" - assert_almost_equal(u_hetatms.atoms.positions, - written_atoms.atoms.positions) + assert len(u_hetatms) == len(written_atoms), "mismatched HETATM number" + assert_almost_equal( + u_hetatms.atoms.positions, written_atoms.atoms.positions + ) - def test_default_atom_record_type_written(self, universe5, tmpdir, - outfile): + def test_default_atom_record_type_written( + self, universe5, tmpdir, outfile + ): """ Checks that ATOM record types are written when there is no record_type attribute. @@ -510,19 +586,19 @@ def test_default_atom_record_type_written(self, universe5, tmpdir, u = universe5 - expected_msg = ("Found no information for attr: " - "'record_types' Using default value of 'ATOM'") + expected_msg = ( + "Found no information for attr: " + "'record_types' Using default value of 'ATOM'" + ) with pytest.warns(UserWarning, match=expected_msg): u.atoms.write(outfile) written = mda.Universe(outfile) - assert len(u.atoms) == len(written.atoms), \ - "mismatched number of atoms" + assert len(u.atoms) == len(written.atoms), "mismatched number of atoms" atms = written.select_atoms("record_type ATOM") - assert len(atms.atoms) == len(u.atoms), \ - "mismatched ATOM number" + assert len(atms.atoms) == len(u.atoms), "mismatched ATOM number" hetatms = written.select_atoms("record_type HETATM") assert len(hetatms.atoms) == 0, "mismatched HETATM number" @@ -533,10 +609,12 @@ def test_abnormal_record_type(self, universe5, tmpdir, outfile): neither ATOM or HETATM. """ u = universe5 - u.add_TopologyAttr('record_type', ['ABNORM']*len(u.atoms)) + u.add_TopologyAttr("record_type", ["ABNORM"] * len(u.atoms)) - expected_msg = ("Found ABNORM for the record type, but only " - "allowed types are ATOM or HETATM") + expected_msg = ( + "Found ABNORM for the record type, but only " + "allowed types are ATOM or HETATM" + ) with pytest.raises(ValueError, match=expected_msg): u.atoms.write(outfile) @@ -559,14 +637,14 @@ def test_no_reindex_bonds(self, universe, outfile): record match the non-reindexed atoms. """ universe.atoms.ids = universe.atoms.ids + 23 - universe.atoms.write(outfile, reindex=False, bonds='all') + universe.atoms.write(outfile, reindex=False, bonds="all") with open(outfile) as infile: for line in infile: - if line.startswith('CONECT'): + if line.startswith("CONECT"): assert line.strip() == "CONECT 23 24 25 26 27" break else: - raise AssertError('No CONECT record fond in the output.') + raise AssertError("No CONECT record fond in the output.") @pytest.mark.filterwarnings(IGNORE_NO_INFORMATION_WARNING) def test_reindex(self, universe, outfile): @@ -586,43 +664,57 @@ def test_no_reindex_missing_ids(self, u_no_ids, outfile): then an exception is raised. """ # Making sure AG.ids is indeed missing - assert not hasattr(u_no_ids.atoms, 'ids') + assert not hasattr(u_no_ids.atoms, "ids") with pytest.raises(mda.exceptions.NoDataError): u_no_ids.atoms.write(outfile, reindex=False) class TestMultiPDBReader(object): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def multiverse(): return mda.Universe(PDB_multiframe, guess_bonds=True) @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def conect(): return mda.Universe(CONECT, guess_bonds=True) def test_n_frames(self, multiverse): - assert_equal(multiverse.trajectory.n_frames, 24, - "Wrong number of frames read from PDB muliple model file") + assert_equal( + multiverse.trajectory.n_frames, + 24, + "Wrong number of frames read from PDB muliple model file", + ) def test_n_atoms_frame(self, multiverse): u = multiverse desired = 392 for frame in u.trajectory: - assert_equal(len(u.atoms), desired, err_msg="The number of atoms " - "in the Universe (%d) does not" " match the number " - "of atoms in the test case (%d) at frame %d" % ( - len(u.atoms), desired, u.trajectory.frame)) + assert_equal( + len(u.atoms), + desired, + err_msg="The number of atoms " + "in the Universe (%d) does not" + " match the number " + "of atoms in the test case (%d) at frame %d" + % (len(u.atoms), desired, u.trajectory.frame), + ) def test_rewind(self, multiverse): u = multiverse u.trajectory[11] - assert_equal(u.trajectory.ts.frame, 11, - "Failed to forward to 11th frame (frame index 11)") + assert_equal( + u.trajectory.ts.frame, + 11, + "Failed to forward to 11th frame (frame index 11)", + ) u.trajectory.rewind() - assert_equal(u.trajectory.ts.frame, 0, - "Failed to rewind to 0th frame (frame index 0)") + assert_equal( + u.trajectory.ts.frame, + 0, + "Failed to rewind to 0th frame (frame index 0)", + ) def test_iteration(self, multiverse): u = multiverse @@ -634,25 +726,29 @@ def test_iteration(self, multiverse): for ts in u.trajectory: frames.append(ts) assert_equal( - len(frames), u.trajectory.n_frames, + len(frames), + u.trajectory.n_frames, "iterated number of frames %d is not the expected number %d; " - "trajectory iterator fails to rewind" % - (len(frames), u.trajectory.n_frames)) + "trajectory iterator fails to rewind" + % (len(frames), u.trajectory.n_frames), + ) def test_slice_iteration(self, multiverse): u = multiverse frames = [] for ts in u.trajectory[4:-2:4]: frames.append(ts.frame) - assert_equal(np.array(frames), - np.arange(u.trajectory.n_frames)[4:-2:4], - err_msg="slicing did not produce the expected frames") + assert_equal( + np.array(frames), + np.arange(u.trajectory.n_frames)[4:-2:4], + err_msg="slicing did not produce the expected frames", + ) def test_conect_bonds_conect(self, tmpdir, conect): assert_equal(len(conect.atoms), 1890) assert_equal(len(conect.bonds), 1922) - outfile = str(tmpdir.join('test-pdb-hbonds.pdb')) + outfile = str(tmpdir.join("test-pdb-hbonds.pdb")) conect.atoms.write(outfile, bonds="conect") u1 = mda.Universe(outfile, guess_bonds=True) @@ -660,7 +756,7 @@ def test_conect_bonds_conect(self, tmpdir, conect): assert_equal(len(u1.bonds), 1922) def test_conect_error(self): - with pytest.warns(UserWarning, match='CONECT records was corrupt'): + with pytest.warns(UserWarning, match="CONECT records was corrupt"): u = mda.Universe(CONECT_ERROR) def test_numconnections(self, multiverse): @@ -668,31 +764,34 @@ def test_numconnections(self, multiverse): # the bond list is sorted - so swaps in input pdb sequence should not # be a problem - desired = [[48, 365], - [99, 166], - [166, 99], - [249, 387], - [313, 331], - [331, 313, 332, 340], - [332, 331, 333, 338, 341], - [333, 332, 334, 342, 343], - [334, 333, 335, 344, 345], - [335, 334, 336, 337], - [336, 335], - [337, 335, 346, 347, 348], [338, 332, 339, 349], - [339, 338], - [340, 331], - [341, 332], - [342, 333], - [343, 333], - [344, 334], - [345, 334], - [346, 337], - [347, 337], - [348, 337], - [349, 338], - [365, 48], - [387, 249]] + desired = [ + [48, 365], + [99, 166], + [166, 99], + [249, 387], + [313, 331], + [331, 313, 332, 340], + [332, 331, 333, 338, 341], + [333, 332, 334, 342, 343], + [334, 333, 335, 344, 345], + [335, 334, 336, 337], + [336, 335], + [337, 335, 346, 347, 348], + [338, 332, 339, 349], + [339, 338], + [340, 331], + [341, 332], + [342, 333], + [343, 333], + [344, 334], + [345, 334], + [346, 337], + [347, 337], + [348, 337], + [349, 338], + [365, 48], + [387, 249], + ] def helper(atoms, bonds): """ @@ -711,15 +810,28 @@ def helper(atoms, bonds): atoms = sorted([a.index for a in atoms]) - conect = [([a, ] + sorted(con[a])) for a in atoms if a in con] + conect = [ + ( + [ + a, + ] + + sorted(con[a]) + ) + for a in atoms + if a in con + ] conect = [[a + 1 for a in c] for c in conect] return conect conect = helper(u.atoms, [b for b in u.bonds if not b.is_guessed]) - assert_equal(conect, desired, err_msg="The bond list does not match " - "the test reference; len(actual) is %d, len(desired) " - "is %d" % (len(u._topology.bonds.values), len(desired))) + assert_equal( + conect, + desired, + err_msg="The bond list does not match " + "the test reference; len(actual) is %d, len(desired) " + "is %d" % (len(u._topology.bonds.values), len(desired)), + ) def test_conect_bonds_all(tmpdir): @@ -728,7 +840,7 @@ def test_conect_bonds_all(tmpdir): assert_equal(len(conect.atoms), 1890) assert_equal(len(conect.bonds), 1922) - outfile = os.path.join(str(tmpdir), 'pdb-connect-bonds.pdb') + outfile = os.path.join(str(tmpdir), "pdb-connect-bonds.pdb") conect.atoms.write(outfile, bonds="all") u2 = mda.Universe(outfile, guess_bonds=True) @@ -743,7 +855,7 @@ def test_write_bonds_partial(tmpdir): # grab all atoms with bonds ag = (u.atoms.bonds.atom1 + u.atoms.bonds.atom2).unique - outfile = os.path.join(str(tmpdir), 'test.pdb') + outfile = os.path.join(str(tmpdir), "test.pdb") ag.write(outfile) u2 = mda.Universe(outfile) @@ -760,8 +872,8 @@ def test_write_bonds_with_100000_ag_index(tmpdir): ag = u.atoms ag.ids = ag.ids + 100000 - with pytest.warns(UserWarning, match='Atom with index'): - outfile = os.path.join(str(tmpdir), 'test.pdb') + with pytest.warns(UserWarning, match="Atom with index"): + outfile = os.path.join(str(tmpdir), "test.pdb") ag.write(outfile, reindex=False) @@ -788,13 +900,13 @@ def universe2(): @staticmethod @pytest.fixture def outfile(tmpdir): - return os.path.join(str(tmpdir), 'multiwriter-test-1.pdb') + return os.path.join(str(tmpdir), "multiwriter-test-1.pdb") def test_write_atomselection(self, multiverse, outfile): """Test if multiframe writer can write selected frames for an atomselection.""" u = multiverse - group = u.select_atoms('name CA', 'name C') + group = u.select_atoms("name CA", "name C") desired_group = 56 desired_frames = 6 pdb = mda.Writer(outfile, multiframe=True, start=12, step=2) @@ -802,15 +914,21 @@ def test_write_atomselection(self, multiverse, outfile): pdb.write(group) pdb.close() u2 = mda.Universe(outfile) - assert_equal(len(u2.atoms), desired_group, - err_msg="MultiPDBWriter trajectory written for an " - "AtomGroup contains %d atoms, it should contain %d" % ( - len(u2.atoms), desired_group)) + assert_equal( + len(u2.atoms), + desired_group, + err_msg="MultiPDBWriter trajectory written for an " + "AtomGroup contains %d atoms, it should contain %d" + % (len(u2.atoms), desired_group), + ) - assert_equal(len(u2.trajectory), desired_frames, - err_msg="MultiPDBWriter trajectory written for an " - "AtomGroup contains %d frames, it should have %d" % ( - len(u.trajectory), desired_frames)) + assert_equal( + len(u2.trajectory), + desired_frames, + err_msg="MultiPDBWriter trajectory written for an " + "AtomGroup contains %d frames, it should have %d" + % (len(u.trajectory), desired_frames), + ) def test_write_all_timesteps(self, multiverse, outfile): """ @@ -818,22 +936,28 @@ def test_write_all_timesteps(self, multiverse, outfile): for an atomselection) """ u = multiverse - group = u.select_atoms('name CA', 'name C') + group = u.select_atoms("name CA", "name C") desired_group = 56 desired_frames = 6 with mda.Writer(outfile, multiframe=True, start=12, step=2) as W: W.write_all_timesteps(group) u2 = mda.Universe(outfile) - assert_equal(len(u2.atoms), desired_group, - err_msg="MultiPDBWriter trajectory written for an " - "AtomGroup contains %d atoms, it should contain %d" % ( - len(u2.atoms), desired_group)) + assert_equal( + len(u2.atoms), + desired_group, + err_msg="MultiPDBWriter trajectory written for an " + "AtomGroup contains %d atoms, it should contain %d" + % (len(u2.atoms), desired_group), + ) - assert_equal(len(u2.trajectory), desired_frames, - err_msg="MultiPDBWriter trajectory written for an " - "AtomGroup contains %d frames, it should have %d" % ( - len(u.trajectory), desired_frames)) + assert_equal( + len(u2.trajectory), + desired_frames, + err_msg="MultiPDBWriter trajectory written for an " + "AtomGroup contains %d frames, it should have %d" + % (len(u.trajectory), desired_frames), + ) with open(outfile, "r") as f: lines = f.read() @@ -845,7 +969,7 @@ def test_write_loop(self, multiverse, outfile): for an atomselection) """ u = multiverse - group = u.select_atoms('name CA', 'name C') + group = u.select_atoms("name CA", "name C") desired_group = 56 desired_frames = 6 @@ -854,15 +978,21 @@ def test_write_loop(self, multiverse, outfile): W.write(group) u2 = mda.Universe(outfile) - assert_equal(len(u2.atoms), desired_group, - err_msg="MultiPDBWriter trajectory written for an " - f"AtomGroup contains {len(u2.atoms)} atoms, " - f"it should contain {desired_group}") + assert_equal( + len(u2.atoms), + desired_group, + err_msg="MultiPDBWriter trajectory written for an " + f"AtomGroup contains {len(u2.atoms)} atoms, " + f"it should contain {desired_group}", + ) - assert_equal(len(u2.trajectory), desired_frames, - err_msg="MultiPDBWriter trajectory written for an " - f"AtomGroup contains {len(u.trajectory)} " - f"frames, it should have {desired_frames}") + assert_equal( + len(u2.trajectory), + desired_frames, + err_msg="MultiPDBWriter trajectory written for an " + f"AtomGroup contains {len(u.trajectory)} " + f"frames, it should have {desired_frames}", + ) with open(outfile, "r") as f: lines = f.read() @@ -878,42 +1008,52 @@ def test_write_atoms(self, universe2, outfile): W.write(u.atoms) u0 = mda.Universe(outfile) - assert_equal(u0.trajectory.n_frames, - 2, - err_msg="The number of frames should be 2.") + assert_equal( + u0.trajectory.n_frames, + 2, + err_msg="The number of frames should be 2.", + ) class TestPDBReaderBig(RefAdK): prec = 6 @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return mda.Universe(PDB) def test_load_pdb(self, universe): U = universe - assert_equal(len(U.atoms), self.ref_n_atoms, - "load Universe from big PDB") - assert_equal(U.atoms.select_atoms('resid 150 and name HA2').atoms[0], - U.atoms[self.ref_E151HA2_index], "Atom selections") + assert_equal( + len(U.atoms), self.ref_n_atoms, "load Universe from big PDB" + ) + assert_equal( + U.atoms.select_atoms("resid 150 and name HA2").atoms[0], + U.atoms[self.ref_E151HA2_index], + "Atom selections", + ) def test_selection(self, universe): - na = universe.select_atoms('resname NA+') - assert_equal(len(na), self.ref_Na_sel_size, - "Atom selection of last atoms in file") + na = universe.select_atoms("resname NA+") + assert_equal( + len(na), + self.ref_Na_sel_size, + "Atom selection of last atoms in file", + ) def test_n_atoms(self, universe): - assert_equal(universe.trajectory.n_atoms, self.ref_n_atoms, - "wrong number of atoms") + assert_equal( + universe.trajectory.n_atoms, + self.ref_n_atoms, + "wrong number of atoms", + ) def test_n_frames(self, universe): - assert_equal(universe.trajectory.n_frames, 1, - "wrong number of frames") + assert_equal(universe.trajectory.n_frames, 1, "wrong number of frames") def test_time(self, universe): - assert_equal(universe.trajectory.time, 0.0, - "wrong time of the frame") + assert_equal(universe.trajectory.time, 0.0, "wrong time of the frame") def test_frame(self, universe): assert_equal(universe.trajectory.frame, 0, "wrong frame number") @@ -924,37 +1064,48 @@ def test_dt(self, universe): assert_equal(universe.trajectory.dt, 1.0) def test_coordinates(self, universe): - A10CA = universe.select_atoms('name CA')[10] - assert_almost_equal(A10CA.position, - self.ref_coordinates['A10CA'], - self.prec, - err_msg="wrong coordinates for A10:CA") + A10CA = universe.select_atoms("name CA")[10] + assert_almost_equal( + A10CA.position, + self.ref_coordinates["A10CA"], + self.prec, + err_msg="wrong coordinates for A10:CA", + ) def test_distances(self, universe): - NTERM = universe.select_atoms('name N')[0] - CTERM = universe.select_atoms('name C')[-1] + NTERM = universe.select_atoms("name N")[0] + CTERM = universe.select_atoms("name C")[-1] d = mda.lib.mdamath.norm(NTERM.position - CTERM.position) - assert_almost_equal(d, self.ref_distances['endtoend'], self.prec, - err_msg="wrong distance between M1:N and G214:C") + assert_almost_equal( + d, + self.ref_distances["endtoend"], + self.prec, + err_msg="wrong distance between M1:N and G214:C", + ) def test_selection(self, universe): - na = universe.select_atoms('resname NA+') - assert_equal(len(na), self.ref_Na_sel_size, - "Atom selection of last atoms in file") + na = universe.select_atoms("resname NA+") + assert_equal( + len(na), + self.ref_Na_sel_size, + "Atom selection of last atoms in file", + ) def test_unitcell(self, universe): assert_array_almost_equal( universe.dimensions, self.ref_unitcell, self.prec, - err_msg="unit cell dimensions (rhombic dodecahedron), issue 60") + err_msg="unit cell dimensions (rhombic dodecahedron), issue 60", + ) def test_volume(self, universe): assert_almost_equal( universe.coord.volume, self.ref_volume, 0, - err_msg="wrong volume for unitcell (rhombic dodecahedron)") + err_msg="wrong volume for unitcell (rhombic dodecahedron)", + ) def test_n_residues(self, universe): # Should have first 10000 residues, then another 1302 @@ -968,20 +1119,27 @@ def test_first_residue(self, universe): class TestPDBVaryingOccTmp: @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(): return mda.Universe(PDB_varying) def test_ts_data_keys(self, u): ts = u.trajectory.ts - assert 'occupancy' in ts.data - assert 'tempfactor' in ts.data - - @pytest.mark.parametrize('attr,ag_attr,vals', [ - ('occupancy', 'occupancies', [[1.0, 0.0], [0.9, 0.0], [0.0, 0.0]]), - ('tempfactor', 'tempfactors', [[23.44, 1.0], [24.44, 23.44], [1.0, 23.44]]), - ]) + assert "occupancy" in ts.data + assert "tempfactor" in ts.data + + @pytest.mark.parametrize( + "attr,ag_attr,vals", + [ + ("occupancy", "occupancies", [[1.0, 0.0], [0.9, 0.0], [0.0, 0.0]]), + ( + "tempfactor", + "tempfactors", + [[23.44, 1.0], [24.44, 23.44], [1.0, 23.44]], + ), + ], + ) def test_varying_attrs(self, u, attr, ag_attr, vals): u.trajectory[0] assert_allclose(u.trajectory.ts.data[attr], vals[0]) @@ -1002,8 +1160,9 @@ class TestIncompletePDB(object): Reads an incomplete (but still intelligible) PDB file """ + @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(): return mda.Universe(INC_PDB) @@ -1011,32 +1170,38 @@ def test_natoms(self, u): assert_equal(len(u.atoms), 3) def test_coords(self, u): - assert_array_almost_equal(u.atoms.positions, - np.array([[111.2519989, 98.3730011, - 98.18699646], - [111.20300293, 101.74199677, - 96.43000031], [107.60700226, - 102.96800232, - 96.31600189]], - dtype=np.float32)) + assert_array_almost_equal( + u.atoms.positions, + np.array( + [ + [111.2519989, 98.3730011, 98.18699646], + [111.20300293, 101.74199677, 96.43000031], + [107.60700226, 102.96800232, 96.31600189], + ], + dtype=np.float32, + ), + ) def test_dims(self, u): - assert_array_almost_equal(u.dimensions, - np.array([216.48899841, 216.48899841, - 216.48899841, 90., 90., 90.], - dtype=np.float32)) + assert_array_almost_equal( + u.dimensions, + np.array( + [216.48899841, 216.48899841, 216.48899841, 90.0, 90.0, 90.0], + dtype=np.float32, + ), + ) def test_names(self, u): - assert all(u.atoms.names == 'CA') + assert all(u.atoms.names == "CA") def test_residues(self, u): assert_equal(len(u.residues), 3) def test_resnames(self, u): assert_equal(len(u.atoms.resnames), 3) - assert 'VAL' in u.atoms.resnames - assert 'LYS' in u.atoms.resnames - assert 'PHE' in u.atoms.resnames + assert "VAL" in u.atoms.resnames + assert "LYS" in u.atoms.resnames + assert "PHE" in u.atoms.resnames def test_reading_trajectory(self, u): counter = 0 @@ -1047,8 +1212,9 @@ def test_reading_trajectory(self, u): class TestPDBXLSerial(object): """For Issue #446""" + @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(): return mda.Universe(PDB_xlserial) @@ -1066,6 +1232,7 @@ def test_serials(self, u): class TestPSF_CRDReader(_SingleFrameReader): __test__ = True + def setUp(self): self.universe = mda.Universe(PSF, CRD) self.prec = 5 # precision in CRD (at least we are writing %9.5f) @@ -1081,7 +1248,9 @@ def setUp(self): def test_uses_PDBReader(self): from MDAnalysis.coordinates.PDB import PDBReader - assert isinstance(self.universe.trajectory, PDBReader), "failed to choose PDBReader" + assert isinstance( + self.universe.trajectory, PDBReader + ), "failed to choose PDBReader" def test_write_occupancies(tmpdir): @@ -1089,7 +1258,7 @@ def test_write_occupancies(tmpdir): u = mda.Universe(PDB_small) u.atoms.occupancies = 0.12 - outfile = str(tmpdir.join('occ.pdb')) + outfile = str(tmpdir.join("occ.pdb")) u.atoms.write(outfile) @@ -1099,48 +1268,70 @@ def test_write_occupancies(tmpdir): class TestWriterAlignments(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def writtenstuff(self, tmpdir_factory): u = mda.Universe(ALIGN) - outfile = str(tmpdir_factory.mktemp('pdb').join('nucl.pdb')) + outfile = str(tmpdir_factory.mktemp("pdb").join("nucl.pdb")) u.atoms.write(outfile) with open(outfile) as fh: return fh.readlines() def test_atomname_alignment(self, writtenstuff): # Our PDBWriter adds some stuff up top, so line 1 happens at [9] - refs = ("ATOM 1 H5T", - "ATOM 2 CA ", - "ATOM 3 CA ", - "ATOM 4 H5''",) + refs = ( + "ATOM 1 H5T", + "ATOM 2 CA ", + "ATOM 3 CA ", + "ATOM 4 H5''", + ) for written, reference in zip(writtenstuff[9:], refs): assert_equal(written[:16], reference) def test_atomtype_alignment(self, writtenstuff): - result_line = ("ATOM 1 H5T GUA X 1 7.974 6.430 9.561" - " 1.00 0.00 RNAA \n") + result_line = ( + "ATOM 1 H5T GUA X 1 7.974 6.430 9.561" + " 1.00 0.00 RNAA \n" + ) assert_equal(writtenstuff[9], result_line) -@pytest.mark.parametrize('atom, refname', ((mda.coordinates.PDB.Pair('ASP', 'CA'), ' CA '), # Regular protein carbon alpha - (mda.coordinates.PDB.Pair('GLU', 'OE1'), ' OE1'), - (mda.coordinates.PDB.Pair('MSE', 'SE'), 'SE '), # Selenium like in 4D3L - (mda.coordinates.PDB.Pair('CA', 'CA'), 'CA '), # Calcium like in 4D3L - (mda.coordinates.PDB.Pair('HDD', 'FE'), 'FE '), # Iron from a heme like in 1GGE - (mda.coordinates.PDB.Pair('PLC', 'P'), ' P '), # Lipid phosphorus (1EIN) -)) +@pytest.mark.parametrize( + "atom, refname", + ( + ( + mda.coordinates.PDB.Pair("ASP", "CA"), + " CA ", + ), # Regular protein carbon alpha + (mda.coordinates.PDB.Pair("GLU", "OE1"), " OE1"), + ( + mda.coordinates.PDB.Pair("MSE", "SE"), + "SE ", + ), # Selenium like in 4D3L + (mda.coordinates.PDB.Pair("CA", "CA"), "CA "), # Calcium like in 4D3L + ( + mda.coordinates.PDB.Pair("HDD", "FE"), + "FE ", + ), # Iron from a heme like in 1GGE + ( + mda.coordinates.PDB.Pair("PLC", "P"), + " P ", + ), # Lipid phosphorus (1EIN) + ), +) def test_deduce_PDB_atom_name(atom, refname): # The Pair named tuple is used to mock atoms as we only need them to have a # ``resname`` and a ``name`` attribute. dummy_file = StringIO() - name = (mda.coordinates.PDB.PDBWriter(dummy_file, n_atoms=1) - ._deduce_PDB_atom_name(atom.name, atom.resname)) + name = mda.coordinates.PDB.PDBWriter( + dummy_file, n_atoms=1 + )._deduce_PDB_atom_name(atom.name, atom.resname) assert_equal(name, refname) -@pytest.mark.parametrize('pdbfile', [PDB_cm, PDB_cm_bz2, PDB_cm_gz, - PDB_mc, PDB_mc_bz2, PDB_mc_gz]) +@pytest.mark.parametrize( + "pdbfile", [PDB_cm, PDB_cm_bz2, PDB_cm_gz, PDB_mc, PDB_mc_bz2, PDB_mc_gz] +) class TestCrystModelOrder(object): """Check offset based reading of pdb files @@ -1163,6 +1354,7 @@ class TestCrystModelOrder(object): # ... # ENDMDL """ + boxsize = [80, 70, 60] position = [10, 20, 30] @@ -1174,7 +1366,8 @@ def test_order(self, pdbfile): u = mda.Universe(pdbfile) for ts, refbox, refpos in zip( - u.trajectory, self.boxsize, self.position): + u.trajectory, self.boxsize, self.position + ): assert_almost_equal(u.dimensions[0], refbox) assert_almost_equal(u.atoms[0].position[0], refpos) @@ -1207,7 +1400,7 @@ def test_write_pdb_zero_atoms(tmpdir): u = make_Universe(trajectory=True) with tmpdir.as_cwd(): - outfile = 'out.pdb' + outfile = "out.pdb" ag = u.atoms[:0] # empty ag @@ -1218,12 +1411,15 @@ def test_write_pdb_zero_atoms(tmpdir): def test_atom_not_match(tmpdir): # issue 1998 - outfile = str(tmpdir.mkdir("PDBReader").join('test_atom_not_match' + ".pdb")) + outfile = str( + tmpdir.mkdir("PDBReader").join("test_atom_not_match" + ".pdb") + ) u = mda.Universe(PSF, DCD) # select two groups of atoms protein = u.select_atoms("protein and name CA") atoms = u.select_atoms( - 'resid 1 or resid 10 or resid 100 or resid 1000 or resid 10000') + "resid 1 or resid 10 or resid 100 or resid 1000 or resid 10000" + ) with mda.Writer(outfile, multiframe=True, n_atoms=10) as pdb: # write these two groups of atoms to pdb # Then the n_atoms will not match @@ -1232,20 +1428,25 @@ def test_atom_not_match(tmpdir): reader = mda.coordinates.PDB.PDBReader(outfile) with pytest.raises(ValueError) as excinfo: reader._read_frame(1) - assert 'Inconsistency in file' in str(excinfo.value) + assert "Inconsistency in file" in str(excinfo.value) def test_partially_missing_cryst(): # issue 2252 - raw = open(INC_PDB, 'r').readlines() + raw = open(INC_PDB, "r").readlines() # mangle the cryst lines so that only box angles are left # this mimics '6edu' from PDB - raw = [line if not line.startswith('CRYST') - else line[:6] + ' ' * 28 + line[34:] - for line in raw] + raw = [ + ( + line + if not line.startswith("CRYST") + else line[:6] + " " * 28 + line[34:] + ) + for line in raw + ] with pytest.warns(UserWarning): - u = mda.Universe(StringIO('\n'.join(raw)), format='PDB') + u = mda.Universe(StringIO("\n".join(raw)), format="PDB") assert len(u.atoms) == 3 assert len(u.trajectory) == 2 @@ -1264,9 +1465,9 @@ def test_write_no_atoms_elements(dummy_universe_without_elements): element_symbols = [ line[76:78].strip() for line in content.splitlines() - if line[:6] == 'ATOM ' + if line[:6] == "ATOM " ] - expectation = ['', '', '', '', ''] + expectation = ["", "", "", "", ""] assert element_symbols == expectation @@ -1277,10 +1478,10 @@ def test_write_atom_elements(dummy_universe_without_elements): See `Issue 2423 `_. """ - elems = ['S', 'O', '', 'C', 'Na'] - expectation = ['S', 'O', '', 'C', 'NA'] + elems = ["S", "O", "", "C", "Na"] + expectation = ["S", "O", "", "C", "NA"] dummy_universe_with_elements = dummy_universe_without_elements - dummy_universe_with_elements.add_TopologyAttr('elements', elems) + dummy_universe_with_elements.add_TopologyAttr("elements", elems) destination = StringIO() with mda.coordinates.PDB.PDBWriter(destination) as writer: writer.write(dummy_universe_without_elements.atoms) @@ -1288,7 +1489,7 @@ def test_write_atom_elements(dummy_universe_without_elements): element_symbols = [ line[76:78].strip() for line in content.splitlines() - if line[:6] == 'ATOM ' + if line[:6] == "ATOM " ] assert element_symbols == expectation @@ -1300,7 +1501,7 @@ def test_elements_roundtrip(tmpdir): u = mda.Universe(CONECT) elements = u.atoms.elements - outfile = os.path.join(str(tmpdir), 'elements.pdb') + outfile = os.path.join(str(tmpdir), "elements.pdb") with mda.coordinates.PDB.PDBWriter(outfile) as writer: writer.write(u.atoms) @@ -1312,14 +1513,16 @@ def test_elements_roundtrip(tmpdir): def test_cryst_meaningless_warning(): # issue 2599 # FIXME: This message might change with Issue #2698 - with pytest.warns(UserWarning, match="Unit cell dimensions will be set to None."): + with pytest.warns( + UserWarning, match="Unit cell dimensions will be set to None." + ): mda.Universe(PDB_CRYOEM_BOX) def test_cryst_meaningless_select(): # issue 2599 u = mda.Universe(PDB_CRYOEM_BOX) - cur_sele = u.select_atoms('around 0.1 (resid 4 and name CA and segid A)') + cur_sele = u.select_atoms("around 0.1 (resid 4 and name CA and segid A)") assert cur_sele.n_atoms == 0 @@ -1329,7 +1532,7 @@ def test_charges_roundtrip(tmpdir): """ u = mda.Universe(PDB_charges) - outfile = os.path.join(str(tmpdir), 'newcharges.pdb') + outfile = os.path.join(str(tmpdir), "newcharges.pdb") with mda.coordinates.PDB.PDBWriter(outfile) as writer: writer.write(u.atoms) @@ -1345,7 +1548,7 @@ def test_charges_not_int(): mda.coordinates.PDB.PDBWriter._format_PDB_charges(arr) -@pytest.mark.parametrize('value', [99, -100]) +@pytest.mark.parametrize("value", [99, -100]) def test_charges_limit(value): # test for raising error when writing charges > 9 arr = np.array([0, 0, 0, value, 1, -1, 0], dtype=int) diff --git a/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py b/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py index 2caf44ecbbc..06bc62830b7 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py +++ b/testsuite/MDAnalysisTests/coordinates/test_pdbqt.py @@ -51,22 +51,25 @@ def test_chainID(self, universe): assert_equal(sel.n_atoms, 896, "failed to chainID segment B") def test_protein(self, universe): - sel = universe.select_atoms('protein') + sel = universe.select_atoms("protein") assert_equal(sel.n_atoms, 1805, "failed to select protein") - assert_equal(sel.atoms.ix, universe.atoms.ix, - "selected protein is not the same as auto-generated protein segment A+B") + assert_equal( + sel.atoms.ix, + universe.atoms.ix, + "selected protein is not the same as auto-generated protein segment A+B", + ) def test_backbone(self, universe): - sel = universe.select_atoms('backbone') + sel = universe.select_atoms("backbone") assert_equal(sel.n_atoms, 796) def test_neighborhood(self, universe): - '''test KDTree-based distance search around query atoms + """test KDTree-based distance search around query atoms Creates a KDTree of the protein and uses the coordinates of the atoms in the query pdb to create a list of protein residues within 4.0A of the query atoms. - ''' + """ query_universe = mda.Universe(PDBQT_querypdb) # PDB file protein = universe.select_atoms("protein") @@ -76,27 +79,38 @@ def test_neighborhood(self, universe): assert_equal(len(residue_neighbors), 80) def test_n_frames(self, universe): - assert_equal(universe.trajectory.n_frames, 1, - "wrong number of frames in pdb") + assert_equal( + universe.trajectory.n_frames, 1, "wrong number of frames in pdb" + ) def test_time(self, universe): assert_equal(universe.trajectory.time, 0.0, "wrong time of the frame") def test_frame(self, universe): - assert_equal(universe.trajectory.frame, 0, - "wrong frame number (0-based, should be 0 for single frame readers)") + assert_equal( + universe.trajectory.frame, + 0, + "wrong frame number (0-based, should be 0 for single frame readers)", + ) class TestPDBQTWriter(object): - reqd_attributes = ['names', 'types', 'resids', 'resnames', 'radii', - 'charges'] + reqd_attributes = [ + "names", + "types", + "resids", + "resnames", + "radii", + "charges", + ] @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir) + 'out.pdbqt' + return str(tmpdir) + "out.pdbqt" - @pytest.mark.parametrize('filename', - ['test.pdbqt', 'test.pdbqt.bz2', 'test.pdbqt.gz']) + @pytest.mark.parametrize( + "filename", ["test.pdbqt", "test.pdbqt.bz2", "test.pdbqt.gz"] + ) def test_roundtrip_writing_coords(self, filename, tmpdir): with tmpdir.as_cwd(): @@ -104,17 +118,20 @@ def test_roundtrip_writing_coords(self, filename, tmpdir): u.atoms.write(filename) u2 = mda.Universe(filename) - assert_equal(u2.atoms.positions, u.atoms.positions, - "Round trip does not preserve coordinates") + assert_equal( + u2.atoms.positions, + u.atoms.positions, + "Round trip does not preserve coordinates", + ) def test_roundtrip_formatting(self, outfile): # Compare formatting of first line u = mda.Universe(PDBQT_input) u.atoms.write(outfile) - with open(PDBQT_input, 'r') as inf: + with open(PDBQT_input, "r") as inf: l_ref = inf.readline().strip() - with open(outfile, 'r') as inf: + with open(outfile, "r") as inf: inf.readline() # header inf.readline() # cryst l_new = inf.readline().strip() @@ -127,7 +144,7 @@ def assert_writing_warns(u, outfile): def test_write_no_charges(self, outfile): attrs = self.reqd_attributes - attrs.remove('charges') + attrs.remove("charges") u = make_Universe(attrs, trajectory=True) self.assert_writing_warns(u, outfile) @@ -138,15 +155,15 @@ def test_write_no_charges(self, outfile): def test_write_no_chainids_with_segids(self, outfile): attrs = self.reqd_attributes - attrs.append('segids') + attrs.append("segids") u = make_Universe(attrs, trajectory=True) u.atoms.write(outfile) u2 = mda.Universe(outfile) # Should have used last letter of segid as chainid - assert all(u2.atoms[:25].segids == 'A') - assert all(u2.atoms[25:50].segids == 'B') + assert all(u2.atoms[:25].segids == "A") + assert all(u2.atoms[25:50].segids == "B") def test_get_writer(self, outfile): u = mda.Universe(PDBQT_input) diff --git a/testsuite/MDAnalysisTests/coordinates/test_pqr.py b/testsuite/MDAnalysisTests/coordinates/test_pqr.py index b3a54447247..fc69337ddbe 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_pqr.py +++ b/testsuite/MDAnalysisTests/coordinates/test_pqr.py @@ -37,6 +37,7 @@ class TestPQRReader(_SingleFrameReader): __test__ = True + def setUp(self): self.universe = mda.Universe(PQR) # 3 decimals in PDB spec @@ -45,27 +46,39 @@ def setUp(self): def test_total_charge(self): assert_almost_equal( - self.universe.atoms.total_charge(), self.ref_charmm_totalcharge, 3, - "Total charge (in CHARMM) does not match expected value.") + self.universe.atoms.total_charge(), + self.ref_charmm_totalcharge, + 3, + "Total charge (in CHARMM) does not match expected value.", + ) def test_hydrogenCharges(self): - assert_almost_equal(self.universe.select_atoms('name H').charges, - self.ref_charmm_Hcharges, 3, - "Charges for H atoms do not match.") + assert_almost_equal( + self.universe.select_atoms("name H").charges, + self.ref_charmm_Hcharges, + 3, + "Charges for H atoms do not match.", + ) # Note that the whole system gets the sysID 'SYSTEM' for the PQR file (when # read with a PSF it is 's4AKE') def test_ArgCACharges(self): - ag = self.universe.select_atoms('resname ARG and name CA') + ag = self.universe.select_atoms("resname ARG and name CA") assert_almost_equal( - ag.charges, self.ref_charmm_ArgCAcharges, - 3, "Charges for CA atoms in Arg residues do not match.") + ag.charges, + self.ref_charmm_ArgCAcharges, + 3, + "Charges for CA atoms in Arg residues do not match.", + ) def test_ProNCharges(self): - ag = self.universe.select_atoms('resname PRO and name N') + ag = self.universe.select_atoms("resname PRO and name N") assert_almost_equal( - ag.charges, self.ref_charmm_ProNcharges, 3, - "Charges for N atoms in Pro residues do not match.") + ag.charges, + self.ref_charmm_ProNcharges, + 3, + "Charges for N atoms in Pro residues do not match.", + ) def test_dimensions(self): # Issue #3327 - dimensions should always be set to None @@ -80,87 +93,116 @@ def universe(): prec = 3 - @pytest.mark.parametrize('filename', - ['test.pqr', 'test.pqr.bz2', 'test.pqr.gz']) + @pytest.mark.parametrize( + "filename", ["test.pqr", "test.pqr.bz2", "test.pqr.gz"] + ) def test_simple_writer_roundtrip(self, universe, filename, tmpdir): with tmpdir.as_cwd(): universe.atoms.write(filename) u2 = mda.Universe(filename) - assert_equal(universe.atoms.positions, - u2.atoms.positions) + assert_equal(universe.atoms.positions, u2.atoms.positions) def test_writer_noChainID(self, universe, tmpdir): - outfile = str(tmpdir.join('pqr-test.pqr')) + outfile = str(tmpdir.join("pqr-test.pqr")) - assert_equal(universe.segments.segids[0], 'SYSTEM') + assert_equal(universe.segments.segids[0], "SYSTEM") universe.atoms.write(outfile) u = mda.Universe(outfile) - assert_equal(u.segments.segids[0], 'SYSTEM') - assert_almost_equal(u.atoms.positions, - universe.atoms.positions, self.prec, - err_msg="Writing PQR file with PQRWriter does " - "not reproduce original coordinates") - assert_almost_equal(u.atoms.charges, universe.atoms.charges, - self.prec, err_msg="Writing PQR file with " - "PQRWriter does not reproduce original charges") - assert_almost_equal(u.atoms.radii, universe.atoms.radii, - self.prec, err_msg="Writing PQR file with " - "PQRWriter does not reproduce original radii") + assert_equal(u.segments.segids[0], "SYSTEM") + assert_almost_equal( + u.atoms.positions, + universe.atoms.positions, + self.prec, + err_msg="Writing PQR file with PQRWriter does " + "not reproduce original coordinates", + ) + assert_almost_equal( + u.atoms.charges, + universe.atoms.charges, + self.prec, + err_msg="Writing PQR file with " + "PQRWriter does not reproduce original charges", + ) + assert_almost_equal( + u.atoms.radii, + universe.atoms.radii, + self.prec, + err_msg="Writing PQR file with " + "PQRWriter does not reproduce original radii", + ) # 363 TODO: # Not sure if this should be a segid or chainID? # Topology system now allows for both of these def test_write_withChainID(self, universe, tmpdir): - outfile = str(tmpdir.join('pqr-test.pqr')) + outfile = str(tmpdir.join("pqr-test.pqr")) - universe.segments.segids = 'A' - assert_equal(universe.segments.segids[0], 'A') # sanity check + universe.segments.segids = "A" + assert_equal(universe.segments.segids[0], "A") # sanity check universe.atoms.write(outfile) u = mda.Universe(outfile) - assert_equal(u.segments.segids[0], 'A') - assert_almost_equal(u.atoms.positions, - universe.atoms.positions, self.prec, - err_msg="Writing PQR file with PQRWriter does " - "not reproduce original coordinates") - assert_almost_equal(u.atoms.charges, universe.atoms.charges, - self.prec, err_msg="Writing PQR file with " - "PQRWriter does not reproduce original charges") - assert_almost_equal(u.atoms.radii, universe.atoms.radii, - self.prec, err_msg="Writing PQR file with " - "PQRWriter does not reproduce original radii") + assert_equal(u.segments.segids[0], "A") + assert_almost_equal( + u.atoms.positions, + universe.atoms.positions, + self.prec, + err_msg="Writing PQR file with PQRWriter does " + "not reproduce original coordinates", + ) + assert_almost_equal( + u.atoms.charges, + universe.atoms.charges, + self.prec, + err_msg="Writing PQR file with " + "PQRWriter does not reproduce original charges", + ) + assert_almost_equal( + u.atoms.radii, + universe.atoms.radii, + self.prec, + err_msg="Writing PQR file with " + "PQRWriter does not reproduce original radii", + ) def test_timestep_not_modified_by_writer(self, universe, tmpdir): - outfile = str(tmpdir.join('pqr-test.pqr')) + outfile = str(tmpdir.join("pqr-test.pqr")) ts = universe.trajectory.ts x = ts.positions.copy() universe.atoms.write(outfile) - assert_equal(ts.positions, x, - err_msg="Positions in Timestep were modified by writer.") + assert_equal( + ts.positions, + x, + err_msg="Positions in Timestep were modified by writer.", + ) def test_total_charge(self, universe, tmpdir): - outfile = str(tmpdir.join('pqr-test.pqr')) + outfile = str(tmpdir.join("pqr-test.pqr")) universe.atoms.write(outfile) u = mda.Universe(outfile) assert_almost_equal( - u.atoms.total_charge(), self.ref_charmm_totalcharge, 3, - "Total charge (in CHARMM) does not match expected value.") + u.atoms.total_charge(), + self.ref_charmm_totalcharge, + 3, + "Total charge (in CHARMM) does not match expected value.", + ) + class TestPQRWriterMissingAttrs(object): # pqr requires names, resids, resnames, segids, radii, charges @staticmethod @pytest.fixture def reqd_attributes(): - return ['names', 'resids', 'resnames', 'radii', 'charges'] + return ["names", "resids", "resnames", "radii", "charges"] @staticmethod @pytest.fixture def outfile(tmpdir): - return str(tmpdir.join('pqr-writer-test.pqr')) + return str(tmpdir.join("pqr-writer-test.pqr")) def test_no_names_writing(self, reqd_attributes, outfile): attrs = reqd_attributes - attrs.remove('names') + attrs.remove("names") u = make_Universe(attrs, trajectory=True) with pytest.warns(UserWarning): @@ -168,11 +210,11 @@ def test_no_names_writing(self, reqd_attributes, outfile): u2 = mda.Universe(outfile) - assert all(u2.atoms.names == 'X') + assert all(u2.atoms.names == "X") def test_no_resnames_writing(self, reqd_attributes, outfile): attrs = reqd_attributes - attrs.remove('resnames') + attrs.remove("resnames") u = make_Universe(attrs, trajectory=True) with pytest.warns(UserWarning): @@ -180,11 +222,11 @@ def test_no_resnames_writing(self, reqd_attributes, outfile): u2 = mda.Universe(outfile) - assert all(u2.residues.resnames == 'UNK') + assert all(u2.residues.resnames == "UNK") def test_no_radii_writing(self, reqd_attributes, outfile): attrs = reqd_attributes - attrs.remove('radii') + attrs.remove("radii") u = make_Universe(attrs, trajectory=True) with pytest.warns(UserWarning): @@ -196,7 +238,7 @@ def test_no_radii_writing(self, reqd_attributes, outfile): def test_no_charges_writing(self, reqd_attributes, outfile): attrs = reqd_attributes - attrs.remove('charges') + attrs.remove("charges") u = make_Universe(attrs, trajectory=True) with pytest.warns(UserWarning): diff --git a/testsuite/MDAnalysisTests/coordinates/test_reader_api.py b/testsuite/MDAnalysisTests/coordinates/test_reader_api.py index c2062ab995f..4ae5c0f5c6c 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_reader_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_reader_api.py @@ -20,23 +20,24 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -import numpy as np from collections import OrderedDict + +import numpy as np +import pytest from MDAnalysis.coordinates.base import ( - Timestep, + ReaderBase, SingleFrameReaderBase, - ReaderBase + Timestep, ) -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_allclose, assert_equal -import pytest """ Isolate the API definitions of Readers independent of implementations """ class AmazingMultiFrameReader(ReaderBase): - format = 'AmazingMulti' + format = "AmazingMulti" def __init__(self, filename, **kwargs): self.filename = filename @@ -71,7 +72,7 @@ def _reopen(self): class AmazingReader(SingleFrameReaderBase): - format = 'Amazing' + format = "Amazing" # have to hack this in to get the base class to "work" def _read_first_frame(self): @@ -86,7 +87,7 @@ class _TestReader(object): @pytest.fixture() def reader(self): - return self.readerclass('test.txt') + return self.readerclass("test.txt") @pytest.fixture() def ts(self, reader): @@ -94,10 +95,17 @@ def ts(self, reader): def test_required_attributes(self, reader): """Test that Reader has the required attributes""" - for attr in ['filename', 'n_atoms', 'n_frames', 'ts', - 'units', 'format']: - assert_equal(hasattr(reader, attr), True, - "Missing attr: {0}".format(attr)) + for attr in [ + "filename", + "n_atoms", + "n_frames", + "ts", + "units", + "format", + ]: + assert_equal( + hasattr(reader, attr), True, "Missing attr: {0}".format(attr) + ) def test_iter(self, reader): l = [ts for ts in reader] @@ -105,7 +113,7 @@ def test_iter(self, reader): assert_equal(len(l), self.n_frames) def test_close(self): - sfr = self.readerclass('text.txt') + sfr = self.readerclass("text.txt") ret = sfr.close() # Check that method works? @@ -118,7 +126,7 @@ def test_rewind(self, reader): assert_equal(reader.ts.frame, 0) def test_context(self): - with self.readerclass('text.txt') as sfr: + with self.readerclass("text.txt") as sfr: l = sfr.ts.frame assert_equal(l, 0) @@ -133,16 +141,17 @@ def test_raises_StopIteration(self, reader): with pytest.raises(StopIteration): next(reader) - @pytest.mark.parametrize('order', ['turnip', 'abc']) + @pytest.mark.parametrize("order", ["turnip", "abc"]) def test_timeseries_raises_unknown_order_key(self, reader, order): with pytest.raises(ValueError, match="Unrecognized order key"): reader.timeseries(order=order) - @pytest.mark.parametrize('order', ['faac', 'affc', 'afcc', '']) + @pytest.mark.parametrize("order", ["faac", "affc", "afcc", ""]) def test_timeseries_raises_incorrect_order_key(self, reader, order): with pytest.raises(ValueError, match="Repeated or missing keys"): reader.timeseries(order=order) + class _Multi(_TestReader): n_frames = 10 n_atoms = 10 @@ -154,34 +163,37 @@ class TestMultiFrameReader(_Multi): __test__ = True - @pytest.mark.parametrize('start, stop, step', [ - (None, None, None), # blank slice - (None, 5, None), # set end point - (2, None, None), # set start point - (2, 5, None), # start & end - (None, None, 2), # set skip - (None, None, -1), # backwards skip - (None, -1, -1), - (10, 0, -1), - (0, 10, 1), - (0, 10, 2), - (None, 20, None), # end beyond real end - (None, 20, 2), # with skip - (0, 5, 2), - (5, None, -1), - (None, 5, -1), - (100, 10, 1), - (-10, None, 1), - (100, None, -1), # beyond real end - (100, 5, -20), - (5, 1, 1), # Stop less than start - (1, 5, -1), # Stop less than start - (-100, None, None), - (100, None, None), # Outside of range of trajectory - (-2, 10, -2), - (0, 0, 1), # empty - (10, 1, 2), # empty - ]) + @pytest.mark.parametrize( + "start, stop, step", + [ + (None, None, None), # blank slice + (None, 5, None), # set end point + (2, None, None), # set start point + (2, 5, None), # start & end + (None, None, 2), # set skip + (None, None, -1), # backwards skip + (None, -1, -1), + (10, 0, -1), + (0, 10, 1), + (0, 10, 2), + (None, 20, None), # end beyond real end + (None, 20, 2), # with skip + (0, 5, 2), + (5, None, -1), + (None, 5, -1), + (100, 10, 1), + (-10, None, 1), + (100, None, -1), # beyond real end + (100, 5, -20), + (5, 1, 1), # Stop less than start + (1, 5, -1), # Stop less than start + (-100, None, None), + (100, None, None), # Outside of range of trajectory + (-2, 10, -2), + (0, 0, 1), # empty + (10, 1, 2), # empty + ], + ) def test_slice(self, start, stop, step, reader): """Compare the slice applied to trajectory, to slice of list""" res = [ts.frame for ts in reader[start:stop:step]] @@ -203,16 +215,30 @@ def sl(): with pytest.raises(TypeError): sl() - @pytest.mark.parametrize('slice_cls', [list, np.array]) - @pytest.mark.parametrize('sl', [ - [0, 1, 4, 5], - [5, 1, 6, 2, 7, 3, 8], - [0, 1, 1, 1, 0, 0, 2, 3, 4], - [True, False, True, False, True, False, True, False, True, False], - [True, True, False, False, True, True, False, True, False, True], - [True, True, True, True, True, True, True, True, True, True], - [False, False, False, False, False, False, False, False, False, False], - ]) + @pytest.mark.parametrize("slice_cls", [list, np.array]) + @pytest.mark.parametrize( + "sl", + [ + [0, 1, 4, 5], + [5, 1, 6, 2, 7, 3, 8], + [0, 1, 1, 1, 0, 0, 2, 3, 4], + [True, False, True, False, True, False, True, False, True, False], + [True, True, False, False, True, True, False, True, False, True], + [True, True, True, True, True, True, True, True, True, True], + [ + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + ], + ], + ) def test_getitem(self, slice_cls, sl, reader): sl = slice_cls(sl) res = [ts.frame for ts in reader[sl]] @@ -222,23 +248,26 @@ def test_getitem(self, slice_cls, sl, reader): assert_equal(res, ref) - @pytest.mark.parametrize('sl', [ - [0, 1, 2, 3], # ordered list of indices without duplicates - [1, 3, 4, 2, 9], # disordered list of indices without duplicates - [0, 1, 1, 2, 2, 2], # ordered list with duplicates - [-1, -2, 3, -1, 0], # disordered list with duplicates - [True, ] * 10, - [False, ] * 10, - [True, False, ] * 5, - slice(None, None, None), - slice(0, 10, 1), - slice(None, None, -1), - slice(10, 0, -1), - slice(2, 7, 2), - slice(7, 2, -2), - slice(7, 2, 1), # empty - slice(0, 0, 1), # empty - ]) + @pytest.mark.parametrize( + "sl", + [ + [0, 1, 2, 3], # ordered list of indices without duplicates + [1, 3, 4, 2, 9], # disordered list of indices without duplicates + [0, 1, 1, 2, 2, 2], # ordered list with duplicates + [-1, -2, 3, -1, 0], # disordered list with duplicates + [True] * 10, + [False] * 10, + [True, False] * 5, + slice(None, None, None), + slice(0, 10, 1), + slice(None, None, -1), + slice(10, 0, -1), + slice(2, 7, 2), + slice(7, 2, -2), + slice(7, 2, 1), # empty + slice(0, 0, 1), # empty + ], + ) def test_getitem_len(self, sl, reader): traj_iterable = reader[sl] if not isinstance(sl, slice): @@ -246,31 +275,37 @@ def test_getitem_len(self, sl, reader): ref = self.reference[sl] assert len(traj_iterable) == len(ref) - @pytest.mark.parametrize('iter_type', (list, np.array)) + @pytest.mark.parametrize("iter_type", (list, np.array)) def test_getitem_len_empty(self, reader, iter_type): # Indexing a numpy array with an empty array tends to break. traj_iterable = reader[iter_type([])] assert len(traj_iterable) == 0 # All the sl1 slice must be 5 frames long so that the sl2 can be a mask - @pytest.mark.parametrize('sl1', [ - [0, 1, 2, 3, 4], - [1, 1, 1, 1, 1], - [True, False, ] * 5, - slice(None, None, 2), - slice(None, None, -2), - ]) - @pytest.mark.parametrize('sl2', [ - [0, -1, 2], - [-1,-1, -1], - [True, False, True, True, False], - np.array([True, False, True, True, False]), - slice(None, None, None), - slice(None, 3, None), - slice(4, 0, -1), - slice(None, None, -1), - slice(None, None, 2), - ]) + @pytest.mark.parametrize( + "sl1", + [ + [0, 1, 2, 3, 4], + [1, 1, 1, 1, 1], + [True, False] * 5, + slice(None, None, 2), + slice(None, None, -2), + ], + ) + @pytest.mark.parametrize( + "sl2", + [ + [0, -1, 2], + [-1, -1, -1], + [True, False, True, True, False], + np.array([True, False, True, True, False]), + slice(None, None, None), + slice(None, 3, None), + slice(4, 0, -1), + slice(None, None, -1), + slice(None, None, 2), + ], + ) def test_double_getitem(self, sl1, sl2, reader): traj_iterable = reader[sl1][sl2] # Old versions of numpy do not behave the same when indexing with a @@ -285,15 +320,18 @@ def test_double_getitem(self, sl1, sl2, reader): assert_equal(res, ref) assert len(traj_iterable) == len(ref) - @pytest.mark.parametrize('sl1', [ - [0, 1, 2, 3, 4], - [1, 1, 1, 1, 1], - [True, False, ] * 5, - slice(None, None, 2), - slice(None, None, -2), - slice(None, None, None), - ]) - @pytest.mark.parametrize('idx2', [0, 2, 4, -1, -2, -4]) + @pytest.mark.parametrize( + "sl1", + [ + [0, 1, 2, 3, 4], + [1, 1, 1, 1, 1], + [True, False] * 5, + slice(None, None, 2), + slice(None, None, -2), + slice(None, None, None), + ], + ) + @pytest.mark.parametrize("idx2", [0, 2, 4, -1, -2, -4]) def test_double_getitem_int(self, sl1, idx2, reader): ts = reader[sl1][idx2] # Old versions of numpy do not behave the same when indexing with a @@ -305,7 +343,7 @@ def test_double_getitem_int(self, sl1, idx2, reader): def test_list_TE(self, reader): def sl(): - return list(reader[[0, 'a', 5, 6]]) + return list(reader[[0, "a", 5, 6]]) with pytest.raises(TypeError): sl() @@ -317,14 +355,17 @@ def sl(): with pytest.raises(TypeError): sl() - @pytest.mark.parametrize('sl1', [ - [0, 1, 2, 3, 4], - [1, 1, 1, 1, 1], - [True, False, ] * 5, - slice(None, None, 2), - slice(None, None, -2), - ]) - @pytest.mark.parametrize('idx2', [5, -6]) + @pytest.mark.parametrize( + "sl1", + [ + [0, 1, 2, 3, 4], + [1, 1, 1, 1, 1], + [True, False] * 5, + slice(None, None, 2), + slice(None, None, -2), + ], + ) + @pytest.mark.parametrize("idx2", [5, -6]) def test_getitem_IE(self, sl1, idx2, reader): partial_reader = reader[sl1] with pytest.raises(IndexError): @@ -339,6 +380,7 @@ class _Single(_TestReader): class TestSingleFrameReader(_Single): __test__ = True + def test_next(self, reader): with pytest.raises(StopIteration): reader.next() diff --git a/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py b/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py index a12934199d1..1b2bc468b3e 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_timestep_api.py @@ -28,18 +28,45 @@ """ import itertools import numpy as np -from numpy.testing import assert_equal, assert_allclose, assert_array_almost_equal +from numpy.testing import ( + assert_equal, + assert_allclose, + assert_array_almost_equal, +) from MDAnalysis.lib.mdamath import triclinic_vectors import MDAnalysis as mda -from MDAnalysisTests.datafiles import (PSF, XYZ_five, INPCRD, DCD, DLP_CONFIG, - DLP_HISTORY, DMS, GMS_ASYMOPT, GRO, XTC, - TRR, LAMMPSdata, LAMMPSdata2, - LAMMPSdcd2, mol2_molecules, PDB_small, - PDBQT_input, PQR, PRM, TRJ, PRMncdf, - NCDF, TRZ_psf, TRZ) - -from MDAnalysisTests.coordinates.base import assert_timestep_equal, assert_timestep_almost_equal +from MDAnalysisTests.datafiles import ( + PSF, + XYZ_five, + INPCRD, + DCD, + DLP_CONFIG, + DLP_HISTORY, + DMS, + GMS_ASYMOPT, + GRO, + XTC, + TRR, + LAMMPSdata, + LAMMPSdata2, + LAMMPSdcd2, + mol2_molecules, + PDB_small, + PDBQT_input, + PQR, + PRM, + TRJ, + PRMncdf, + NCDF, + TRZ_psf, + TRZ, +) + +from MDAnalysisTests.coordinates.base import ( + assert_timestep_equal, + assert_timestep_almost_equal, +) from MDAnalysis.coordinates.timestep import Timestep import pytest @@ -52,6 +79,7 @@ class TestTimestep(object): These test the Timestep independent of the Reader which it comes into contact with. Failures here are the Timesteps fault. """ + # define the class made in test Timestep = Timestep name = "base" # for error messages only @@ -63,9 +91,9 @@ class TestTimestep(object): # If you can set box, what the underlying unitcell should be # if dimensions are: - newbox = np.array([10., 11., 12., 90., 90., 90.]) - unitcell = np.array([10., 11., 12., 90., 90., 90.]) - ref_volume = 1320. # what the volume is after setting newbox + newbox = np.array([10.0, 11.0, 12.0, 90.0, 90.0, 90.0]) + unitcell = np.array([10.0, 11.0, 12.0, 90.0, 90.0, 90.0]) + ref_volume = 1320.0 # what the volume is after setting newbox uni_args = None @pytest.fixture() @@ -87,7 +115,6 @@ def test_getitem_neg_IE(self, ts): with pytest.raises(IndexError): ts.__getitem__(-(self.size + 1)) - def test_getitem_pos_IE(self, ts): with pytest.raises(IndexError): ts.__getitem__((self.size + 1)) @@ -107,7 +134,7 @@ def test_getitem_ndarray(self, ts): def test_getitem_TE(self, ts): with pytest.raises(TypeError): - ts.__getitem__('string') + ts.__getitem__("string") def test_len(self, ts): assert_equal(len(ts), self.size) @@ -121,16 +148,15 @@ 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)) + assert "with unit cell dimensions" in repr(ts) def test_repr_no_box(self, ts): ts.dimensions = None - assert("with unit cell dimensions" not in repr(ts)) + assert "with unit cell dimensions" not in repr(ts) def test_default_dtype_npf32(self, ts): assert_equal(ts.dtype, np.float32) - # Dimensions has 2 possible cases # Timestep doesn't do dimensions, # returns None for .dimension and 0 for .volume @@ -138,7 +164,7 @@ def test_default_dtype_npf32(self, ts): def test_dimensions(self, ts): assert_allclose(ts.dimensions, self.newbox) - @pytest.mark.parametrize('dtype', (int, np.float32, np.float64)) + @pytest.mark.parametrize("dtype", (int, np.float32, np.float64)) def test_dimensions_set_box(self, ts, dtype): ts.dimensions = self.newbox.astype(dtype) assert ts.dimensions.dtype == np.float32 @@ -149,21 +175,22 @@ def test_volume(self, ts): assert_equal(ts.volume, self.ref_volume) def test_triclinic_vectors(self, ts): - assert_allclose(ts.triclinic_dimensions, - triclinic_vectors(ts.dimensions)) + assert_allclose( + ts.triclinic_dimensions, triclinic_vectors(ts.dimensions) + ) def test_set_triclinic_vectors(self, ts): ref_vec = triclinic_vectors(self.newbox) ts.triclinic_dimensions = ref_vec assert_equal(ts.dimensions, self.newbox) - def test_set_dimensions_None(self,ts): + def test_set_dimensions_None(self, ts): ts.dimensions = None - assert(not ts._unitcell.any()) + assert not ts._unitcell.any() - def test_set_triclinic_dimensions_None(self,ts): + def test_set_triclinic_dimensions_None(self, ts): ts.triclinic_dimensions = None - assert(not ts._unitcell.any()) + assert not ts._unitcell.any() def test_coordinate_getter_shortcuts(self, ts): """testing that reading _x, _y, and _z works as expected @@ -174,7 +201,7 @@ def test_coordinate_getter_shortcuts(self, ts): def test_coordinate_setter_shortcuts(self, ts): # Check that _x _y and _z are read only - for coordinate in ('_x', '_y', '_z'): + for coordinate in ("_x", "_y", "_z"): random_positions = np.arange(self.size).astype(np.float32) with pytest.raises(AttributeError): setattr(ts, coordinate, random_positions) @@ -186,22 +213,22 @@ def test_n_atoms(self, ts): def test_n_atoms_readonly(self, ts): with pytest.raises(AttributeError): - ts.__setattr__('n_atoms', 20) + ts.__setattr__("n_atoms", 20) def test_n_atoms_presence(self, ts): - assert_equal(hasattr(ts, 'n_atoms'), True) + assert_equal(hasattr(ts, "n_atoms"), True) def test_unitcell_presence(self, ts): - assert_equal(hasattr(ts, 'dimensions'), True) + assert_equal(hasattr(ts, "dimensions"), True) def test_data_presence(self, ts): - assert_equal(hasattr(ts, 'data'), True) + assert_equal(hasattr(ts, "data"), True) assert_equal(isinstance(ts.data, dict), True) def test_allocate_velocities(self, ts): assert_equal(ts.has_velocities, False) with pytest.raises(mda.NoDataError): - getattr(ts, 'velocities') + getattr(ts, "velocities") ts.has_velocities = True assert_equal(ts.has_velocities, True) @@ -210,7 +237,7 @@ def test_allocate_velocities(self, ts): def test_allocate_forces(self, ts): assert_equal(ts.has_forces, False) with pytest.raises(mda.NoDataError): - getattr(ts, 'forces') + getattr(ts, "forces") ts.has_forces = True assert_equal(ts.has_forces, True) @@ -224,7 +251,7 @@ def test_velocities_remove(self): ts.has_velocities = False assert_equal(ts.has_velocities, False) with pytest.raises(mda.NoDataError): - getattr(ts, 'velocities') + getattr(ts, "velocities") def test_forces_remove(self): ts = self.Timestep(10, forces=True) @@ -234,7 +261,7 @@ def test_forces_remove(self): ts.has_forces = False assert_equal(ts.has_forces, False) with pytest.raises(mda.NoDataError): - getattr(ts, 'forces') + getattr(ts, "forces") def test_check_ts(self): with pytest.raises(ValueError): @@ -249,9 +276,9 @@ def _from_coords(self, p, v, f): return ts - @pytest.mark.parametrize('p, v, f', filter(any, - itertools.product([True, False], - repeat=3))) + @pytest.mark.parametrize( + "p, v, f", filter(any, itertools.product([True, False], repeat=3)) + ) def test_from_coordinates(self, p, v, f): ts = self._from_coords(p, v, f) @@ -259,17 +286,17 @@ def test_from_coordinates(self, p, v, f): assert_array_almost_equal(ts.positions, self.refpos) else: with pytest.raises(mda.NoDataError): - getattr(ts, 'positions') + getattr(ts, "positions") if v: assert_array_almost_equal(ts.velocities, self.refvel) else: with pytest.raises(mda.NoDataError): - getattr(ts, 'velocities') + getattr(ts, "velocities") if f: assert_array_almost_equal(ts.forces, self.reffor) else: with pytest.raises(mda.NoDataError): - getattr(ts, 'forces') + getattr(ts, "forces") def test_from_coordinates_mismatch(self): velo = self.refvel[:2] @@ -286,29 +313,29 @@ def test_supply_dt(self): # Check that this gets stored in data properly ts = self.Timestep(20, dt=0.04) - assert_equal(ts.data['dt'], 0.04) + assert_equal(ts.data["dt"], 0.04) assert_equal(ts.dt, 0.04) def test_redefine_dt(self): ts = self.Timestep(20, dt=0.04) - assert_equal(ts.data['dt'], 0.04) + assert_equal(ts.data["dt"], 0.04) assert_equal(ts.dt, 0.04) ts.dt = refdt = 0.46 - assert_equal(ts.data['dt'], refdt) + assert_equal(ts.data["dt"], refdt) assert_equal(ts.dt, refdt) def test_delete_dt(self): ts = self.Timestep(20, dt=0.04) - assert_equal(ts.data['dt'], 0.04) + assert_equal(ts.data["dt"], 0.04) assert_equal(ts.dt, 0.04) del ts.dt - assert_equal('dt' in ts.data, False) + assert_equal("dt" in ts.data, False) assert_equal(ts.dt, 1.0) # default value def test_supply_time_offset(self): ts = self.Timestep(20, time_offset=100.0) - assert_equal(ts.data['time_offset'], 100.0) + assert_equal(ts.data["time_offset"], 100.0) def test_time(self): ts = self.Timestep(20) @@ -379,8 +406,9 @@ def _check_copy(self, name, ref_ts): """Check basic copy""" ts2 = ref_ts.copy() - err_msg = ("Timestep copy failed for format {form}" - " on attribute {att}") + err_msg = ( + "Timestep copy failed for format {form}" " on attribute {att}" + ) # eq method checks: # - frame @@ -389,8 +417,9 @@ def _check_copy(self, name, ref_ts): assert ref_ts == ts2 if not ref_ts.dimensions is None: - assert_array_almost_equal(ref_ts.dimensions, ts2.dimensions, - decimal=4) + assert_array_almost_equal( + ref_ts.dimensions, ts2.dimensions, decimal=4 + ) else: assert ref_ts.dimensions == ts2.dimensions @@ -398,8 +427,7 @@ def _check_copy(self, name, ref_ts): for d in ref_ts.data: assert d in ts2.data if isinstance(ref_ts.data[d], np.ndarray): - assert_array_almost_equal( - ref_ts.data[d], ts2.data[d]) + assert_array_almost_equal(ref_ts.data[d], ts2.data[d]) else: assert ref_ts.data[d] == ts2.data[d] @@ -433,10 +461,27 @@ def _check_copy_slice_slice(self, name, ts): self._check_slice(ts, ts2, sl) def _check_npint_slice(self, name, ts): - for integers in [np.byte, np.short, np.intc, np.int_, np.longlong, - np.intp, np.int8, np.int16, np.int32, np.int64, - np.ubyte, np.ushort, np.uintc, np.ulonglong, - np.uintp, np.uint8, np.uint16, np.uint32, np.uint64]: + for integers in [ + np.byte, + np.short, + np.intc, + np.int_, + np.longlong, + np.intp, + np.int8, + np.int16, + np.int32, + np.int64, + np.ubyte, + np.ushort, + np.uintc, + np.ulonglong, + np.uintp, + np.uint8, + np.uint16, + np.uint32, + np.uint64, + ]: sl = slice(1, 2, 1) ts2 = ts.copy_slice(slice(integers(1), integers(2), integers(1))) self._check_slice(ts, ts2, sl) @@ -449,13 +494,16 @@ def _check_slice(self, ts1, ts2, sl): if ts1.has_forces: assert_array_almost_equal(ts1.forces[sl], ts2.forces) - @pytest.mark.parametrize('func', [ - _check_copy, - _check_independent, - _check_copy_slice_indices, - _check_copy_slice_slice, - _check_npint_slice - ]) + @pytest.mark.parametrize( + "func", + [ + _check_copy, + _check_independent, + _check_copy_slice_indices, + _check_copy_slice_slice, + _check_npint_slice, + ], + ) def test_copy(self, func, ts): if self.uni_args is None: return @@ -463,24 +511,28 @@ def test_copy(self, func, ts): ts = u.trajectory.ts func(self, self.name, ts) - @pytest.fixture(params=filter(any, - itertools.product([True, False], repeat=3))) + @pytest.fixture( + params=filter(any, itertools.product([True, False], repeat=3)) + ) def some_ts(self, request): p, v, f = request.param return self._from_coords(p, v, f) - @pytest.mark.parametrize('func', [ - _check_copy, - _check_independent, - _check_copy_slice_indices, - _check_copy_slice_slice, - _check_npint_slice - ]) + @pytest.mark.parametrize( + "func", + [ + _check_copy, + _check_independent, + _check_copy_slice_indices, + _check_copy_slice_slice, + _check_npint_slice, + ], + ) def test_copy_slice(self, func, some_ts): func(self, self.name, some_ts) def test_bad_slice(self, some_ts): - sl = ['this', 'is', 'silly'] + sl = ["this", "is", "silly"] with pytest.raises(TypeError): some_ts.copy_slice(sl) @@ -494,18 +546,12 @@ def _get_pos(self): # Get generic reference positions return np.arange(30).reshape(10, 3) * 1.234 - @pytest.mark.parametrize('p, v, f', filter(any, - itertools.product([True, False], - repeat=3))) + @pytest.mark.parametrize( + "p, v, f", filter(any, itertools.product([True, False], repeat=3)) + ) def test_check_equal(self, p, v, f): - ts1 = self.Timestep(self.size, - positions=p, - velocities=v, - forces=f) - ts2 = self.Timestep(self.size, - positions=p, - velocities=v, - forces=f) + ts1 = self.Timestep(self.size, positions=p, velocities=v, forces=f) + ts2 = self.Timestep(self.size, positions=p, velocities=v, forces=f) if p: ts1.positions = self.refpos.copy() ts2.positions = self.refpos.copy() @@ -622,12 +668,12 @@ def test_check_wrong_forces_equality(self): assert ts1 != ts2 assert ts2 != ts1 - @pytest.mark.parametrize('dim1', [None, [2., 2., 2., 90., 90., 90.]]) + @pytest.mark.parametrize("dim1", [None, [2.0, 2.0, 2.0, 90.0, 90.0, 90.0]]) def test_dims_mismatch_inequality(self, dim1): ts1 = self.Timestep(self.size) ts1.dimensions = dim1 ts2 = self.Timestep(self.size) - ts2.dimensions = [1., 1., 1., 90., 90., 90.] + ts2.dimensions = [1.0, 1.0, 1.0, 90.0, 90.0, 90.0] assert ts1 != ts2 assert ts2 != ts1 @@ -637,6 +683,7 @@ def test_dims_mismatch_inequality(self, dim1): # These tests are all included in BaseReaderTest # Once Readers use that TestClass, delete this one + class TestBaseTimestepInterface(object): """Test the Timesteps created by Readers @@ -647,32 +694,41 @@ class TestBaseTimestepInterface(object): See Issue #250 for discussion """ - @pytest.fixture(params=( - (XYZ_five, INPCRD, None, None), - (PSF, DCD, None, None), - (DLP_CONFIG, None, 'CONFIG', None), - (DLP_HISTORY, None, 'HISTORY', None), - (DMS, None, None, None), - (GRO, None, None, None), - (XYZ_five, INPCRD, None, None), - (LAMMPSdata, None, None, None), - (mol2_molecules, None, None, None), - (PDB_small, None, None, None), - (PQR, None, None, None), - (PDBQT_input, None, None, None), - (PRM, TRJ, None, None), - (GRO, XTC, None, None), - (TRZ_psf, TRZ, None, None), - (GRO, TRR, None, None), - (GMS_ASYMOPT, GMS_ASYMOPT, 'GMS', 'GMS'), - (LAMMPSdata2, LAMMPSdcd2, 'LAMMPS', 'DATA'), - (PRMncdf, NCDF, None, None), - )) + + @pytest.fixture( + params=( + (XYZ_five, INPCRD, None, None), + (PSF, DCD, None, None), + (DLP_CONFIG, None, "CONFIG", None), + (DLP_HISTORY, None, "HISTORY", None), + (DMS, None, None, None), + (GRO, None, None, None), + (XYZ_five, INPCRD, None, None), + (LAMMPSdata, None, None, None), + (mol2_molecules, None, None, None), + (PDB_small, None, None, None), + (PQR, None, None, None), + (PDBQT_input, None, None, None), + (PRM, TRJ, None, None), + (GRO, XTC, None, None), + (TRZ_psf, TRZ, None, None), + (GRO, TRR, None, None), + (GMS_ASYMOPT, GMS_ASYMOPT, "GMS", "GMS"), + (LAMMPSdata2, LAMMPSdcd2, "LAMMPS", "DATA"), + (PRMncdf, NCDF, None, None), + ) + ) def universe(self, request): - topology, trajectory, trajectory_format, topology_format = request.param + topology, trajectory, trajectory_format, topology_format = ( + request.param + ) if trajectory_format is not None and topology_format is not None: - return mda.Universe(topology, trajectory, format=trajectory_format, - topology_format=topology_format) + return mda.Universe( + topology, + trajectory, + format=trajectory_format, + topology_format=topology_format, + ) if trajectory is not None: return mda.Universe(topology, trajectory) @@ -686,15 +742,18 @@ def test_dt(self, universe): assert_equal(universe.trajectory.dt, universe.trajectory.ts.dt) -@pytest.mark.parametrize('uni', [ - [(PSF, DCD), {}], # base Timestep - [(DLP_CONFIG,), {'format': 'CONFIG'}], # DLPoly - [(DMS,), {}], # DMS - [(GRO,), {}], # GRO - [(np.zeros((1, 30, 3)),), {}], # memory - [(PRM, TRJ), {}], # TRJ - [(TRZ_psf, TRZ), {}], # TRZ -]) +@pytest.mark.parametrize( + "uni", + [ + [(PSF, DCD), {}], # base Timestep + [(DLP_CONFIG,), {"format": "CONFIG"}], # DLPoly + [(DMS,), {}], # DMS + [(GRO,), {}], # GRO + [(np.zeros((1, 30, 3)),), {}], # memory + [(PRM, TRJ), {}], # TRJ + [(TRZ_psf, TRZ), {}], # TRZ + ], +) def test_atomgroup_dims_access(uni): uni_args, uni_kwargs = uni # check that AtomGroup.dimensions always returns a copy diff --git a/testsuite/MDAnalysisTests/coordinates/test_tng.py b/testsuite/MDAnalysisTests/coordinates/test_tng.py index c9ea9b8678e..8eaa64d5ec2 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_tng.py +++ b/testsuite/MDAnalysisTests/coordinates/test_tng.py @@ -241,7 +241,9 @@ def test_initial_frame_is_0(self, universe): assert_equal( universe.trajectory.ts.frame, 0, - "initial frame is not 0 but {0}".format(universe.trajectory.ts.frame), + "initial frame is not 0 but {0}".format( + universe.trajectory.ts.frame + ), ) def test_starts_with_first_frame(self, universe): @@ -253,7 +255,9 @@ def test_rewind(self, universe): trj = universe.trajectory trj.next() trj.next() # for readers that do not support indexing - assert_equal(trj.ts.frame, 2, "failed to forward to frame 2 (frameindex 2)") + assert_equal( + trj.ts.frame, 2, "failed to forward to frame 2 (frameindex 2)" + ) trj.rewind() assert_equal(trj.ts.frame, 0, "failed to rewind to first frame") assert np.any(universe.atoms.positions > 0) @@ -289,7 +293,9 @@ def test_positions_first_frame(self, universe): def test_box_first_frame(self, universe): dims = universe.trajectory[0].dimensions - assert_allclose(dims, triclinic_box(*self._box_frame_0), rtol=10**-self.prec) + assert_allclose( + dims, triclinic_box(*self._box_frame_0), rtol=10**-self.prec + ) def test_positions_last_frame(self, universe): pos = universe.trajectory[100].positions @@ -315,21 +321,29 @@ def test_lambda_in_ts(self, universe): ts = universe.trajectory[10] assert "TNG_GMX_LAMBDA" in ts.data.keys() assert isinstance(ts.data["TNG_GMX_LAMBDA"], np.ndarray) - assert_equal(ts.data["TNG_GMX_LAMBDA"], np.asarray([[0]], dtype=np.float32)) + assert_equal( + ts.data["TNG_GMX_LAMBDA"], np.asarray([[0]], dtype=np.float32) + ) def test_read_box_fail_strange_step(self, universe): stepnum = 123 # step number with no data iterator_step = universe.trajectory._file_iterator.read_step(stepnum) with pytest.raises(IOError, match="Failed to read box from TNG file"): - universe.trajectory._frame_to_ts(iterator_step, universe.trajectory.ts) + universe.trajectory._frame_to_ts( + iterator_step, universe.trajectory.ts + ) def test_read_pos_fail_strange_step(self, universe): stepnum = 123 # step number with no data iterator_step = universe.trajectory._file_iterator.read_step(stepnum) # set _has_box to False to trigger position reading error universe.trajectory._has_box = False - with pytest.raises(IOError, match="Failed to read positions from TNG file"): - universe.trajectory._frame_to_ts(iterator_step, universe.trajectory.ts) + with pytest.raises( + IOError, match="Failed to read positions from TNG file" + ): + universe.trajectory._frame_to_ts( + iterator_step, universe.trajectory.ts + ) def test_additional_block_read_fails(self, universe): stepnum = 123 # step number with no data @@ -341,7 +355,9 @@ def test_additional_block_read_fails(self, universe): with pytest.raises( IOError, match="Failed to read additional block TNG_GMX_LAMBDA" ): - universe.trajectory._frame_to_ts(iterator_step, universe.trajectory.ts) + universe.trajectory._frame_to_ts( + iterator_step, universe.trajectory.ts + ) def test_parse_n_atoms(self, universe): assert universe.trajectory.parse_n_atoms(TNG_traj) == self._n_atoms @@ -396,8 +412,12 @@ def test_read_vels_fail_strange_step(self, universe): # set _has_* attrs to False to trigger velocities reading error universe.trajectory._has_box = False universe.trajectory._has_positions = False - with pytest.raises(IOError, match="Failed to read velocities from TNG file"): - universe.trajectory._frame_to_ts(iterator_step, universe.trajectory.ts) + with pytest.raises( + IOError, match="Failed to read velocities from TNG file" + ): + universe.trajectory._frame_to_ts( + iterator_step, universe.trajectory.ts + ) def test_read_force_fail_strange_step(self, universe): stepnum = 123 # step number with no data @@ -406,8 +426,12 @@ def test_read_force_fail_strange_step(self, universe): universe.trajectory._has_box = False universe.trajectory._has_positions = False universe.trajectory._has_velocities = False - with pytest.raises(IOError, match="Failed to read forces from TNG file"): - universe.trajectory._frame_to_ts(iterator_step, universe.trajectory.ts) + with pytest.raises( + IOError, match="Failed to read forces from TNG file" + ): + universe.trajectory._frame_to_ts( + iterator_step, universe.trajectory.ts + ) @pytest.mark.skipif(not HAS_PYTNG, reason="pytng not installed") diff --git a/testsuite/MDAnalysisTests/coordinates/test_trc.py b/testsuite/MDAnalysisTests/coordinates/test_trc.py index 430eb422374..f01f6c7ed92 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_trc.py +++ b/testsuite/MDAnalysisTests/coordinates/test_trc.py @@ -50,7 +50,9 @@ def test_trc_positions(self, TRC_U): ) # fith frame first particle TRC_U.trajectory[4] - assert_allclose(TRC_U.atoms.positions[0], [0.37026654, 22.78805010, 3.69695262]) + assert_allclose( + TRC_U.atoms.positions[0], [0.37026654, 22.78805010, 3.69695262] + ) def test_trc_dimensions(self, TRC_U): assert TRC_U.trajectory[0].dimensions is None @@ -91,13 +93,17 @@ def test_rewind(self, TRC_U): trc.next() trc.next() trc.next() - assert trc.ts.frame == 4, "trajectory.next() did not forward to frameindex 4" + assert ( + trc.ts.frame == 4 + ), "trajectory.next() did not forward to frameindex 4" trc.rewind() - assert trc.ts.frame == 0, "trajectory.rewind() failed to rewind to first frame" + assert ( + trc.ts.frame == 0 + ), "trajectory.rewind() failed to rewind to first frame" - assert np.any(TRC_U.atoms.positions != 0), ( - "The atom positions are not populated" - ) + assert np.any( + TRC_U.atoms.positions != 0 + ), "The atom positions are not populated" def test_random_access(self, TRC_U): TRC_U.trajectory[0] @@ -132,7 +138,8 @@ def test_periodic(self, TRC_U): def test_trc_dimensions(self, TRC_U): ts = TRC_U.trajectory[1] assert_allclose( - ts.dimensions, [30.54416298, 30.54416298, 30.54416298, 90.0, 90.0, 90.0] + ts.dimensions, + [30.54416298, 30.54416298, 30.54416298, 90.0, 90.0, 90.0], ) def test_open_twice(self, TRC_U): @@ -149,7 +156,8 @@ def TRC_U(self): def test_trc_dimensions(self, TRC_U): ts = TRC_U.trajectory[1] assert_allclose( - ts.dimensions, [3.054416298, 3.054416298, 3.054416298, 90.0, 90.0, 90.0] + ts.dimensions, + [3.054416298, 3.054416298, 3.054416298, 90.0, 90.0, 90.0], ) @@ -197,8 +205,14 @@ def test_trc_distances(self, TRC_U): ag1 = atom_1a + atom_1b + atom_1c + atom_1d ag2 = atom_2a + atom_2b + atom_2c + atom_2d - dist_A = distances.dist(ag1, ag2, box=ts.dimensions)[2] # return distance - dist_B = atomicdistances.AtomicDistances(ag1, ag2, pbc=True).run().results[0] + dist_A = distances.dist(ag1, ag2, box=ts.dimensions)[ + 2 + ] # return distance + dist_B = ( + atomicdistances.AtomicDistances(ag1, ag2, pbc=True) + .run() + .results[0] + ) assert_allclose( dist_A, [5.9488481, 4.4777278, 20.8165518, 7.5727112], rtol=1e-06 @@ -235,7 +249,9 @@ class TestTRCEmptyFile: def test_universe(self): with pytest.raises( ValueError, - match=("No supported blocks were found within the GROMOS trajectory!"), + match=( + "No supported blocks were found within the GROMOS trajectory!" + ), ): mda.Universe(TRC_PDB_VAC, TRC_EMPTY) @@ -274,11 +290,15 @@ def test_trc_n_frames(self, TRC_U): assert TRC_U.trajectory.n_frames == 3 def test_trc_frame(self, TRC_U): - with pytest.warns(UserWarning, match="POSITION block is not supported!"): + with pytest.warns( + UserWarning, match="POSITION block is not supported!" + ): assert TRC_U.trajectory[0].frame == 0 assert TRC_U.trajectory[2].frame == 2 def test_trc_time(self, TRC_U): - with pytest.warns(UserWarning, match="POSITION block is not supported!"): + with pytest.warns( + UserWarning, match="POSITION block is not supported!" + ): assert TRC_U.trajectory[0].time == 0 assert TRC_U.trajectory[2].time == 0 diff --git a/testsuite/MDAnalysisTests/coordinates/test_trj.py b/testsuite/MDAnalysisTests/coordinates/test_trj.py index 1ba1271f5fb..65c1e62dba0 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_trj.py +++ b/testsuite/MDAnalysisTests/coordinates/test_trj.py @@ -23,70 +23,91 @@ import numpy as np import pytest -from numpy.testing import ( - assert_equal, - assert_almost_equal -) +from numpy.testing import assert_equal, assert_almost_equal import MDAnalysis as mda from MDAnalysisTests.coordinates.reference import RefACHE, RefCappedAla -from MDAnalysisTests.datafiles import (PRM, TRJ, TRJ_bz2, PRMpbc, TRJpbc_bz2) +from MDAnalysisTests.datafiles import PRM, TRJ, TRJ_bz2, PRMpbc, TRJpbc_bz2 class _TRJReaderTest(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return mda.Universe(self.topology_file, self.trajectory_file) def test_load_prm(self, universe): - assert_equal(len(universe.atoms), self.ref_n_atoms, - "load Universe from PRM and TRJ") + assert_equal( + len(universe.atoms), + self.ref_n_atoms, + "load Universe from PRM and TRJ", + ) def test_n_atoms(self, universe): - assert_equal(universe.trajectory.n_atoms, self.ref_n_atoms, - "wrong number of atoms") + assert_equal( + universe.trajectory.n_atoms, + self.ref_n_atoms, + "wrong number of atoms", + ) def test_n_frames(self, universe): - assert_equal(universe.trajectory.n_frames, self.ref_n_frames, - "wrong number of frames in xyz") + assert_equal( + universe.trajectory.n_frames, + self.ref_n_frames, + "wrong number of frames in xyz", + ) def test_periodic(self, universe): assert_equal(universe.trajectory.periodic, self.ref_periodic) def test_amber_proteinselection(self, universe): - protein = universe.select_atoms('protein') - assert_equal(protein.n_atoms, self.ref_proteinatoms, - "error in protein selection (HIS or termini?)") + protein = universe.select_atoms("protein") + assert_equal( + protein.n_atoms, + self.ref_proteinatoms, + "error in protein selection (HIS or termini?)", + ) def test_sum_centres_of_geometry(self, universe): - protein = universe.select_atoms('protein') - total = np.sum([protein.center_of_geometry() for ts in - universe.trajectory]) - assert_almost_equal(total, self.ref_sum_centre_of_geometry, self.prec, - err_msg="sum of centers of geometry over the " - "trajectory do not match") + protein = universe.select_atoms("protein") + total = np.sum( + [protein.center_of_geometry() for ts in universe.trajectory] + ) + assert_almost_equal( + total, + self.ref_sum_centre_of_geometry, + self.prec, + err_msg="sum of centers of geometry over the " + "trajectory do not match", + ) def test_initial_frame_is_0(self, universe): - assert_equal(universe.trajectory.ts.frame, 0, - "initial frame is not 0 but {0}".format( - universe.trajectory.ts.frame)) + assert_equal( + universe.trajectory.ts.frame, + 0, + "initial frame is not 0 but {0}".format( + universe.trajectory.ts.frame + ), + ) def test_starts_with_first_frame(self, universe): """Test that coordinate arrays are filled as soon as the trajectory has been opened.""" - assert np.any(universe.atoms.positions > 0), "Reader does not " \ - "populate positions right away." + assert np.any(universe.atoms.positions > 0), ( + "Reader does not " "populate positions right away." + ) def test_rewind(self, universe): trj = universe.trajectory trj.next() trj.next() # for readers that do not support indexing - assert_equal(trj.ts.frame, 2, - "failed to forward to frame 2 (frameindex 2)") + assert_equal( + trj.ts.frame, 2, "failed to forward to frame 2 (frameindex 2)" + ) trj.rewind() assert_equal(trj.ts.frame, 0, "failed to rewind to first frame") - assert np.any(universe.atoms.positions > 0), "Reader does not " \ - "populate positions after rewinding." + assert np.any(universe.atoms.positions > 0), ( + "Reader does not " "populate positions after rewinding." + ) def test_full_slice(self, universe): trj_iter = universe.trajectory[:] @@ -135,4 +156,4 @@ class TestBzippedTRJReaderPBC(_TRJReaderTest, RefCappedAla): def test_trj_no_natoms(): with pytest.raises(ValueError): - mda.coordinates.TRJ.TRJReader('somefile.txt') + mda.coordinates.TRJ.TRJReader("somefile.txt") diff --git a/testsuite/MDAnalysisTests/coordinates/test_trz.py b/testsuite/MDAnalysisTests/coordinates/test_trz.py index ea455bb8c71..8e42bc644fe 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_trz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_trz.py @@ -24,15 +24,11 @@ import MDAnalysis as mda import os -from numpy.testing import ( - assert_equal, - assert_almost_equal, - assert_allclose -) +from numpy.testing import assert_equal, assert_almost_equal, assert_allclose import numpy as np from MDAnalysisTests.coordinates.reference import RefTRZ -from MDAnalysisTests.datafiles import (TRZ_psf, TRZ, two_water_gro) +from MDAnalysisTests.datafiles import TRZ_psf, TRZ, two_water_gro def test_deprecated_trz_reader(): @@ -49,7 +45,7 @@ def test_deprecated_trz_writer(tmpdir): with pytest.warns(DeprecationWarning, match=wmsg): with tmpdir.as_cwd(): - with mda.coordinates.TRZ.TRZWriter('test.trz', len(u.atoms)) as W: + with mda.coordinates.TRZ.TRZWriter("test.trz", len(u.atoms)) as W: W.write(u) @@ -64,14 +60,16 @@ def universe(self): def test_load_trz(self, universe): U = universe - assert_equal(len(U.atoms), self.ref_n_atoms, - "load Universe from PSF and TRZ") + assert_equal( + len(U.atoms), self.ref_n_atoms, "load Universe from PSF and TRZ" + ) def test_next_trz(self, universe): assert_equal(universe.trajectory.ts.frame, 0, "starts at first frame") universe.trajectory.next() - assert_equal(universe.trajectory.ts.frame, 1, - "next returns frame index 1") + assert_equal( + universe.trajectory.ts.frame, 1, "next returns frame index 1" + ) def test_rewind_trz(self, universe): # move to different frame and rewind to get first frame back @@ -80,8 +78,11 @@ def test_rewind_trz(self, universe): assert_equal(universe.trajectory.ts.frame, 0, "rewinding to frame 1") def test_n_frames(self, universe): - assert_equal(universe.trajectory.n_frames, self.ref_n_frames, - "wrong number of frames in trz") + assert_equal( + universe.trajectory.n_frames, + self.ref_n_frames, + "wrong number of frames in trz", + ) def test_seeking(self, universe): universe.trajectory[3] @@ -93,58 +94,79 @@ def test_seeking(self, universe): assert_equal(universe.trajectory.ts.frame, 4, "loading frame 4") universe.trajectory[3] - assert_almost_equal(universe.atoms[0:3].positions, orig, - self.prec) + assert_almost_equal(universe.atoms[0:3].positions, orig, self.prec) universe.trajectory[0] assert_equal(universe.trajectory.ts.frame, 0, "loading frame 0") universe.trajectory[3] - assert_almost_equal(universe.atoms[0:3].positions, orig, - self.prec) + assert_almost_equal(universe.atoms[0:3].positions, orig, self.prec) def test_volume(self, universe): # Lower precision here because errors seem to accumulate and # throw this off (is rounded value**3) - assert_almost_equal(universe.trajectory.ts.volume, self.ref_volume, 1, - "wrong volume for trz") + assert_almost_equal( + universe.trajectory.ts.volume, + self.ref_volume, + 1, + "wrong volume for trz", + ) def test_unitcell(self, universe): - assert_almost_equal(universe.trajectory.ts.dimensions, - self.ref_dimensions, self.prec, - "wrong dimensions for trz") + assert_almost_equal( + universe.trajectory.ts.dimensions, + self.ref_dimensions, + self.prec, + "wrong dimensions for trz", + ) def test_coordinates(self, universe): fortytwo = universe.atoms[41] # 41 because is 0 based - assert_almost_equal(fortytwo.position, self.ref_coordinates, self.prec, - "wrong coordinates in trz") + assert_almost_equal( + fortytwo.position, + self.ref_coordinates, + self.prec, + "wrong coordinates in trz", + ) def test_velocities(self, universe): - fortytwo = universe.select_atoms('bynum 42') - assert_almost_equal(fortytwo.velocities, self.ref_velocities, - self.prec, "wrong velocities in trz") + fortytwo = universe.select_atoms("bynum 42") + assert_almost_equal( + fortytwo.velocities, + self.ref_velocities, + self.prec, + "wrong velocities in trz", + ) def test_delta(self, universe): - assert_almost_equal(universe.trajectory.delta, self.ref_delta, - self.prec, - "wrong time delta in trz") + assert_almost_equal( + universe.trajectory.delta, + self.ref_delta, + self.prec, + "wrong time delta in trz", + ) def test_time(self, universe): - assert_almost_equal(universe.trajectory.time, self.ref_time, self.prec, - "wrong time value in trz") + assert_almost_equal( + universe.trajectory.time, + self.ref_time, + self.prec, + "wrong time value in trz", + ) def test_title(self, universe): - assert_equal(self.ref_title, universe.trajectory.title, - "wrong title in trz") + assert_equal( + self.ref_title, universe.trajectory.title, "wrong title in trz" + ) def test_get_writer(self, universe, tmpdir): - self.outfile = os.path.join(str(tmpdir), 'test-trz-writer.trz') + self.outfile = os.path.join(str(tmpdir), "test-trz-writer.trz") with universe.trajectory.Writer(self.outfile) as W: assert_equal(isinstance(W, mda.coordinates.TRZ.TRZWriter), True) assert_equal(W.n_atoms, universe.trajectory.n_atoms) def test_get_writer_2(self, universe, tmpdir): - self.outfile = os.path.join(str(tmpdir), 'test-trz-writer-1.trz') + self.outfile = os.path.join(str(tmpdir), "test-trz-writer-1.trz") with universe.trajectory.Writer(self.outfile, n_atoms=100) as W: assert_equal(isinstance(W, mda.coordinates.TRZ.TRZWriter), True) assert_equal(W.n_atoms, 100) @@ -154,7 +176,7 @@ def test_get_wrong_n_atoms(self): mda.Universe(TRZ, n_atoms=8080) def test_read_zero_box(self, tmpdir): - outfile = str(tmpdir.join('/test-trz-writer.trz')) + outfile = str(tmpdir.join("/test-trz-writer.trz")) u = mda.Universe.empty(10, trajectory=True) u.dimensions = None @@ -171,7 +193,7 @@ def test_read_zero_box(self, tmpdir): class TestTRZWriter(RefTRZ): prec = 3 writer = mda.coordinates.TRZ.TRZWriter - title_to_write = 'Test title TRZ' + title_to_write = "Test title TRZ" @pytest.fixture() def universe(self): @@ -179,7 +201,7 @@ def universe(self): @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir.join('/test-trz-writer.trz')) + return str(tmpdir.join("/test-trz-writer.trz")) def test_write_trajectory(self, universe, outfile): t = universe.trajectory @@ -193,32 +215,44 @@ def _copy_traj(self, writer, universe, outfile): uw = mda.Universe(TRZ_psf, outfile) - assert_equal(uw.trajectory.title, self.title_to_write, - "Title mismatch between original and written files.") - - for orig_ts, written_ts in zip(universe.trajectory, - uw.trajectory): - assert_almost_equal(orig_ts._pos, written_ts._pos, self.prec, - err_msg="Coordinate mismatch between " - "orig and written at frame %d" % - orig_ts.frame) - assert_almost_equal(orig_ts._velocities, - written_ts._velocities, self.prec, - err_msg="Coordinate mismatch between " - "orig and written at frame %d" % - orig_ts.frame) - assert_almost_equal(orig_ts._unitcell, written_ts._unitcell, - self.prec, err_msg="Unitcell mismatch " - "between orig and written at frame %d" % - orig_ts.frame) + assert_equal( + uw.trajectory.title, + self.title_to_write, + "Title mismatch between original and written files.", + ) + + for orig_ts, written_ts in zip(universe.trajectory, uw.trajectory): + assert_almost_equal( + orig_ts._pos, + written_ts._pos, + self.prec, + err_msg="Coordinate mismatch between " + "orig and written at frame %d" % orig_ts.frame, + ) + assert_almost_equal( + orig_ts._velocities, + written_ts._velocities, + self.prec, + err_msg="Coordinate mismatch between " + "orig and written at frame %d" % orig_ts.frame, + ) + assert_almost_equal( + orig_ts._unitcell, + written_ts._unitcell, + self.prec, + err_msg="Unitcell mismatch " + "between orig and written at frame %d" % orig_ts.frame, + ) for att in orig_ts.data: - assert_almost_equal(orig_ts.data[att], - written_ts.data[att], self.prec, - err_msg="TS equal failed for {0!s}".format( - att)) + assert_almost_equal( + orig_ts.data[att], + written_ts.data[att], + self.prec, + err_msg="TS equal failed for {0!s}".format(att), + ) def test_long_title(self, outfile): - title = '*' * 81 + title = "*" * 81 with pytest.raises(ValueError): self.writer(outfile, self.ref_n_atoms, title=title) @@ -226,8 +260,9 @@ def test_no_box_warning(self, outfile): u = mda.Universe.empty(10, trajectory=True) u.dimensions = None - with pytest.warns(UserWarning, - match="box will be written as all zero values"): + with pytest.warns( + UserWarning, match="box will be written as all zero values" + ): with mda.Writer(outfile, n_atoms=10) as w: w.write(u.atoms) @@ -240,7 +275,7 @@ def u(self): @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir.join('/trz-writer-2.trz')) + return str(tmpdir.join("/trz-writer-2.trz")) def test_writer_trz_from_other(self, u, outfile): with mda.coordinates.TRZ.TRZWriter(outfile, len(u.atoms)) as W: @@ -256,7 +291,7 @@ def test_no_dt_warning(self, u, outfile): u2 = mda.Universe(two_water_gro, outfile) - wmsg = ('Reader has no dt information, set to 1.0 ps') + wmsg = "Reader has no dt information, set to 1.0 ps" with pytest.warns(UserWarning, match=wmsg): assert_allclose(u2.trajectory.dt, 1.0) @@ -270,6 +305,7 @@ class TestWrite_Partial_Timestep(object): just checks that Writer is receiving this information properly. """ + prec = 3 @pytest.fixture() @@ -277,15 +313,17 @@ def universe(self): return mda.Universe(TRZ_psf, TRZ) def test_write_trajectory(self, universe, tmpdir): - ag = universe.select_atoms('name N') - outfile = str(tmpdir.join('/partial-write-test.pdb')) + ag = universe.select_atoms("name N") + outfile = str(tmpdir.join("/partial-write-test.pdb")) writer = mda.Writer(outfile, n_atoms=len(ag)) writer.write(ag) writer.close() u_ag = mda.Universe(outfile) - assert_almost_equal(ag.positions, - u_ag.atoms.positions, - self.prec, - err_msg="Writing AtomGroup timestep failed.") + assert_almost_equal( + ag.positions, + u_ag.atoms.positions, + self.prec, + err_msg="Writing AtomGroup timestep failed.", + ) diff --git a/testsuite/MDAnalysisTests/coordinates/test_txyz.py b/testsuite/MDAnalysisTests/coordinates/test_txyz.py index fda7e62ba89..66cd38067e9 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_txyz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_txyz.py @@ -44,20 +44,23 @@ def ARC_PBC_U(): def test_txyz_positions(TXYZ_U): - assert_almost_equal(TXYZ_U.atoms.positions[0], - [-6.553398, -1.854369, 0.000000]) + assert_almost_equal( + TXYZ_U.atoms.positions[0], [-6.553398, -1.854369, 0.000000] + ) def test_arc_positions(ARC_U): - assert_almost_equal(ARC_U.atoms.positions[0], - [-6.553398, -1.854369, 0.000000]) + assert_almost_equal( + ARC_U.atoms.positions[0], [-6.553398, -1.854369, 0.000000] + ) def test_arc_positions_frame_2(ARC_U): ARC_U.trajectory[1] - assert_almost_equal(ARC_U.atoms.positions[0], - [-0.231579, -0.350841, -0.037475]) + assert_almost_equal( + ARC_U.atoms.positions[0], [-0.231579, -0.350841, -0.037475] + ) def test_arc_traj_length(ARC_U): @@ -69,10 +72,11 @@ def test_arcpbc_traj_length(ARC_PBC_U): def test_pbc_boxsize(ARC_PBC_U): - ref_dimensions=[[ 38.860761, 38.860761, 38.860761, 90.000000, 90.000000, 90.000000], - [ 39.860761, 39.860761, 39.860761, 90.000000, 90.000000, 90.000000], - [ 40.860761, 40.860761, 40.860761, 90.000000, 90.000000, 90.000000]] + ref_dimensions = [ + [38.860761, 38.860761, 38.860761, 90.000000, 90.000000, 90.000000], + [39.860761, 39.860761, 39.860761, 90.000000, 90.000000, 90.000000], + [40.860761, 40.860761, 40.860761, 90.000000, 90.000000, 90.000000], + ] for ref_box, ts in zip(ref_dimensions, ARC_PBC_U.trajectory): assert_almost_equal(ref_box, ts.dimensions, decimal=5) - diff --git a/testsuite/MDAnalysisTests/coordinates/test_windows.py b/testsuite/MDAnalysisTests/coordinates/test_windows.py index 723ce5e689d..86194e1e4b2 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_windows.py +++ b/testsuite/MDAnalysisTests/coordinates/test_windows.py @@ -49,26 +49,36 @@ class TestWinLammpsDump(TestLammpsDumpReader): @pytest.fixture def u(self): - return mda.Universe(WIN_LAMMPSDUMP, format='LAMMPSDUMP') + return mda.Universe(WIN_LAMMPSDUMP, format="LAMMPSDUMP") class TestWinPDB(object): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def multiverse(): return mda.Universe(WIN_PDB_multiframe, guess_bonds=True) def test_n_frames(self, multiverse): - assert_equal(multiverse.trajectory.n_frames, 24, - "Wrong number of frames read from PDB muliple model file") + assert_equal( + multiverse.trajectory.n_frames, + 24, + "Wrong number of frames read from PDB muliple model file", + ) + def test_rewind(self, multiverse): u = multiverse u.trajectory[11] - assert_equal(u.trajectory.ts.frame, 11, - "Failed to forward to 11th frame (frame index 11)") + assert_equal( + u.trajectory.ts.frame, + 11, + "Failed to forward to 11th frame (frame index 11)", + ) u.trajectory.rewind() - assert_equal(u.trajectory.ts.frame, 0, - "Failed to rewind to 0th frame (frame index 0)") + assert_equal( + u.trajectory.ts.frame, + 0, + "Failed to rewind to 0th frame (frame index 0)", + ) def test_iteration(self, multiverse): u = multiverse @@ -80,19 +90,23 @@ def test_iteration(self, multiverse): for ts in u.trajectory: frames.append(ts) assert_equal( - len(frames), u.trajectory.n_frames, + len(frames), + u.trajectory.n_frames, "iterated number of frames %d is not the expected number %d; " - "trajectory iterator fails to rewind" % - (len(frames), u.trajectory.n_frames)) + "trajectory iterator fails to rewind" + % (len(frames), u.trajectory.n_frames), + ) def test_slice_iteration(self, multiverse): u = multiverse frames = [] for ts in u.trajectory[4:-2:4]: frames.append(ts.frame) - assert_equal(np.array(frames), - np.arange(u.trajectory.n_frames)[4:-2:4], - err_msg="slicing did not produce the expected frames") + assert_equal( + np.array(frames), + np.arange(u.trajectory.n_frames)[4:-2:4], + err_msg="slicing did not produce the expected frames", + ) class TestWinDLPolyHistory(TestDLPolyHistory): @@ -113,11 +127,13 @@ def test_n_frames(self, WIN_ARC_U): assert len(WIN_ARC_U.trajectory) == 2 def test_positions(self, WIN_ARC_U): - assert_almost_equal(WIN_ARC_U.atoms.positions[0], - [-6.553398, -1.854369, 0.000000]) + assert_almost_equal( + WIN_ARC_U.atoms.positions[0], [-6.553398, -1.854369, 0.000000] + ) def test_positions_2(self, WIN_ARC_U): WIN_ARC_U.trajectory[1] - assert_almost_equal(WIN_ARC_U.atoms.positions[0], - [-0.231579, -0.350841, -0.037475]) + assert_almost_equal( + WIN_ARC_U.atoms.positions[0], [-0.231579, -0.350841, -0.037475] + ) diff --git a/testsuite/MDAnalysisTests/coordinates/test_writer_api.py b/testsuite/MDAnalysisTests/coordinates/test_writer_api.py index 54d364fb75b..4c3ac08cb5b 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_writer_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_writer_api.py @@ -27,9 +27,11 @@ # grab all known writers # sort so test order is predictable for parallel tests -writers = sorted(set(mda._MULTIFRAME_WRITERS.values()) | - set(mda._SINGLEFRAME_WRITERS.values()), - key=lambda x: x.__name__) +writers = sorted( + set(mda._MULTIFRAME_WRITERS.values()) + | set(mda._SINGLEFRAME_WRITERS.values()), + key=lambda x: x.__name__, +) known_ts_haters = [ mda.coordinates.MOL2.MOL2Writer, mda.coordinates.PDB.PDBWriter, @@ -41,7 +43,7 @@ ] -@pytest.mark.parametrize('writer', writers) +@pytest.mark.parametrize("writer", writers) def test_ts_error(writer, tmpdir): u = mda.Universe.empty(10, trajectory=True) @@ -49,16 +51,21 @@ def test_ts_error(writer, tmpdir): # chemfiles Writer exists but doesn't work without chemfiles if not mda.coordinates.chemfiles.check_chemfiles_version(): pytest.skip("Chemfiles not available") - fn = str(tmpdir.join('out.xtc')) + fn = str(tmpdir.join("out.xtc")) elif writer == mda.coordinates.LAMMPS.DATAWriter: pytest.skip("DATAWriter requires integer atom types") - elif writer == mda.coordinates.H5MD.H5MDWriter and not mda.coordinates.H5MD.HAS_H5PY: + elif ( + writer == mda.coordinates.H5MD.H5MDWriter + and not mda.coordinates.H5MD.HAS_H5PY + ): pytest.skip("skipping H5MDWriter test because h5py is not installed") else: - fn = str(tmpdir.join('out.traj')) + fn = str(tmpdir.join("out.traj")) - if (writer == mda.coordinates.PDB.PDBWriter or - writer == mda.coordinates.PDB.MultiPDBWriter): + if ( + writer == mda.coordinates.PDB.PDBWriter + or writer == mda.coordinates.PDB.MultiPDBWriter + ): errmsg = "PDBWriter cannot write Timestep objects directly" else: errmsg = "neither an AtomGroup or Universe" @@ -68,7 +75,7 @@ def test_ts_error(writer, tmpdir): w.write(u.trajectory.ts) -@pytest.mark.parametrize('writer', writers) +@pytest.mark.parametrize("writer", writers) def test_write_with_atomgroup(writer, tmpdir): u = mda.Universe.empty(10, trajectory=True) @@ -76,25 +83,28 @@ def test_write_with_atomgroup(writer, tmpdir): # chemfiles Writer exists but doesn't work without chemfiles if not mda.coordinates.chemfiles.check_chemfiles_version(): pytest.skip("Chemfiles not available") - fn = str(tmpdir.join('out.xtc')) + fn = str(tmpdir.join("out.xtc")) elif writer == mda.coordinates.MOL2.MOL2Writer: pytest.skip("MOL2 only writes MOL2 back out") elif writer == mda.coordinates.LAMMPS.DATAWriter: pytest.skip("DATAWriter requires integer atom types") - elif writer == mda.coordinates.H5MD.H5MDWriter and not mda.coordinates.H5MD.HAS_H5PY: + elif ( + writer == mda.coordinates.H5MD.H5MDWriter + and not mda.coordinates.H5MD.HAS_H5PY + ): pytest.skip("skipping H5MDWriter test because h5py is not installed") else: - fn = str(tmpdir.join('out.traj')) + fn = str(tmpdir.join("out.traj")) # H5MDWriter raises ValueError if the trajectory has no units and # convert_units is True - convert_units = (writer != mda.coordinates.H5MD.H5MDWriter) + convert_units = writer != mda.coordinates.H5MD.H5MDWriter with writer(fn, n_atoms=u.atoms.n_atoms, convert_units=convert_units) as w: w.write(u.atoms) -@pytest.mark.parametrize('writer', writers) +@pytest.mark.parametrize("writer", writers) def test_write_with_universe(writer, tmpdir): u = mda.Universe.empty(10, trajectory=True) @@ -102,19 +112,22 @@ def test_write_with_universe(writer, tmpdir): # chemfiles Writer exists but doesn't work without chemfiles if not mda.coordinates.chemfiles.check_chemfiles_version(): pytest.skip("Chemfiles not available") - fn = str(tmpdir.join('out.xtc')) + fn = str(tmpdir.join("out.xtc")) elif writer == mda.coordinates.MOL2.MOL2Writer: pytest.skip("MOL2 only writes MOL2 back out") elif writer == mda.coordinates.LAMMPS.DATAWriter: pytest.skip("DATAWriter requires integer atom types") - elif writer == mda.coordinates.H5MD.H5MDWriter and not mda.coordinates.H5MD.HAS_H5PY: + elif ( + writer == mda.coordinates.H5MD.H5MDWriter + and not mda.coordinates.H5MD.HAS_H5PY + ): pytest.skip("skipping H5MDWriter test because h5py is not installed") else: - fn = str(tmpdir.join('out.traj')) + fn = str(tmpdir.join("out.traj")) # H5MDWriter raises ValueError if the trajectory has no units and # convert_units is True - convert_units = (writer != mda.coordinates.H5MD.H5MDWriter) + convert_units = writer != mda.coordinates.H5MD.H5MDWriter with writer(fn, n_atoms=u.atoms.n_atoms, convert_units=convert_units) as w: w.write(u.atoms) diff --git a/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py b/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py index e5fdf19744a..616bae0ef64 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py +++ b/testsuite/MDAnalysisTests/coordinates/test_writer_registration.py @@ -29,7 +29,7 @@ class TestWriterCreation(object): class MagicWriter(WriterBase): # this writer does the 'magic' format - format = 'MAGIC' + format = "MAGIC" def __init__(self, filename, n_atoms=None): self.filename = filename @@ -38,22 +38,24 @@ def __init__(self, filename, n_atoms=None): class MultiMagicWriter(MagicWriter): # this writer does the 'magic' and 'magic2' formats # but *only* supports multiframe writing. - format = ['MAGIC', 'MAGIC2'] + format = ["MAGIC", "MAGIC2"] multiframe = True singleframe = False def test_default_multiframe(self): - assert isinstance(mda.Writer('this.magic'), self.MultiMagicWriter) + assert isinstance(mda.Writer("this.magic"), self.MultiMagicWriter) def test_singleframe(self): # check that singleframe=False has been respected - assert isinstance(mda.Writer('this.magic', multiframe=False), self.MagicWriter) + assert isinstance( + mda.Writer("this.magic", multiframe=False), self.MagicWriter + ) def test_multiframe_magic2(self): # this will work as we go for multiframe - assert isinstance(mda.Writer('that.magic2'), self.MultiMagicWriter) + assert isinstance(mda.Writer("that.magic2"), self.MultiMagicWriter) def test_singleframe_magic2(self): # this should fail, there isn't a singleframe writer for magic2 with pytest.raises(TypeError): - mda.Writer('that.magic2', multiframe=False) + mda.Writer("that.magic2", multiframe=False) diff --git a/testsuite/MDAnalysisTests/coordinates/test_xdr.py b/testsuite/MDAnalysisTests/coordinates/test_xdr.py index efb15d78250..4a356eed042 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_xdr.py +++ b/testsuite/MDAnalysisTests/coordinates/test_xdr.py @@ -30,17 +30,31 @@ from pathlib import Path import numpy as np -from numpy.testing import (assert_equal, - assert_almost_equal, - assert_allclose) +from numpy.testing import assert_equal, assert_almost_equal, assert_allclose from MDAnalysisTests import make_Universe from MDAnalysisTests.datafiles import ( - PDB_sub_dry, PDB_sub_sol, TRR_sub_sol, TRR, XTC, GRO, PDB, CRD, PRMncdf, - NCDF, XTC_sub_sol, COORDINATES_XTC, COORDINATES_TOPOLOGY, COORDINATES_TRR) -from MDAnalysisTests.coordinates.base import (MultiframeReaderTest, - BaseReference, BaseWriterTest, - assert_timestep_almost_equal) + PDB_sub_dry, + PDB_sub_sol, + TRR_sub_sol, + TRR, + XTC, + GRO, + PDB, + CRD, + PRMncdf, + NCDF, + XTC_sub_sol, + COORDINATES_XTC, + COORDINATES_TOPOLOGY, + COORDINATES_TRR, +) +from MDAnalysisTests.coordinates.base import ( + MultiframeReaderTest, + BaseReference, + BaseWriterTest, + assert_timestep_almost_equal, +) import MDAnalysis as mda from MDAnalysis.coordinates.base import Timestep @@ -48,11 +62,14 @@ from MDAnalysisTests.util import get_userid -@pytest.mark.parametrize("filename,kwargs,reference", [ - ("foo.xtc", {}, ".foo.xtc_offsets.npz"), - ("foo.xtc", {"ending": "npz"}, ".foo.xtc_offsets.npz"), - ("bar.0001.trr", {"ending": "npzzzz"}, ".bar.0001.trr_offsets.npzzzz"), -]) +@pytest.mark.parametrize( + "filename,kwargs,reference", + [ + ("foo.xtc", {}, ".foo.xtc_offsets.npz"), + ("foo.xtc", {"ending": "npz"}, ".foo.xtc_offsets.npz"), + ("bar.0001.trr", {"ending": "npzzzz"}, ".bar.0001.trr_offsets.npzzzz"), + ], +) def test_offsets_filename(filename, kwargs, reference): fn = XDR.offsets_filename(filename, **kwargs) assert fn == reference @@ -93,13 +110,14 @@ class _GromacsReader(object): # This base class assumes same lengths and dt for XTC and TRR test cases! filename = None ref_unitcell = np.array( - [80.017, 80.017, 80.017, 60., 60., 90.], dtype=np.float32) + [80.017, 80.017, 80.017, 60.0, 60.0, 90.0], dtype=np.float32 + ) # computed with Gromacs: 362.26999999999998 nm**3 * 1000 A**3/nm**3 ref_volume = 362270.0 prec = 3 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return mda.Universe(GRO, self.filename, convert_units=True) @@ -119,8 +137,9 @@ def test_jump_xdrtrj(self, universe): def test_jump_lastframe_xdrtrj(self, universe): universe.trajectory[-1] - assert_equal(universe.coord.frame, 9, - "indexing last frame with trajectory[-1]") + assert_equal( + universe.coord.frame, 9, "indexing last frame with trajectory[-1]" + ) def test_slice_xdrtrj(self, universe): frames = [ts.frame for ts in universe.trajectory[2:9:3]] @@ -132,22 +151,23 @@ def test_reverse_xdrtrj(self, universe): def test_coordinates(self, universe): ca_nm = np.array( - [[6.043369675, 7.385184479, 1.381425762]], dtype=np.float32) + [[6.043369675, 7.385184479, 1.381425762]], dtype=np.float32 + ) # coordinates in the base unit (needed for True) ca_Angstrom = ca_nm * 10.0 universe.trajectory.rewind() universe.trajectory.next() universe.trajectory.next() assert_equal(universe.coord.frame, 2, "failed to step to frame 3") - ca = universe.select_atoms('name CA and resid 122') + ca = universe.select_atoms("name CA and resid 122") # low precision match (2 decimals in A, 3 in nm) because the above are # the trr coords assert_almost_equal( ca.positions, ca_Angstrom, 2, - err_msg="coords of Ca of resid 122 do not " - "match for frame 3") + err_msg="coords of Ca of resid 122 do not " "match for frame 3", + ) def test_unitcell(self, universe): """Test that xtc/trr unitcell is read correctly (Issue 34)""" @@ -157,7 +177,8 @@ def test_unitcell(self, universe): uc, self.ref_unitcell, self.prec, - err_msg="unit cell dimensions (rhombic dodecahedron)") + err_msg="unit cell dimensions (rhombic dodecahedron)", + ) def test_volume(self, universe): # need to reduce precision for test (nm**3 <--> A**3) @@ -167,11 +188,13 @@ def test_volume(self, universe): vol, self.ref_volume, 0, - err_msg="unit cell volume (rhombic dodecahedron)") + err_msg="unit cell volume (rhombic dodecahedron)", + ) def test_dt(self, universe): assert_almost_equal( - universe.trajectory.dt, 100.0, 4, err_msg="wrong timestep dt") + universe.trajectory.dt, 100.0, 4, err_msg="wrong timestep dt" + ) def test_totaltime(self, universe): # test_totaltime(): need to reduce precision because dt is only precise @@ -181,7 +204,8 @@ def test_totaltime(self, universe): universe.trajectory.totaltime, 900.0, 3, - err_msg="wrong total length of trajectory") + err_msg="wrong total length of trajectory", + ) def test_frame(self, universe): universe.trajectory[4] # index is 0-based and frames are 0-based @@ -190,11 +214,12 @@ def test_frame(self, universe): def test_time(self, universe): universe.trajectory[4] assert_almost_equal( - universe.trajectory.time, 400.0, 3, err_msg="wrong time of frame") + universe.trajectory.time, 400.0, 3, err_msg="wrong time of frame" + ) def test_get_Writer(self, universe, tmpdir): ext = os.path.splitext(self.filename)[1] - outfile = str(tmpdir.join('xdr-reader-test' + ext)) + outfile = str(tmpdir.join("xdr-reader-test" + ext)) with universe.trajectory.Writer(outfile) as W: assert_equal(universe.trajectory.format, W.format) assert_equal(universe.atoms.n_atoms, W.n_atoms) @@ -202,7 +227,7 @@ def test_get_Writer(self, universe, tmpdir): def test_Writer(self, tmpdir): universe = mda.Universe(GRO, self.filename, convert_units=True) ext = os.path.splitext(self.filename)[1] - outfile = str(tmpdir.join('/xdr-reader-test' + ext)) + outfile = str(tmpdir.join("/xdr-reader-test" + ext)) with universe.trajectory.Writer(outfile) as W: W.write(universe.atoms) universe.trajectory.next() @@ -213,8 +238,9 @@ def test_Writer(self, tmpdir): assert_equal(u.trajectory.n_frames, 2) # prec = 6: TRR test fails; here I am generous and take self.prec = # 3... - assert_almost_equal(u.atoms.positions, universe.atoms.positions, - self.prec) + assert_almost_equal( + u.atoms.positions, universe.atoms.positions, self.prec + ) def test_EOFraisesStopIteration(self, universe): def go_beyond_EOF(): @@ -227,11 +253,12 @@ def go_beyond_EOF(): def test_read_next_timestep_ts_no_positions(self, universe): # primarily tests branching on ts.has_positions in _read_next_timestep ts = universe.trajectory[0] - ts.has_positions=False + ts.has_positions = False ts_passed_in = universe.trajectory._read_next_timestep(ts=ts).copy() universe.trajectory.rewind() ts_returned = universe.trajectory._read_next_timestep(ts=None).copy() - assert(ts_passed_in == ts_returned) + assert ts_passed_in == ts_returned + class TestXTCReader(_GromacsReader): filename = XTC @@ -250,11 +277,13 @@ def test_with_statement(self): assert_equal( N, 10, - err_msg="with_statement: XTCReader reads wrong number of frames") + err_msg="with_statement: XTCReader reads wrong number of frames", + ) assert_equal( frames, np.arange(0, N), - err_msg="with_statement: XTCReader does not read all frames") + err_msg="with_statement: XTCReader does not read all frames", + ) class TestTRRReader(_GromacsReader): @@ -266,9 +295,12 @@ def test_velocities(self, universe): # v[47675]={-7.86469e-01, 1.57479e+00, 2.79722e-01} # v[47676]={ 2.70593e-08, 1.08052e-06, 6.97028e-07} v_native = np.array( - [[-7.86469e-01, 1.57479e+00, 2.79722e-01], - [2.70593e-08, 1.08052e-06, 6.97028e-07]], - dtype=np.float32) + [ + [-7.86469e-01, 1.57479e00, 2.79722e-01], + [2.70593e-08, 1.08052e-06, 6.97028e-07], + ], + dtype=np.float32, + ) # velocities in the MDA base unit A/ps (needed for True) v_base = v_native * 10.0 @@ -280,22 +312,26 @@ def test_velocities(self, universe): v_base, self.prec, err_msg="ts._velocities for indices 47675,47676 do not " - "match known values") + "match known values", + ) assert_almost_equal( universe.atoms.velocities[[47675, 47676]], v_base, self.prec, err_msg="velocities for indices 47675,47676 do not " - "match known values") + "match known values", + ) for index, v_known in zip([47675, 47676], v_base): assert_almost_equal( universe.atoms[index].velocity, v_known, self.prec, - err_msg="atom[{0:d}].velocity does not match known values". - format(index)) + err_msg="atom[{0:d}].velocity does not match known values".format( + index + ), + ) class _XDRNoConversion(object): @@ -308,13 +344,15 @@ def universe(self): def test_coordinates(self, universe): # note: these are the native coordinates in nm ca_nm = np.array( - [[6.043369675, 7.385184479, 1.381425762]], dtype=np.float32) + [[6.043369675, 7.385184479, 1.381425762]], dtype=np.float32 + ) universe.trajectory.rewind() universe.trajectory.next() universe.trajectory.next() - assert_equal(universe.trajectory.ts.frame, 2, - "failed to step to frame 3") - ca = universe.select_atoms('name CA and resid 122') + assert_equal( + universe.trajectory.ts.frame, 2, "failed to step to frame 3" + ) + ca = universe.select_atoms("name CA and resid 122") # low precision match because we also look at the trr: only 3 decimals # in nm in xtc! assert_almost_equal( @@ -323,7 +361,8 @@ def test_coordinates(self, universe): 3, err_msg="native coords of Ca of resid 122 " "do not match for frame 3 with " - "convert_units=False") + "convert_units=False", + ) class TestXTCNoConversion(_XDRNoConversion): @@ -337,11 +376,11 @@ class TestTRRNoConversion(_XDRNoConversion): class _GromacsWriter(object): infilename = None # XTC or TRR Writers = { - '.trr': mda.coordinates.TRR.TRRWriter, - '.xtc': mda.coordinates.XTC.XTCWriter, + ".trr": mda.coordinates.TRR.TRRWriter, + ".xtc": mda.coordinates.XTC.XTCWriter, } - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return mda.Universe(GRO, self.infilename) @@ -353,11 +392,13 @@ def Writer(self): @pytest.fixture() def outfile(self, tmpdir): ext = os.path.splitext(self.infilename)[1] - return str(tmpdir.join('xdr-writer-test' + ext)) + return str(tmpdir.join("xdr-writer-test" + ext)) def test_write_trajectory(self, universe, Writer, outfile): """Test writing Gromacs trajectories (Issue 38)""" - with Writer(outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt) as W: + with Writer( + outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt + ) as W: for ts in universe.trajectory: W.write(universe) @@ -371,8 +412,9 @@ def test_write_trajectory(self, universe, Writer, outfile): 3, err_msg="coordinate mismatch between " "original and written trajectory at " - "frame %d (orig) vs %d (written)" % (orig_ts.frame, - written_ts.frame)) + "frame %d (orig) vs %d (written)" + % (orig_ts.frame, written_ts.frame), + ) def test_timestep_not_modified_by_writer(self, universe, Writer, outfile): trj = universe.trajectory @@ -390,9 +432,11 @@ def test_timestep_not_modified_by_writer(self, universe, Writer, outfile): assert_equal( ts._pos, x, - err_msg="Positions in Timestep were modified by writer.") + err_msg="Positions in Timestep were modified by writer.", + ) assert_equal( - ts.time, time, err_msg="Time in Timestep was modified by writer.") + ts.time, time, err_msg="Time in Timestep was modified by writer." + ) class TestXTCWriter(_GromacsWriter): @@ -405,7 +449,9 @@ class TestTRRWriter(_GromacsWriter): infilename = TRR def test_velocities(self, universe, Writer, outfile): - with Writer(outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt) as W: + with Writer( + outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt + ) as W: for ts in universe.trajectory: W.write(universe) @@ -419,13 +465,16 @@ def test_velocities(self, universe, Writer, outfile): 3, err_msg="velocities mismatch between " "original and written trajectory at " - "frame %d (orig) vs %d (written)" % (orig_ts.frame, - written_ts.frame)) + "frame %d (orig) vs %d (written)" + % (orig_ts.frame, written_ts.frame), + ) def test_gaps(self, universe, Writer, outfile): """Tests the writing and reading back of TRRs with gaps in any of the coordinates/velocities properties.""" - with Writer(outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt) as W: + with Writer( + outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt + ) as W: for ts in universe.trajectory: # Inset some gaps in the properties: coords every 4 steps, vels # every 2. @@ -447,10 +496,11 @@ def test_gaps(self, universe, Writer, outfile): err_msg="coordinates mismatch " "between original and written " "trajectory at frame {} (orig) " - "vs {} (written)".format(orig_ts.frame, written_ts.frame)) + "vs {} (written)".format(orig_ts.frame, written_ts.frame), + ) else: with pytest.raises(mda.NoDataError): - getattr(written_ts, 'positions') + getattr(written_ts, "positions") if ts.frame % 2 != 0: assert_almost_equal( @@ -460,23 +510,26 @@ def test_gaps(self, universe, Writer, outfile): err_msg="velocities mismatch " "between original and written " "trajectory at frame {} (orig) " - "vs {} (written)".format(orig_ts.frame, written_ts.frame)) + "vs {} (written)".format(orig_ts.frame, written_ts.frame), + ) else: with pytest.raises(mda.NoDataError): - getattr(written_ts, 'velocities') + getattr(written_ts, "velocities") def test_data_preservation(self, universe, Writer, outfile): - with Writer(outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt) as W: + with Writer( + outfile, universe.atoms.n_atoms, dt=universe.trajectory.dt + ) as W: for ts in universe.trajectory: W.write(universe) uw = mda.Universe(GRO, outfile) - assert np.isclose(ts.data['time'], 0.0) - assert ts.data['step'] == 0 - assert np.isclose(ts.data['lambda'], 0.0) - assert np.isclose(ts.data['dt'], 100.0) + assert np.isclose(ts.data["time"], 0.0) + assert ts.data["step"] == 0 + assert np.isclose(ts.data["lambda"], 0.0) + assert np.isclose(ts.data["dt"], 100.0) # check that the data are identical for each time step for orig_ts, written_ts in zip(universe.trajectory, uw.trajectory): @@ -487,21 +540,23 @@ def test_data_preservation(self, universe, Writer, outfile): for k in orig_ts.data: assert k in written_ts.data - err_msg = ('mismatch between ' - 'original and written trajectory at ' - f'frame {orig_ts.frame} vs {written_ts.frame}') + err_msg = ( + "mismatch between " + "original and written trajectory at " + f"frame {orig_ts.frame} vs {written_ts.frame}" + ) # check that each value is the same for k in orig_ts.data: - assert_allclose(orig_ts.data[k], - written_ts.data[k], - err_msg=err_msg) + assert_allclose( + orig_ts.data[k], written_ts.data[k], err_msg=err_msg + ) class _GromacsWriterIssue101(object): Writers = { - '.trr': mda.coordinates.TRR.TRRWriter, - '.xtc': mda.coordinates.XTC.XTCWriter, + ".trr": mda.coordinates.TRR.TRRWriter, + ".xtc": mda.coordinates.XTC.XTCWriter, } ext = None # set to '.xtc' or '.trr' prec = 3 @@ -512,7 +567,7 @@ def Writer(self): @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir.join('/xdr-writer-issue101' + self.ext)) + return str(tmpdir.join("/xdr-writer-issue101" + self.ext)) def test_single_frame_GRO(self, Writer, outfile): self._single_frame(GRO, Writer, outfile) @@ -528,13 +583,17 @@ def _single_frame(self, filename, Writer, outfile): with Writer(outfile, u.atoms.n_atoms) as W: W.write(u.atoms) w = mda.Universe(filename, outfile) - assert_equal(w.trajectory.n_frames, 1, - "single frame trajectory has wrong number of frames") + assert_equal( + w.trajectory.n_frames, + 1, + "single frame trajectory has wrong number of frames", + ) assert_almost_equal( w.atoms.positions, u.atoms.positions, self.prec, - err_msg="coordinates do not match for {0!r}".format(filename)) + err_msg="coordinates do not match for {0!r}".format(filename), + ) class TestXTCWriterSingleFrame(_GromacsWriterIssue101): @@ -548,6 +607,7 @@ class TestTRRWriterSingleFrame(_GromacsWriterIssue101): class _GromacsWriterIssue117(object): """Issue 117: Cannot write XTC or TRR from AMBER NCDF""" + ext = None prec = 5 @@ -558,7 +618,7 @@ def universe(self): @pytest.mark.filterwarnings("ignore: ATOMIC_NUMBER record not found") def test_write_trajectory(self, universe, tmpdir): """Test writing Gromacs trajectories from AMBER NCDF (Issue 117)""" - outfile = str(tmpdir.join('xdr-writer-issue117' + self.ext)) + outfile = str(tmpdir.join("xdr-writer-issue117" + self.ext)) with mda.Writer(outfile, n_atoms=universe.atoms.n_atoms) as W: for ts in universe.trajectory: W.write(universe) @@ -571,9 +631,12 @@ def test_write_trajectory(self, universe, tmpdir): written_ts._pos, orig_ts._pos, self.prec, - err_msg=("coordinate mismatch between original and written " - f"trajectory at frame {orig_ts.frame:d} (orig) vs " - f"{orig_ts.frame:d} (written)")) + err_msg=( + "coordinate mismatch between original and written " + f"trajectory at frame {orig_ts.frame:d} (orig) vs " + f"{orig_ts.frame:d} (written)" + ), + ) class TestXTCWriterIssue117(_GromacsWriterIssue117): @@ -596,7 +659,8 @@ def test_triclinic_box(): new_unitcell, unitcell, 3, - err_msg="unitcell round-trip connversion failed (Issue 61)") + err_msg="unitcell round-trip connversion failed (Issue 61)", + ) class XTCReference(BaseReference): @@ -606,7 +670,7 @@ def __init__(self): self.topology = COORDINATES_TOPOLOGY self.reader = mda.coordinates.XTC.XTCReader self.writer = mda.coordinates.XTC.XTCWriter - self.ext = 'xtc' + self.ext = "xtc" self.prec = 3 self.changing_dimensions = True @@ -625,7 +689,7 @@ def ref(): return XTCReference() def test_different_precision(self, ref, tmpdir): - out = 'precision-test' + ref.ext + out = "precision-test" + ref.ext # store more then 9 atoms to enable compression n_atoms = 40 with tmpdir.as_cwd(): @@ -648,7 +712,7 @@ def __init__(self): self.changing_dimensions = True self.reader = mda.coordinates.TRR.TRRReader self.writer = mda.coordinates.TRR.TRRWriter - self.ext = 'trr' + self.ext = "trr" self.prec = 3 self.first_frame.velocities = self.first_frame.positions / 10 self.first_frame.forces = self.first_frame.positions / 100 @@ -687,17 +751,19 @@ def ref(): # tests writing and reading in one! def test_lambda(self, ref, universe, tmpdir): - outfile = 'write-lambda-test' + ref.ext + outfile = "write-lambda-test" + ref.ext with tmpdir.as_cwd(): with ref.writer(outfile, universe.trajectory.n_atoms) as W: for i, ts in enumerate(universe.trajectory): - ts.data['lambda'] = i / float(universe.trajectory.n_frames) + ts.data["lambda"] = i / float(universe.trajectory.n_frames) W.write(universe) reader = ref.reader(outfile) for i, ts in enumerate(reader): - assert_almost_equal(ts.data['lambda'], i / float(reader.n_frames)) + assert_almost_equal( + ts.data["lambda"], i / float(reader.n_frames) + ) class _GromacsReader_offsets(object): @@ -705,19 +771,20 @@ class _GromacsReader_offsets(object): # This base class assumes same lengths and dt for XTC and TRR test cases! filename = None ref_unitcell = np.array( - [80.017, 80.017, 80.017, 60., 60., 90.], dtype=np.float32) + [80.017, 80.017, 80.017, 60.0, 60.0, 90.0], dtype=np.float32 + ) # computed with Gromacs: 362.26999999999998 nm**3 * 1000 A**3/nm**3 ref_volume = 362270.0 ref_offsets = None _reader = None prec = 3 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def traj(self, tmpdir_factory): # copy of original test trajectory in a temporary folder. This is # needed since offsets are automatically generated in the same # directory. Here we also clean up nicely all files we generate - tmpdir = tmpdir_factory.mktemp('xtc') + tmpdir = tmpdir_factory.mktemp("xtc") shutil.copy(self.filename, str(tmpdir)) traj = str(tmpdir.join(os.path.basename(self.filename))) # ensure initialization of offsets @@ -733,30 +800,34 @@ def test_offsets(self, trajectory, traj): assert_almost_equal( trajectory._xdr.offsets, self.ref_offsets, - err_msg="wrong frame offsets") + err_msg="wrong frame offsets", + ) outfile_offsets = XDR.offsets_filename(traj) saved_offsets = XDR.read_numpy_offsets(outfile_offsets) - assert isinstance(saved_offsets, dict), \ - "read_numpy_offsets did not return a dict" + assert isinstance( + saved_offsets, dict + ), "read_numpy_offsets did not return a dict" assert_almost_equal( trajectory._xdr.offsets, - saved_offsets['offsets'], - err_msg="error saving frame offsets") + saved_offsets["offsets"], + err_msg="error saving frame offsets", + ) assert_almost_equal( self.ref_offsets, - saved_offsets['offsets'], - err_msg="saved frame offsets don't match " - "the known ones") + saved_offsets["offsets"], + err_msg="saved frame offsets don't match " "the known ones", + ) trajectory._load_offsets() assert_almost_equal( trajectory._xdr.offsets, self.ref_offsets, - err_msg="error loading frame offsets") - assert_equal(saved_offsets['ctime'], os.path.getctime(traj)) - assert_equal(saved_offsets['size'], os.path.getsize(traj)) + err_msg="error loading frame offsets", + ) + assert_equal(saved_offsets["ctime"], os.path.getctime(traj)) + assert_equal(saved_offsets["size"], os.path.getsize(traj)) def test_reload_offsets(self, traj): self._reader(traj, refresh_offsets=True) @@ -766,8 +837,12 @@ def test_nonexistent_offsets_file(self, traj): outfile_offsets = XDR.offsets_filename(traj) with patch.object(np, "load") as np_load_mock: np_load_mock.side_effect = IOError - with pytest.warns(UserWarning, match=re.escape( - f"Failed to load offsets file {outfile_offsets}")): + with pytest.warns( + UserWarning, + match=re.escape( + f"Failed to load offsets file {outfile_offsets}" + ), + ): saved_offsets = XDR.read_numpy_offsets(outfile_offsets) assert saved_offsets == False @@ -777,8 +852,12 @@ def test_corrupted_offsets_file(self, traj): outfile_offsets = XDR.offsets_filename(traj) with patch.object(np, "load") as np_load_mock: np_load_mock.side_effect = ValueError - with pytest.warns(UserWarning, match=re.escape( - f"Failed to load offsets file {outfile_offsets}")): + with pytest.warns( + UserWarning, + match=re.escape( + f"Failed to load offsets file {outfile_offsets}" + ), + ): saved_offsets = XDR.read_numpy_offsets(outfile_offsets) assert saved_offsets == False @@ -788,16 +867,18 @@ def test_reload_offsets_if_offsets_readin_io_fails(self, trajectory): # ensure that offsets are then read-in from the trajectory with patch.object(np, "load") as np_load_mock: np_load_mock.side_effect = IOError - with (pytest.warns(UserWarning, - match="Failed to load offsets file") and - pytest.warns(UserWarning, - match="reading offsets from trajectory instead")): + with pytest.warns( + UserWarning, match="Failed to load offsets file" + ) and pytest.warns( + UserWarning, match="reading offsets from trajectory instead" + ): trajectory._load_offsets() assert_almost_equal( trajectory._xdr.offsets, self.ref_offsets, - err_msg="error loading frame offsets") + err_msg="error loading frame offsets", + ) def test_reload_offsets_if_offsets_readin_value_fails(self, trajectory): # force the np.load call that is called in read_numpy_offsets @@ -810,7 +891,8 @@ def test_reload_offsets_if_offsets_readin_value_fails(self, trajectory): assert_almost_equal( trajectory._xdr.offsets, self.ref_offsets, - err_msg="error loading frame offsets") + err_msg="error loading frame offsets", + ) def test_persistent_offsets_size_mismatch(self, traj): # check that stored offsets are not loaded when trajectory @@ -818,11 +900,12 @@ def test_persistent_offsets_size_mismatch(self, traj): fname = XDR.offsets_filename(traj) saved_offsets = XDR.read_numpy_offsets(fname) - assert isinstance(saved_offsets, dict), \ - "read_numpy_offsets did not return a dict" + assert isinstance( + saved_offsets, dict + ), "read_numpy_offsets did not return a dict" - saved_offsets['size'] += 1 - with open(fname, 'wb') as f: + saved_offsets["size"] += 1 + with open(fname, "wb") as f: np.savez(f, **saved_offsets) with pytest.warns(UserWarning, match="Reload offsets"): @@ -834,11 +917,12 @@ def test_persistent_offsets_ctime_mismatch(self, traj): fname = XDR.offsets_filename(traj) saved_offsets = XDR.read_numpy_offsets(fname) - assert isinstance(saved_offsets, dict), \ - "read_numpy_offsets did not return a dict" + assert isinstance( + saved_offsets, dict + ), "read_numpy_offsets did not return a dict" - saved_offsets['ctime'] += 1 - with open(fname, 'wb') as f: + saved_offsets["ctime"] += 1 + with open(fname, "wb") as f: np.savez(f, **saved_offsets) with pytest.warns(UserWarning, match="Reload offsets"): @@ -850,10 +934,11 @@ def test_persistent_offsets_natoms_mismatch(self, traj): fname = XDR.offsets_filename(traj) saved_offsets = XDR.read_numpy_offsets(fname) - assert isinstance(saved_offsets, dict), \ - "read_numpy_offsets did not return a dict" + assert isinstance( + saved_offsets, dict + ), "read_numpy_offsets did not return a dict" - saved_offsets['n_atoms'] += 1 + saved_offsets["n_atoms"] += 1 np.savez(fname, **saved_offsets) with pytest.warns(UserWarning, match="Reload offsets"): @@ -863,11 +948,12 @@ def test_persistent_offsets_last_frame_wrong(self, traj): fname = XDR.offsets_filename(traj) saved_offsets = XDR.read_numpy_offsets(fname) - assert isinstance(saved_offsets, dict), \ - "read_numpy_offsets did not return a dict" + assert isinstance( + saved_offsets, dict + ), "read_numpy_offsets did not return a dict" idx_frame = 3 - saved_offsets['offsets'][idx_frame] += 42 + saved_offsets["offsets"][idx_frame] += 42 np.savez(fname, **saved_offsets) with pytest.warns(UserWarning, match="seek failed"): @@ -878,11 +964,12 @@ def test_unsupported_format(self, traj): fname = XDR.offsets_filename(traj) saved_offsets = XDR.read_numpy_offsets(fname) - assert isinstance(saved_offsets, dict), \ - "read_numpy_offsets did not return a dict" + assert isinstance( + saved_offsets, dict + ), "read_numpy_offsets did not return a dict" idx_frame = 3 - saved_offsets.pop('n_atoms') + saved_offsets.pop("n_atoms") np.savez(fname, **saved_offsets) # ok as long as this doesn't throw @@ -894,28 +981,32 @@ def test_unsupported_format(self, traj): def test_persistent_offsets_readonly(self, tmpdir): shutil.copy(self.filename, str(tmpdir)) - if os.name == 'nt': + if os.name == "nt": # Windows platform has a unique way to deny write access - subprocess.call("icacls {fname} /deny Users:W".format(fname=tmpdir), - shell=True) + subprocess.call( + "icacls {fname} /deny Users:W".format(fname=tmpdir), shell=True + ) else: os.chmod(str(tmpdir), 0o555) filename = str(tmpdir.join(os.path.basename(self.filename))) # try to write a offsets file - with (pytest.warns(UserWarning, match="Couldn't save offsets") and - pytest.warns(UserWarning, match="Cannot write")): + with pytest.warns( + UserWarning, match="Couldn't save offsets" + ) and pytest.warns(UserWarning, match="Cannot write"): self._reader(filename) assert_equal(os.path.exists(XDR.offsets_filename(filename)), False) # check the lock file is not created as well. - assert_equal(os.path.exists(XDR.offsets_filename(filename, - ending='.lock')), False) + assert_equal( + os.path.exists(XDR.offsets_filename(filename, ending=".lock")), + False, + ) # pre-teardown permission fix - leaving permission blocked dir # is problematic on py3.9 + Windows it seems. See issue # [4123](https://github.com/MDAnalysis/mdanalysis/issues/4123) # for more details. - if os.name == 'nt': + if os.name == "nt": subprocess.call(f"icacls {tmpdir} /grant Users:W", shell=True) else: os.chmod(str(tmpdir), 0o777) @@ -923,27 +1014,48 @@ def test_persistent_offsets_readonly(self, tmpdir): shutil.rmtree(tmpdir) def test_offset_lock_created(self): - assert os.path.exists(XDR.offsets_filename(self.filename, - ending='lock')) + assert os.path.exists( + XDR.offsets_filename(self.filename, ending="lock") + ) class TestXTCReader_offsets(_GromacsReader_offsets): __test__ = True filename = XTC - ref_offsets = np.array([ - 0, 165188, 330364, 495520, 660708, 825872, 991044, 1156212, 1321384, - 1486544 - ]) + ref_offsets = np.array( + [ + 0, + 165188, + 330364, + 495520, + 660708, + 825872, + 991044, + 1156212, + 1321384, + 1486544, + ] + ) _reader = mda.coordinates.XTC.XTCReader class TestTRRReader_offsets(_GromacsReader_offsets): __test__ = True filename = TRR - ref_offsets = np.array([ - 0, 1144464, 2288928, 3433392, 4577856, 5722320, 6866784, 8011248, - 9155712, 10300176 - ]) + ref_offsets = np.array( + [ + 0, + 1144464, + 2288928, + 3433392, + 4577856, + 5722320, + 6866784, + 8011248, + 9155712, + 10300176, + ] + ) _reader = mda.coordinates.TRR.TRRReader diff --git a/testsuite/MDAnalysisTests/coordinates/test_xyz.py b/testsuite/MDAnalysisTests/coordinates/test_xyz.py index ac68e4312cf..c9e5eb82418 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_xyz.py +++ b/testsuite/MDAnalysisTests/coordinates/test_xyz.py @@ -32,12 +32,16 @@ from MDAnalysis.coordinates.XYZ import XYZWriter from MDAnalysisTests.datafiles import COORDINATES_XYZ, COORDINATES_XYZ_BZ2 -from MDAnalysisTests.coordinates.base import (MultiframeReaderTest, BaseReference, - BaseWriterTest) +from MDAnalysisTests.coordinates.base import ( + MultiframeReaderTest, + BaseReference, + BaseWriterTest, +) from MDAnalysisTests import make_Universe from MDAnalysis import __version__ + class XYZReference(BaseReference): def __init__(self): super(XYZReference, self).__init__() @@ -46,7 +50,7 @@ def __init__(self): self.topology = COORDINATES_XYZ self.reader = mda.coordinates.XYZ.XYZReader self.writer = mda.coordinates.XYZ.XYZWriter - self.ext = 'xyz' + self.ext = "xyz" self.volume = 0 self.dimensions = None self.container_format = True @@ -71,9 +75,9 @@ def ref(): return XYZReference() def test_write_selection(self, ref, universe, reader, tmpdir): - sel_str = 'name CA' + sel_str = "name CA" sel = universe.select_atoms(sel_str) - outfile = 'write-selection-test' + ref.ext + outfile = "write-selection-test" + ref.ext with tmpdir.as_cwd(): with ref.writer(outfile, sel.n_atoms) as W: @@ -83,13 +87,17 @@ def test_write_selection(self, ref, universe, reader, tmpdir): copy = ref.reader(outfile) for orig_ts, copy_ts in zip(universe.trajectory, copy): assert_almost_equal( - copy_ts._pos, sel.atoms.positions, ref.prec, + copy_ts._pos, + sel.atoms.positions, + ref.prec, err_msg="coordinate mismatch between original and written " - "trajectory at frame {} (orig) vs {} (copy)".format( - orig_ts.frame, copy_ts.frame)) + "trajectory at frame {} (orig) vs {} (copy)".format( + orig_ts.frame, copy_ts.frame + ), + ) def test_write_different_models_in_trajectory(self, ref, universe, tmpdir): - outfile = 'write-models-in-trajectory' + ref.ext + outfile = "write-models-in-trajectory" + ref.ext # n_atoms should match for each TimeStep if it was specified with tmpdir.as_cwd(): with ref.writer(outfile, n_atoms=4) as w: @@ -97,19 +105,25 @@ def test_write_different_models_in_trajectory(self, ref, universe, tmpdir): w.write(universe) def test_no_conversion(self, ref, universe, reader, tmpdir): - outfile = 'write-no-conversion' + ref.ext + outfile = "write-no-conversion" + ref.ext with tmpdir.as_cwd(): with ref.writer(outfile, convert_units=False) as w: for ts in universe.trajectory: w.write(universe) self._check_copy(outfile, ref, reader) - @pytest.mark.parametrize("remarkout, remarkin", - [ + @pytest.mark.parametrize( + "remarkout, remarkin", + [ 2 * ["Curstom Remark"], 2 * [""], - [None, "frame 0 | Written by MDAnalysis XYZWriter (release {0})".format(__version__)], - ] + [ + None, + "frame 0 | Written by MDAnalysis XYZWriter (release {0})".format( + __version__ + ), + ], + ], ) def test_remark(self, universe, remarkout, remarkin, ref, tmpdir): outfile = "write-remark.xyz" @@ -138,7 +152,7 @@ class XYZ_BZ_Reference(XYZReference): def __init__(self): super(XYZ_BZ_Reference, self).__init__() self.trajectory = COORDINATES_XYZ_BZ2 - self.ext = 'xyz.bz2' + self.ext = "xyz.bz2" class Test_XYZBZReader(TestXYZReader): @@ -158,12 +172,12 @@ def ref(): class TestXYZWriterNames(object): @pytest.fixture() def outfile(self, tmpdir): - return str(tmpdir.join('/outfile.xyz')) + return str(tmpdir.join("/outfile.xyz")) def test_no_names(self, outfile): """Atoms should all be named 'X'""" u = make_Universe(trajectory=True) - + wmsg = "does not have atom elements or names" with pytest.warns(UserWarning, match=wmsg): @@ -172,29 +186,29 @@ def test_no_names(self, outfile): w.close() u2 = mda.Universe(outfile) - assert all(u2.atoms.names == 'X') + assert all(u2.atoms.names == "X") @pytest.mark.parametrize("attr", ["elements", "names"]) def test_elements_or_names(self, outfile, attr): u = mda.Universe.empty(n_atoms=5, trajectory=True) - u.add_TopologyAttr(attr, values=['Te', 'S', 'Ti', 'N', 'Ga']) + u.add_TopologyAttr(attr, values=["Te", "S", "Ti", "N", "Ga"]) with mda.Writer(outfile) as w: w.write(u) with open(outfile, "r") as r: - names = ''.join(l.split()[0].strip() for l in r.readlines()[2:-1]) + names = "".join(l.split()[0].strip() for l in r.readlines()[2:-1]) - assert names[:-1].lower() == 'testing' + assert names[:-1].lower() == "testing" def test_elements_and_names(self, outfile): """Should always default to elements over names""" u = mda.Universe.empty(n_atoms=5, trajectory=True) - u.add_TopologyAttr('elements', values=['Te', 'S', 'Ti', 'N', 'Ga']) - u.add_TopologyAttr('names', values=['A', 'B', 'C', 'D', 'E']) + u.add_TopologyAttr("elements", values=["Te", "S", "Ti", "N", "Ga"]) + u.add_TopologyAttr("names", values=["A", "B", "C", "D", "E"]) with mda.Writer(outfile) as w: w.write(u) diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 8e422352e11..8c12d164e7f 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -164,6 +164,7 @@ setup\.py | MDAnalysisTests/transformations/.*\.py | MDAnalysisTests/guesser/.*\.py | MDAnalysisTests/converters/.*\.py +| MDAnalysisTests/coordinates/.*\.py | MDAnalysisTests/data/.*\.py | MDAnalysisTests/formats/.*\.py | MDAnalysisTests/parallelism/.*\.py From 5656fe406ea96dd3e561f4cc6c130693a1304032 Mon Sep 17 00:00:00 2001 From: Tyler Reddy Date: Fri, 27 Dec 2024 18:54:46 -0700 Subject: [PATCH 47/58] ENH: support tpx 134 (GMX 2024.4) (#4866) * ENH: support tpx 134 (GMX 2024.4) * Fixes gh-4855. * Add support for reading topology data from `.tpr` files produced by GROMACS 2024.4, corresponding to tpx version `134`. * My approach was similar to the one I used in gh-4523, except for two things: 1) I retrieved the current generation (`28`) from a `print` in our own binary parser rather than rebuilding GMX from source with added `printf`, and more importantly 2) I had to add some shims because the `.tpr` format has changed. * With regard to the additional field data now stored in the `.tpr` format, I was involved in reviewing it upstream at: https://gitlab.com/gromacs/gromacs/-/merge_requests/4544. You can see that the changes related to the MARTINI functional form made it into the `v2024.4` tag i.e., here: https://gitlab.com/gromacs/gromacs/-/blob/v2024.4/src/gromacs/gmxpreprocess/convparm.cpp?ref_type=tags#L372 * DOC: PR 4866 revisions * Remove the periods (`.`) from Tyler's identifier in the `CHANGELOG` to satisfy reviewer comments. --- package/CHANGELOG | 4 +++- package/MDAnalysis/topology/TPRParser.py | 1 + package/MDAnalysis/topology/tpr/setting.py | 3 ++- package/MDAnalysis/topology/tpr/utils.py | 9 +++++++++ .../data/tprs/2lyz_gmx_2024_4.tpr | Bin 0 -> 515484 bytes .../data/tprs/all_bonded/dummy_2024_4.tpr | Bin 0 -> 5188 bytes .../extra-interactions-2024_4.tpr | Bin 0 -> 16124 bytes testsuite/MDAnalysisTests/datafiles.py | 7 ++++++- .../MDAnalysisTests/topology/test_tprparser.py | 11 +++++++---- 9 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 testsuite/MDAnalysisTests/data/tprs/2lyz_gmx_2024_4.tpr create mode 100644 testsuite/MDAnalysisTests/data/tprs/all_bonded/dummy_2024_4.tpr create mode 100644 testsuite/MDAnalysisTests/data/tprs/virtual_sites/extra-interactions-2024_4.tpr diff --git a/package/CHANGELOG b/package/CHANGELOG index 909020a8661..7b85922487a 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,11 +14,13 @@ The rules for this file: ------------------------------------------------------------------------------- -??/??/?? IAlibay, ChiahsinChu, RMeli, tanishy7777, talagayev +??/??/?? IAlibay, ChiahsinChu, RMeli, tanishy7777, talagayev, + tylerjereddy * 2.9.0 Fixes + * Add support for TPR files produced by GROMACS 2024.4 * 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 diff --git a/package/MDAnalysis/topology/TPRParser.py b/package/MDAnalysis/topology/TPRParser.py index 022af574a28..5c648016e87 100644 --- a/package/MDAnalysis/topology/TPRParser.py +++ b/package/MDAnalysis/topology/TPRParser.py @@ -67,6 +67,7 @@ 127 28 2022 yes 129 28 2023 yes 133 28 2024.1 yes + 134 28 2024.4 yes ========== ============== ==================== ===== .. [*] Files generated by the beta versions of Gromacs 2020 are NOT supported. diff --git a/package/MDAnalysis/topology/tpr/setting.py b/package/MDAnalysis/topology/tpr/setting.py index 89f8ffa09aa..ebb749d588d 100644 --- a/package/MDAnalysis/topology/tpr/setting.py +++ b/package/MDAnalysis/topology/tpr/setting.py @@ -38,7 +38,8 @@ """ #: Gromacs TPR file format versions that can be read by the TPRParser. -SUPPORTED_VERSIONS = (58, 73, 83, 100, 103, 110, 112, 116, 119, 122, 127, 129, 133) +SUPPORTED_VERSIONS = (58, 73, 83, 100, 103, 110, 112, + 116, 119, 122, 127, 129, 133, 134) # Some constants STRLEN = 4096 diff --git a/package/MDAnalysis/topology/tpr/utils.py b/package/MDAnalysis/topology/tpr/utils.py index 9ba7d8e63ab..98b4a4bb845 100644 --- a/package/MDAnalysis/topology/tpr/utils.py +++ b/package/MDAnalysis/topology/tpr/utils.py @@ -481,6 +481,10 @@ def do_iparams(data, functypes, fver): elif i in [setting.F_RESTRANGLES]: data.unpack_real() # harmonic.rA data.unpack_real() # harmonic.krA + if fver >= 134: + data.unpack_real() # harmonic.rB + data.unpack_real() # harmonic.krB + elif i in [setting.F_LINEAR_ANGLES]: data.unpack_real() # linangle.klinA data.unpack_real() # linangle.aA @@ -598,6 +602,9 @@ def do_iparams(data, functypes, fver): elif i in [setting.F_RESTRDIHS]: data.unpack_real() # pdihs.phiA data.unpack_real() # pdihs.cpA + if fver >= 134: + data.unpack_real() # pdihs.phiB + data.unpack_real() # pdihs.cpB elif i in [setting.F_DISRES]: data.unpack_int() # disres.label data.unpack_int() # disres.type @@ -640,6 +647,8 @@ def do_iparams(data, functypes, fver): elif i in [setting.F_CBTDIHS]: ndo_real(data, setting.NR_CBTDIHS) # cbtdihs.cbtcA + if fver >= 134: + ndo_real(data, setting.NR_CBTDIHS) # cbtdihs.cbtcB elif i in [setting.F_RBDIHS]: ndo_real(data, setting.NR_RBDIHS) # iparams_rbdihs_rbcA diff --git a/testsuite/MDAnalysisTests/data/tprs/2lyz_gmx_2024_4.tpr b/testsuite/MDAnalysisTests/data/tprs/2lyz_gmx_2024_4.tpr new file mode 100644 index 0000000000000000000000000000000000000000..9fd71ce8fa6023f3fc11e0a7ad8eb051e402bd71 GIT binary patch literal 515484 zcmeEv2b?8E)%^=Y&amXPBuOGWFYe5+6LuCDU<0rqf=JFH8Id4jz=VncRKNfRKu|;l z#lY??ilTzZr-Fzi11cga0{Y2^{LiUZr(R9bRj+6Ft%84Me@)f7=ib{@w{F+%?)PRW z6bh>q3WZf)x%1xhcbhkNYGqob_KF%Lg+dRGx8v9d$MQIqJLSmZk396$BMI-pe@Zy? zQ$)uBQ4DfJqKg%)tF0jgL78g=*L1P7=1O9S2g(3+>wbxs0GugxoOOu8ECp`jP4mQnnr1Kmw?fPN~6IwSPN!U3uaUc zW>nLRN~605=Wufy4Mtqo+)8sbb{purW+^PbG#bRGuDMB5GZGHwQ4i)(59U$VJY=uY zTino?g*DRBydWnU8nZMt($eTY6C>0J#@x`DOQRtZjJ^?!z7dSR5sbbWjK1kcKN@NV zqi?oHADp8ZjIpWF+j%!N=F(^|+GdDD&%v0c2cw^^(c3dk55_!QV-_Ebbb2t-=^Cl@ zgYiud#y33}Utci3zF>TP!T9=u@%07c>kG!$r}33WPMMfNeHv+LkC4%rORwU60e|f5 z`X-LP*HzsX-#cyMXnUtk9BuEkiKFeEHgUAQ(==N7(-?&{GT~r6?czCazt+I*$a{Mg z*V0(X1Y-`$Xs;`yr8%KxF#6tL^u598dxO!p%jmq>6G!XHsGVoKSdJbaXa*4uN@Q

C}TI2#Q^AQ0#hxV%J;ONP9;2IXLhg z6u#a@kPbn)>uo6X#fkSEy+O6>b=A%Wp%GkpL5=GTYFuwn<9dS{*XwGW_1_GF>MEP~ zVB$eN>vi=^`oT`nR1hs6RIT2iYV`(Ht2d}xz0-s71XZgys9L?#gKOVatI5f?)eL&318vuv@v>CWUan3yNJOD0Y>g*j0jJ zSLq8P92C21Q0%Hfv8x8ft{N1(YEbN|L9wd_#jY9@yJ}GEszI@dIbe?sgMy>cLE0@D$qne$%D{H0Ey(Vn?)u6ytg929# z3S2cPaMhr|Rb7EAjqWk=OhKuux>9GuUw8LFrLiE;K^3e9Rj?XV!D>(it3eg4x+-YH z<7%I9kSXr*OneaTpyE}7idPLPUe#4R`I~qbQFUd`@S~)^ie@vo5`qd>4JuqUsBqPw!c~I`R}Cs$HK=gapu$yy3Rev(Ts5e0)u6&vg9=v- zDqPi7xZ<7@lh#!>yAnYms|JOv>IzwDmmMZ}s<{$X9z8y!bVFO7c}hqr6HB{=jP8HR zvya_AIF-_~JonI$ns{Z^g2GmFg{`zlXsHrh-utkSQu>xhPYozn_YBf_hgA>Rm0UcQse<>=1)0S94WPe0YAPuz2OI#X)VW1+}f_YMb z)q*lrb7iVDfB%U& zN>vLgRV}DgwV+bfg3442DpSo>rt6%*DTUK*YFEt7ii$W;#>x$40qSKU2wl}7iTkQ{Yaqe`Q%3>eL$G`e5F zOuW?V?t!c{`l^YVu0q)v1rKEP;DM|jJdo9c2eNulaq2 zrXEz7dQf5NL4~OY6{a3kn0ioQ>OqC62Nk9sRG7M}FgBh+U8x6krS9sA^e4V8tOpNO z_28kZ9z0algCbK8icCEyGIdvEN~7~9W=2qM>Or}w2j!+7JWADrN2$7dlq!u*ER^-2 z@YI9CQx6JHJ$RI=yGJP-!=UiggThk}3Qs*KJoTXP)PurP4+>8`C_MF`@YI7hg!P~h z)q_G*4+>E|C`9$35Y>Z1R1XSKJt#zVSBPx9T=@~6Saj+^(WwVTrydlYdQf!gLD8uP zMW-GVoqAAo>Os+|2SukI6rFldbm~FTsRxfo^`HpVgCbOSMaae{s62I7dBg|t3+hij zc)eE->QCL(pO(KyP<0x?>%B%$YZ^hVX#};V;cAWiPuz1F?kPz8#A?!T)x`2a4QT{5 zq!HARMo>c|rs40!0rZj?@(gNjbOiR1n;LB!G7Ba_S;6V-!_8%wh`>N4Y%L+j2vE__!gq!_M)DBj;V#5 zuKS*m*FDcp509nMiB`>Ew`vBvRnzTOJ+luDyf)p=)id{q=W%+x?AiJB=W*I`dv<-k zop!gL{SSMdo$jDpnbZtE$Y=&TUDNG!rO}BGYRzEJYX*B>GuZQ*!JgL)_Pl1W=QV>p zuNmxl&0x=K276x9?Rlk{r%brdYX-YtGuZ8#!EVx+_L{-A*9^A3X0YuwgKe)FYF97rfn|oidx%UN|dtb1*_XV4KU$D9N1zT`mum$%8 zTX0{n1@{G8a9^+m_qi?D1|Zn^`huOWFWC9|f}O7~*!lW`ov$z0ulj=hsxR2D`hxwc zFW9g8g8iy5*suD6{i-k6uln46W#{j9DPg_l6Q1}gvM<=1`hvZw&+Sb;bLP6=k78eb zmYFj;R@GOB4Nyw=jGpd30P5N0$aepL6I{PEUzKx(Jx@9Dv?Gr_;b8gdnZ3XJ6{gad zrcrVyE|1NW5M2$YJhp3Fx@%iHt1X=+X=!w4Teiz%JGc3r#h1qpXiE?9q`KLyhlZBJ#?O{FxZ z7t7^cXR9Qa$3`?dbal4!$lm(ZrqW!UtJFU0T(=fIV`JX*#@zJE?d*VxH>I)mlt%ZT zuM?K$>wFlS-mjJCwWl%9O`|+}XDO7%WYse^%NxclHw?-ZW%Rvq%xaGVa_N*uW-IrQ zP)<5$=Y4FJl;-Q#8<8*Kd&YK~@66`5S7dH`&?EEpiCuYKTVY;%$wubuj-oubE!}7T zy>;R-{Vrd5{?1M~PXQ?Jv$s3J+;-ST-5ATG?d%;rKqD`Yde>6ByU&=;P@d;987re9 z_KfZ9g>q*X%JOcrl`ZA5S?(lz?ydW%(ma=K^WATcl*jgS(y{%N53!$&aQ3g97UpMbi_%^vv5o(`_EA%mYsj*siz(JI_q^gyT@6mEpG>{ z?sB6$9D4jo#~f<;&0(dsOm0`0f9w%Q z`gSWhfjjj|9NG?Z4n6hMBNc;{PB`&|9VU)=nf(qu?Z{I!9P+-P=aeH)Jq^FFy&M$Q z#qT0Nf_AHyj+bKaQVzV7125&kOF8gT4!o2Di;)8>!sfr_SPOH}F$MjXj(=YaHbcB$ zNXH4?TQm0sC7zPae&#{17*xqrPA=GH#)@YemCY`b;sjq{#zdSAHWkIm)Z zInUYObL+FsX@B{M83@N=?Fag*Uu*1i&?lUH=26oc^DbC^VCK~88?QQJq2w>tR`JsK z2bNj0);Q~p*Y=lg>#Kd`ga?HO=AQIena-;?Vf}cmMS->)TJ8)xXL{>(q{GTrE4t zul(-s)V^Q4ZTcU!FIRqY?z+?e`uGnj*B^AM(|gmG9Z8fAERJYL7nh2B&w| z<%7*PHoqkMol3)dH9s?j`;z$?u=WE7f3(oJ?hBVX`Eg%&B?bI*tb!>di#Mb3jb)Vu-6-% z{6)GC>Uwwcc#-TU+V9=C?S9m6uWz?MDgU&dsuSA3*8lw3jgOyG>-*P}d(}Ssu6_D@ zW<6AU_eyV<|A7zGXEvwYxzm8`Z#Z?RIehOY-FdD3zy`nCu`y@v_nmz64^0@v)8QccgG!)_b>a)6Kk)#b%p+AKi^+F<6q}Hxvj^- zsu$XSg|$EEdQ%;Dyp6BMJN?(!KUX=Ww3qby&;0ue_0maSb@rF-`Cxt9l@8Z=wLbmJ z%sa7Gefk$pzS85B+Ad?2{*}J?Z4X3 zbe|;N=CATW@s^=-h`ivepVe!%Vz{q}n6 zxA`kQ#aH$_&Hv4D&BwM`&&A8y58U{`pBm2`d!mys_}xm)70;VBu;5d-HCI3FPm;e_ zTkWqJzxL}F`u_dA=L>uPtNTga59@wM_Ybmjysa1gcK_9W=Jf3K=Iq0Ku=WERZLn`+ znV-DH#c_*Y+}Bv^l+O%oary3z^}qi!$zQDPfPH?``1O3D`*q;~-B0TNOwWCK95D)`c57^Z|ZqS&lkeF|JD7Z zo>Luf?;li;Ro|SR-G7~Zus`VjBg_YlcVPRsuF{zHjbk+K7i&B7zCvO2gC~8)wGSM1 z)U2%!+;*ng`;R(m^ka`4HB;>a)#|MB55G54?Ze=o-291|YVWz^($ROXdE!j9f4Th* z<4^4$V815Lr}in!tNkh;3`m4{TbVhvQNEH0I0ksQs&tlt-7OGW_SMAIP?D^IH(qgs0vh!5SeBk`l z-V6N5CO6Gg`#T$7wg0sKY&%+LjYsXwpJIN!cRl9Xdl4VTUoGZo;;Z&v*i+xOqsu;W z6tUWwzlS|}wGY~OseRh|Q~P6_pXXEil(o0*Xitk*Yx02pnGbC7gl*zsTmH@5nLB>t zAho#e&EL@-KL6MIP{!8DKD5g(jSMYiLo0o6&!M}n`3Luk@s+-J7F+&A<$XW7vI;?hJHjoe*a;8!xw+zy5ZH{v5s`v z#^gh9c+-}{{Rf*bj*P$IT&#HOCn}$M&5=WYdCt6R97o^jN8}4O|IscBo@{)bdPC?- zKPDg9>aM59e{#(C7H$38k>PtT)VEOzG5PT2qaR*)?}>#43$Fgi_`pB)b&f(*K5+8u z7k=~P*AKpO`r8-YJ^k$(&u~nB)0clXzTN-6W9am;YsTOF;2KUpA|IM_)e6HK%rN5~ zpL6Ad(vQhUK2tWji)<_XsQigJw~b%&k1H1a@XpJn%XTCtw|dXhkIB!T@!64ot#ab{ zad-dlmL0dbLFX8W$QNyO)2@pi`|_I?B1dO#dCGOtkI9GMzxte^vDwQmykp~=$6x!I zi=-cu5B~f$UmMYY+~}EGmgJtxbFiWq~7=$ z$KNRZn0)*l=UzGdyLD#`J=s`e{9{*cF8!E%rB9sUMLKpSsJz;}4v6?!vch@xI~n-FM{*G5O%l7wBr=Y-m>PIBWoPpGyL1LpO7xwn0)AzHKz^N-@7Mu#!p#;HN^bHDx~Zzv_y^hSz)KK#j$WEh1mE#=gH8*=XYxhYx&o zkMXyaPnJzgz5umo;SCGFL_O6d_G9wlSAXc;B_jzE!#}50HblJw_!zZow&G8dwZnSXUtsWoR>(8gjCMF+W?}~>O zvc}9_X@dWleE8KbzjOSIWu6&+*)qqBulREPIcdx{TRwEsa~~O=GUaU6u<;wmeyZ_A zrfnZ4O_s z^OT*WYt9vs4}N#+bB3>f{NbBV{OiTzsPnRk$%hVk{Jq25d>ME07(e>bkI6@H-$0#3 zwv~QVj{6FuZ(FAykVq59QRB`^ds__c&?$& zEw+_@Og=t-$#TO_9DT<4d7}%5H+t0V4>9@3ia&aM_+59Ov1r~mwv%tRG5PSCwU3e? zw=MhpiOI(w`rQAGy!E0rhwEe4ESj~|t6SQzkICPE%~|6s{o$tJyFd9#>9QS<$QP~q z^qa(}?lgo$e$__h=e={U@!{LeH9U;I(~rn!9&ohzJpLBjNMXLY^rP~zpZ?3}+t%sF+s6=E*SToA7k?I<==nz@C}U%{d-&bkI5JC zxe?F0)EV+>KukXT)3cslw9U^di*`Kyj*$~qarfIX`N+0^eqwy}Z+?6HO{X6*enI(-wrV9AZl-ir# zbVU8?N^if~`|usBo)7NVZ(I3S+xHfH_osTogKGQFU$a*KC%zM8+v&ItD>&5ly?3K^ zs*g6VR@;B!MF-VCefGs_`+o52U)HXjHcM^)kMLch6Q0>g?ZDK{e%kmAzB6Lm{;%P? zTdU){TWT-FcZ*g&`Rsn%_Pqz++1agjn`>W(?;Nd#?;NQ;7oWXv^{##TY}RV23OI*mx(t zJEnHIO>b?y4c~3DZGX@0H`f;6J43b|*z5Aa#z zeudh;kK#LP>pr}-+JPQ?*X?wC*Uh&5m;P$U`WueR^Jv)8QNbH^QO`)@gFTK&!o zmRH+%5x$GG`g!kF+y5xOD>Hohh}s!Hx_4b3gGc}#Gaei`g2&)SAGpgL8~$RiK^{{M zxyBsJ-_SRRW5zRIevZfB3eDN(So_Xv2YK|pYIPnn?%#1k9)qhLxUD%>nfKrzkJWBn z#vE(R-D!}=D#xv3jt>gznEj?-M%r%V}sJ^JZ9W}`yGQ@Pv00*EfosoOYwhW zyc+#Cj~ue$$qO#IvDC1xIr%1b-pStBjwfJ9E_O$6jM|=5I``_q)aOQ;vn)ssa zdVF30`NM}n&d)p)E>l>>U;mLeJ$u1I^0qB3i+}B(<(c<}w{2lL{A>L%&s+JA>x20* zh}O$*@+SXyotyZg_41c|D*v4Q&B$+)zi1c7e!=v;9-}>5{O?)Kdqa*b&-s=2YQ6k7 zH&&i#(=Wj~?Yzr&kU+c&Cmt1Yg8^5qqi{AoS(X(&uzX|`cQ~Td&AIZN~eB=8M z`BeU~&ZY8?yovt|n?G#5{2`ypKVxs!CA+>l|B?9_|GKW!de;~E)cjbdQ}ZLAy1vM# z=I87q_XoM`6Y**LpMCos=K4YF#lLI*BlWvG{#o~`)vBq3XubMJ|E=|Tc-QL~he5Pn z{d4ml+M>08u`R5Gf2|)gKI511;dvOJmGQ65xBT1vWsv(*`~K4SH|I_8@~gJ%{_TAQ z*O%7;?k;qH(VAcU`X-;sfBH$~KlxPtlTYQpv!C(3>!xu0Y`y%oo~ zx@m8yvrR>vL87YyFU4UUIe1x9vas{yvv~lNZ_d_qpsljjt2`LO6fq z{=>{)d8O9NFY>AVgU?B+{34&qFY>AUa`utuvt0Ix^)dcC@wzV6de^@>zv8CWiy!$^ z{P-M^iXZt@{K%)`=j=W@Z{yXuCn_4e^=KPAAS}%U&Q}N?{W-5N&N*YDlWT~*?-U$*1*3uZ{|NZaKr(|9s@+0zo zoO~)jjJ=t6_WN+S#KQ}g3Gr{+gKmH*^Z^KZ+3!Dc z`Dfllo?mj=cN$+O{)KSis+Z(DDF<8D!_u~);w%+|wD?Yq`SsUNqv29^Z{A>M?U*6>912^rh7Jqq?a{jpZM6RE< ze<){MKgPfEQmwbYOo3S{Kgp-^lYA;aoqc3~$z^ZWKl}cw?LYhZv1|S#&yU^l&wVZX z^L6@ftxxvzqnm%^`O%EetUKew^YHn)y??g-XFner|0XXJy!@)|y8n26ZOt$Kd_X>R zf5Y#or}Ce?$^Xdn0r^z^8~a55I{%UD+xXXYt=6lL<}EsrfnkNd3uW zpNNn1AE__Kf1>`h&!1g?a``vodx6_W>Q5^^@%lqPRexN3BKg_&4@VjKY5bdgf&BCD zZ~1pYQ`aB)RDP0A*W{uRDD^$Lw=Dr`4y?3=W^I{v-1< z{xvVP-u%d?=EuK#k-C1!r>-CJsrfnk$n}%UJ`o@1KN26~UvW|E#fN+=KH1OTqkDde$GC!e!1)u>*xH>!1x&_ zw%+@D0YapLKhUKb~*mi`J_zv~R8dJuA*I>x$+6yU7zGr``1QpeX{ePd@BFRr}E#~ zM}9vkw|%5O=dw>+KW2QLc;?N2`uvggH}luJtM%5Od}{sqyPT=|MLxCu?7YF z&t;!jALC!&8&~Unf1G^k`{U;Pikn(5e&kc}+XQ?ne&kc}BcF<&vybFoF8f6MjQ>tN z^FNY*=KPAAS}%U&Q}NrhL;T36;zvFeKW86_UoQJZe2o82d?J44{EC}eFMi}x@tX=h zm4D<@@gtv#pRIWT3_<1^(CKL-?n}B`{P{piS=#ykH0@A zpL&1n#uvGM%=i>nwch@1{41_%z32NaJH(fKD!$}X@pbl*>nE3eBEHUlBtFK!;-c1z z5Bb*qlKuXRd@KL5-*1`ob9|BaUxwGdr`Fs5$fxQr`PBZml^b97^FR62`jJnqpRI^>h9s_0{-K)Zg~`v+Hjz|7Lvu<@S;5qZQwH{Ux8uzn68$Kk}*kBcIAYXCK)= za@i;H&-stU$N1O2q1M|!$fx!X@~Qp9oIkOD7+(8^T5ta#pV~jjr`B(4H@@usgM4cJ z$fwq?ZNC@ZFXKL85Uuz3jq=&Y>nH6~_0yd{^8O&#`4jnL{CDEDZfd>tGv`-csrB-U zd}{r+0iU`*BA;46@~QQ6_L1u^mwh5W|2F@T>)VW9aaZfbpM2{2CZD>#&HNJAx8ao+ zYQ6j*pSr%ur}AT4H@@uan|vxi$fxqd*+;IgT=t3lF#bF7ikn(5e&+mISGC^yl265N zyAJUqpNb#(RQ%fZ+52NY`}qDy`_%sE&L3I7T<1@$kMZA$*Se|o*3X<@>!#LQKk}*d z+a7#se!#LQ zKk}*li+pPRUhc-1y?>EUtsnW+`Z@c^`sK1utY6!I_V<^&=0EcOx;y^)ekS|-%kC6Z@CpwQs5Q_Am0O{fm5R z{a)e5m%V=EQ|m`QwSH~;?E8m&_VN1%+NbUx-1#H-54p~t$RFds6R&ks>#d(Tzw%10 zmtW*l>$fBL)cphb)cTQ6t)H`xy#LQ-pNNn1A9??8{F}IBzyBwndjH?zS}(uIr}C?}!~Dpn@{4?Ger@~g z&o^?}C*srcAKyR7r}hswzDRt{_!L*QUVM#zeXgO_`+TF)A-?2O@g<*%Z`(e5|IKF~ zk00$*@pI>o?7z9rpIBexzZ0*0SFN}In)7Sj)OzbjK6QOm!Kbc&@~QPBpISd>AK8C% z*(c)T{73d*)h?pL{C*&OY+_QZD;M z{GI>E=S#*v^ESmB|Jk1pT5ta&pW6Q#9p*yi)7s7x`3vP4AFj=WzrZ}T6CzZt*cuGWh``Bwb1zrRGjRUfmzzhvge`A6RG8(#PIYQ6h=@~QV@ z__Fs0@~QPBpIX1RefImYT=t3eYx$4A-yxrRzvIRiS${J=t-D%p{f&P; zH>vfWpLXgHU-GH=l266g*+;IAT=t3hw*6;+zS=eaklAvsr!$=-)hZ|*B1hnk^kgV`^yX$-|YQ`d@BFRr}E#~ zNAf?HeIoyz|H$7DF#fqON8WF>&!7E%%lVJ|{eYH#=I|NpRkWdCo)C!YW0Q~5vW)+amv$*1z4d@BE)eI);L*(dVf z`H$qk@vn7P>#cwL{Mqa8{73S?g--&sTHVC)UsTk9@vr{F}Va{(O~u>hskW zZ}TD}{%!wolyUvE?Bk!W&V*U&`XO)PANl->d@BCNJ`q3XKa&5(zviXZ+aJiM_J`39 z^CO?iFY>ARIs3@|kjp+1pSJ((=gY47k33&?$AA3!lKxxym;HR{<{x>!G~-J=|F!+w z`>WvlyT(7`Pq|v}@5i;y&+%tJzjnue{Q9Q<)c#LCmH)F`e6#mg@~QkMpUVHXefIZ{ z^4Z7VPtiX0e#)Ic^8Py4`4iWl@!yHpzO2@(kLLW^x72$37kRUO3syUDTjnua@BP@$ z;FqJ{o$UYLnyvTyi{w-5L*A@!WPi$KpP2u@&wuv**R}CS>W}fSbyMrDANkbvw+q+L z>?_&VANf>$C7)VfW1m&=gRYJR(Rm>>Dn{z5)AzqWn$`|(`%iTJeq z$M+xdsr|=|FA`rfKE+k77hmIF_r+?x>wm06e95QcOFk7}XCK)ga@i;1+xDM*{dUcN zFFR1nQ2lB1>WIx}NZ{=V1^Se2}$&UoD`xdp{ z{Y(DwVIUd%*Y0k7+3QC>wSMGN>*wqv>zB(uv3_m;+28NV<)3kl+@I#M?=-$n{0rgy zk^DFFSKh1j@}GQaf7qi#evwb*7x`3vIs3@_(_Hq6^)deS-c+sk{*-*`{i!*>;-=P% zANf@LW`j@NpOR0-k9;bA&OUN|=CV)3&-st!m+`N-sP*DQKDED+PwlVf{2X88`)h{R zzN*&SU&*JQ@5!guZ;l&Z_WnvfwSMGN>*wqv-(SmbpZ)!{T=t3ecjJrfuU+x~VmN;! z|IPfB_iDZTCvWmA^8R5i^NV>KdH+B@l|ST7{zb0;T=t3h|NH!BKfiQs{E_uD{+9?z^)Z)yVttK&)kU>leI%c%kLLV}n_4e^iQr{YIG6+dSm*}rqy zC*srgpZ$E5%Rl29S>IgtoyOORe<7Sda($ZlE3edg`9(gJU-LWU7x`3vkx%89vybFg zF8jp#wEbu2S1$jF_0479X?&gd7sB}?`DNy>yi)7s7x`3v?b9K@$fxp)d@8@%_Sy9{ zmwjS=TK?npm3*qcy75KgYsROzs`cV){Hw02_3G=s9pXzq6<_kH__pn{=W^I{v+RCGX70m z`253lKwDT9|5`ufQ{P`|@$t_G+Wz4vBmOOWe*Qw%=FXfBtRfkF38r zzt&x?xBlc)*Vlm^)}MT8{mG}+-`Pi=uXEWa*2npe#K-toT-18;A>WEm_V*jexAyPs z?=P71b9|A{-wdDFAIYaae=Wx}{Oi5CTJQZj z`PBP$bAGL>T5o;Hr{Z@I_*8u+pNb#(RQ#NMWdF)#pNOCF--%D;pEX-5usiYL_rkHi@cw}<)=WS2-@1OX|NosPzKnn5 z|KFL5PvozepYm3%m%rpw`Fn^PfA;=KK9#@ZQ~B%cBl(-lK9RrwHU1;{+nxBt^Oycp z`D@~v$lqLiB7bS0s{iCu`Fkk%RQ)2K%3t!S{B`z``}h3z+3%ln*(dVfjW1GPyW;=F zaQ-1Ir1H%BeAmoh`=VNJebzsRTZ%h^ZrE4O`Q{d3tT*4K=$6VJH+ zr_Uc*e=~oryIODk$*0!;@DA%wKDGYjQ|sTh&wl@s%RaF_E&uWNFXU72U)=a2@ipU9 zT-AEu2l}@pt|s@i+bzceP&p$*1ObOo#cAPvs~1)cl-% zBtLW6C*tG$&zN=Dm0W+eg&zEC{dC2@c`tVD`J1XOtcHJW|K|PJxAuAVNuHl=VR`&( z^Bv>UX?~q}<-J-j|IPfB7izuyAfL*QW5K8PXY#51AfKAQvybFQF8f4$od3x6WBe;F zYQ6Z7PwijiQ~Q@WKgSo@zYMQ+Q|qlC`PBYJKDB#EjUU-GH=y|zRA$fx2*J{3P_AIbk*_KEm7|B=5>XZ$m6k-yK@ zKEM6_;u&ZQE8<`4$N7)^eY%!^-v4F)eMa)F>nHo~Gdll~zfafl-;MEOUW$YF_xX%} z6W>nr{}0!ChsUOC__D!|k{@Ff%_WtSoN9sq*f4qK> zPt_0SKT=XIl_MiRxpI!4G`TVUr{(1dm|Npb;zjgg&|NT8T|H$93G2=7u^%x(Xhu;rh8UI>8 zZU6TA3I6_`@z3~2{{CL;{MPSM^N;I4lK-vw#q*zhD*sP%@y*VE@~QkMpUQt{AIbk* z_KEy={v+`*{#6&%di9ZfD?Zup|H!AFpUwGAeq_HNG`#AzTCaYSPdz`APp#j{ZhYD6 zM?STF?7-!%RaGwZU5Q(SJ(VU_OI^vkMCdfpW46N{3H998K3qQwch^K_Mg3f z8UKm>t9AbP{?#4-@%@YbQ~6ImmH(%>_-5xn`BeUsPvyU}k9@x|mwh7tjemWwQLXp= zM)IleH=6TnUsLPtZ{$<)I~9B@KeOXUJ{3RmsrWhj$o`SbJ`unF8vl{~sXOt(^&RSOQ*kPpwz~%>0zMYQ6j=pUU6U-1xKem%Pc}$mg@J`JLWje&kcvC;8O;+VqkDdey<0g+F!}1){lH@{hWQ|`y09J6Y+8WBj4XJ{!LtX|HeGv z^HuxzZ^@^=ztQ63_XlnNaFh}MmObxZ$lKQY{>&K=r{YgO6@T)n_&fW^`{CU7k^IkP zpIARLzD_*z{y%;G$o^^OuXR`JU4P_L*WVjDtUvkG`jbzszq60r-{i7Stk1vAe?8LV`R%iRzap1?;`(vpi|lV*@&95t zeemVQd{+i1^u|948+3$C|=0EcOq&xn(KWBeF zMgOh+Is5Y|H~+}{T{Awt*H!Di-);N1`)ly|l=08_M?RlwouA`p{B7HH|MBOy*8Jl8 zEBRFZztP1vdw(Tw@;~x^hkPpkjeR12o&U)7ZT#!HR_k5g<}Esrfnk z$ouPD_KEly|9Wq&)_Z?VKK1_EoL_NM>&1_JDt>PQpL)J0pNb#(RQ#NMWPi+MpNL=E zfA;>^HUE+Qu{-|b`y>6Q_D47W$o^=?r+rJUw|}+$XYY^3e`0@Zoj<-mcE^8wf29A^ z^DFsO{-5XKo4r4hZ|#rv|IapK;{TtGd@BE)eI);L*(dV9?LYhPpXTz<=d;NDXD<6r zFp} zy&T6C=&ynHQ#d}2<1;uui{nb9Vn5oea9oY!bI{{Y2?u{4W?3A*-)o`sc^qHBaUG5? z;ZjHAB@$1OO%jpI8wZq4Vv{Jng7&s&S{;~YPjgzfFnXUy)v@gp2};$ZB4jN>jG z|C7&G`KSLGJ%9e?yD?rb2g~>58_$j^X$fY{}yx8e*Xyi*aOPH!@)ZAdmN}mxFMdo}bGY;nAV>lS2#l0sA{ zj&;n@CrIizQvly!+8?T(ddfNtmlUxW~=4hrCd8d@-)mZth)6El-zZUt7X_ z2y-d#9COuJ68n-{nRO#-Aty&nb_vYok{z&d9Xz9Wpj*caj#D^@1=21*R6b*%r&<(=HcgB)-}BV*LfE{ z>&Tmb;W|h6gf5LM@7@?4$0YZrT-O(KYYA$Xi*@8Vz{R*rIsClX@cmx5-#__zx-_nX zFcwQvEBvu`@tVq&JNDd5oY!5tzD#c1j(p&nb@=?T{I^1(us%Ll-4DkB-f=MG_>8!K zcRi*}^os|!b)M&EWBA+z$5b3!;8@-2;`3C2mAyv;=SL`GGer} zuk}1@ur%g#8|JIMmEV#1ZoauMb`Ip3r+=+zZS$t_ddQf_eC#8OT4E|HXm)=qw)2|Ht9WLG4t4sB~N}W&G}9y&v7qru33A% z=DsKL;yYpOCv)z+x!Hv=PHJtuxh;+Qy4dkr%sAxE|2+HYWUh^+Jzmw($zDsT+MFi_ z*V${v%aicB39-w+unzb>@~*ctJit`V}Td**|(5)KiUlE*w&uY8`Fe+ zdmC>|s(DlGJ>IGbPJ7l1G{+~i_I^AT`ySrLV`=B98=t?PdH2~R&4+?xjq}$=H8Pbull0k* z_p+18>D=-5=DjrT>6ZAK4)VywAl#3Z_B=fQOZ(p4%`ugKss&4VT)Fo~Zya6OoAO*= zi&?vJ?;DEolJ04d*i34jUntjsV&Tsz7~A6PJzc$~a^%k9yz2{<(cB99v#JKgXYI&;O;*r}6pR zT*QN))xGrjbo}$l)H`t1XfMaQdadWEC544vPV=+A4xh6Y^2}!v&p{LJxANAIaKFhj zPd~=8pX5F23-K8v_dcF?zssxV#dE?Mdh*43PcYeheChM)iQjkd<3E`^U(Dx`=-N-_ z+<9}Z3uAQm1CzY=ySguD?fa97*J8#Y_p@W3{d6*W{L&t;;yKy9BUQ`t#6b1RyXL~@ z#}K>xn`%G9(%5|;@*7+ii{;Dn>&~4g_caF3K0JSjo&Ut#oKBuGOeV*?aW3s^b*bD3 zc;jCh{&U|ac;lGNT6J}A@yBpN`~LK$&!@duERDGMai2^*^6ZxMIe%&AsT-d^=Dhpt zOP^0i?=@Xq^Tsl)5tG^LC!w2rUs4_JX5CxN`<~o&brNw{;`Q;amr2!yyuSw%NKzsqko^GUvfSS^iw$$j0d>cu)oY~8x1?*As~^QF(H+iNhX z-?t0$$gNG{+E^O*rv7!jwC~;B98>wHTHx(9OJiJ%bFbsa%I{C=U0a^(YcXqA?tMcs zUeZ0yTcZi_&|Z{honI)|fnwp$DHvPsz25WD)!faIJByRoT^(bWKA$dMhJCe&&9Hn8 zIwf{)K&Q;kH_+)}=Vo-4VF$m2S6G%E-jyxK&JWR9o*lktT0xx^)me!h{Muq+Wp;S3 zRoJ1!RoVGDI;*k6bFIz}9j?L7edw&o&I9PI#m+JB-FOb~v40c6jy* zJJhMNL!BBs51~_Mhsq6h{(?@EogbnzogI#+j~!C|>~I(Z?9lU0>~K6Y*kQy5)fr-k z5gTTQRwL~2>@(TnNk`eC`7Cxg!kyW90-ashq0X-C@LXf;P;cuebsp-I~@Fe?9lV8*x{J>XNP_c zV24((W`}8WAUhn-YuMr04`PSP2eWe-I)|upC_D6g7&|mSoEV29L+>@d71u|xBdB{_v14*pbjIOfyX z;V@2The3EfI~?H|?C=zCV2Am0CObUWS?us!XS2hZp2H4L`bKu>@LYCyt~aqm>O6M1 zI&VfN?@#l1Y*TAMyJ#&+0WJr)$71N2AMwKCAvZt51nA>`nOG>TX&CoEE0}=S0l!vD z&*(2}&j&k$$xlHaa)+r;-;^z9?S+>IUfbhW0EQ3h8GZ7!7bZ^(x#5NB8~wtHmKREPLH~RQr{&bbeUs}(@=$F=S$qi4ufz_oRxYC9m23OjsB`=UC=3H#Q0Nv8Y9)@md6A!-(m}8|+ zo`*c`g~@a5JO_E{TipVDoAnfi?+JKwVEX46=!3kE$ul1~Ci2p^x`j=xzVOrlZwAab zLeKah?_=?h8=jcwx92Ks5#TKYyp{C_J&vtFp8kc!LvDCtuCI+%VQXNm1!7m&2AJy$ zIb&(Cbioz21<%-9uCN_2#|k-h4d(jTeqnp?jIHp?ff-BTeqhcQdbBN&r@b(Ft}o<< zm%i04>|pJMUjfWm3-1WbctFqiAW#3o1D(xWXVX^9OQ=xgM4)41xFY zFfj8+{0K1D7kczn7`FPtY`tm3C!^ej{@czKef9M%~^0XHwKLr?a!%LsODI2r)!n*-;zY^XZ znDK$0(I?OI36m#=-0;HmjecPd%L~s2rhnl%!1ND2qfegxgvk>_Zg^q(M!zuE^1}0g z*KF$+_5`MX=ox+TjE^vRV#p0oyhXcT*bA6@AI}A@us87LXd!oaYP(;U58lVGv>0;Q z7KmxjK4tp^cwdVlH@vX5E$rv%!&l)|7DGG;@ zSbO0^fw!`{Toh__e^i-h___X1t(h^vUyl!sLk|H@q-?qhENP<%LfG)_BOD2u%ObGy3E?eqr*& zkQ<(O>vq3zQh-ke-UdA6^j#q4dT~COd*M`Iu9xs>z+5lL4NuJVqAvP{(}B5O!mkJB zdO>b@Vvdix=oiia=6VUg0hsG0d_FMa1wErrp7z4zrvO84c(I-!R*cnWo81f02zQGmFvHpbL7~nTsd*~T`^7JQ6p5uYs@Y1Jm%Fea=!fy)j zdB7Y$^o%}vAB%_F@Webn{o!1Nw*>gD0e+kH2R)-tp8kc!LvDCt=FjVKU@Z!72VT#^ z?*L~0KyG+q<_~qzFT4|YLl3_TnE3;_;fcBa)J4DWZs3hQd;u`?NBo7roGZ0=#%%cc*qS;%=pkB&Q-V+nDG(540!#P-0;MV4|UNmd<>ZJ5&k$Z z;{&EldKJ36YU>uh2u%ObGan3*=lF%m6GLuz;%(dg!u6Jioc+R=0{rCw ze+8K5gPzeR?_=?h8=jc!&G;a8g&Tpn-ojr6=6XYJcw(+MbnJ_*=kSZ{eGP*J$3%>>4 z(8G@aGe03WJTc=%UGxjT17>~-{~nn6Df~C!wOV>cpFHhhXE1r@C*%%OpS~&kgS8ia z6nImQ|06Kh7kWmYJpBukCx+bc#M`y|g~x!m_weJuF9(La0NnyH*Nc71{sg>(hyP-E z$PG{Y3d_T`@Mqv1J^X~#hn&6(!t@Q>!e7BpMJp^Ga>El(L*Lp`X7(e-2eB+X3C#5t zehQfD4Y}cox!%-8zwk6L*IW4Sz+7+04NuJYQ5SvwVe$$1e}TE);{OTE^?;tyCr^7} z^2CrEo|ySc-{=>f0p|FH{|}hshurYQ8?^g{XMs6>+JfUB05{`@++m*2a{S_-$ybYu zpv?G%R{>`JL65!+PkUkV$XCh@FMX?91YzujOTcTleesJfj1Tqbqevd-bC^6aElNf7CCo4vhS9 z7;DY^p}Yv)A~6p2i)#X7zjGLKWBxddbD8-EldzE-zbvAl5I@{k)FVe+so);xXUrqzes@WMPFY>N%6FWhJKAve6R^;Mj1 z^@YVl&T|!sIUo8)-<+TG7w!j7Y;}voJU4aGFAe~&i65UmO8uKGBwVCh#U6-UoOsVCXrX_QK?+0K?Ak(zm{f zqt;${7Vz3_U&Wn)xsO55_#n^o36m#=-0;FYANu^Cs2E;&S77=V9s{O-=ox+T^e0T7 z7;?i4(>MCX-7GJ>J1}d5@E*YQ4?Uw#p79YTPYk)?iMMF?i?e~b&+uH}igSQBM+>>b zQ``OGT<|{L%VNlBTO_7E`;^TK@SYY!Zg^p9Tin~zhp*y%iy^0PgPEUPPpoxuU+~ON z;a37PKOr|fG4qqU=oj|`W_}943Yhr`x#5Y|X!ncz2lxPB<|lnYxA=#c2W!0UPV z6kz5P6@~*SbO2O0l-{==V49xKhe*~E0hurYQ96xo@FMbr5 z;}^aJnB#}s@WebHbfmp3x^y|H9;nAvZj6z1=T< z0l4AeF9J7#A*b&mG2_cVW!C{u_we<=eJ#1+iTf=N+v1mj2R!^0;GJ4>`YsaFKYhct z_$A;O9;W?ZOKy1LAzys?KD0W+V){}6bsmY&fkPkY!IOrH4+ zxx>__Z^~`~zqW_J9pLW)Qy+RppS+LB(;jleOP{_e`;OHYzBRz#1?KevJ)=+F$KoM3 zJTcEte>hk1wg7)Wz(275pl9^S)4#BI$PG`-^<{hzyW;J@TwmckfVsYq8=jc!OI`Gf zKLY0Z3f~FL^@ZH<#9SZhqF?+mFxOZ3E?};&@XvrbU+5Wq^0XHw&-I1e@Y1Jm%KpdN z3;zUoE2~TXr@)K{^o%}v`WGfo47uTnU)Js~%Y0zG=pW}U-VMxn3EuV#bTQ z=ofzu%yc1=#!_tFnPuca>GlXzA5{q zwHLlWzz+a3UeGi8Em^i$49~T*Zfg86V+?f!A-z4NuJYP#68;uYfo7@UMXx zAIJ?)%=u9l{o-$c86V-_0y93se*k8Dpl9^S(_WZ7;{&V#p0IOyB4i|IhNm&j$Ek z7DLbIix(CTx#5YKpIlF_bpgkE9xehSKPfjnG4qqU?1M1*DO?6dei}J>V$8?rvfqOP z`RVX7=p#QJMr;{hqep%6w5J_0Ipn923sWZDWv!0za=@E<{PO4{{w3-_Z1 zu~VONAIo-i$f++Zead+*V)C-p`Gt|o#=mrlkv|jG2KnPKt}T;4Tr;i{G4jXtxfUCG zn7)`llv9_OV^W{KnLonJP3Dg<=g#v`kGAA#FHD|ep`5(*rK_QK{;)SIXBIH3-tZi zYzfSBsl#`dfbZd~&Ubv1v z^@SVgGhWoA5Athxm^?A%7X#~_nFeeHpR`jq=vw!1^F^V8QH9Mq@W$FiLbIrW95PdU#;OkTD+ zzwlge^e`um{I{1M(4 zeddqwYtZNVQ;)XfX)jEk`9nE*=}Y&OkkMXvKlG_D{3`UBKh&cS^7JoEo|tm-#Eg&n z`=ihJ2p@nx<3l-hi5VaDUmf5Bff;|wsY^`%93SU$5DxkmJ{W!ar<^=7{i}Zn`i#Hu zq3ARIlv9_O^HKjW^f&hK;pj8|;$MsYS~#djTk^CQCeL_MPG0)bJpwZBwZccDzqW^u zMxXwuM<3*QK4J2-r<^=-)%Gbn3J3MY9TVVV1AH7X*PD9uMc&8aDJM_N_0ZgoN1y8< z{5tfx9+Y$L#9R;cPe7mRA$%hGTo1~rOU(JJe-ip!58;#1=XwaAgFfd=J=&6|y)b#M z2j%3YFWplhqrLE{=yN@UPeY&apdNjYr@k)2tAD!Xg@lf{+9Q02;V)AQv zm^?A%(r5hDe*^lAzwnvpGyas*mYDHZ|EvI?4b1C}a_SP(zhd=9^yy#tT=eOma>kOF z{?&gI`dnY(^U&W22j$cy=6uwDGx}U#;kTf_iHF~gKI2P0+LEWeF!?D~PG0)beJf?dm-a~BK$t|ng7D?N1y(vM<3*Q zK4J2-r<^=-)%Gd75C`?eeIURe4Dg458Gq{07kM9xr<^=7*F$r=7=5mX@Q2aodQi@} z6LUS({|NeA58;oZ&-I|3y2PBn`j?>3^$@-keXfV_r_kqosYhG#v==5n#mdP`U%Hn; zMtk9pp-+9`kE6d14(ic2dHNS7PfR&^V#Y`PPoTe^hd+rv<3l-hi8t{2mk0O?;LUMR zPF-U9=lD36Pvf9};m@E?|CEy_rhoN6i$3Eod?osfKjqXV=6uw@3jK{ed^P%vzxZ#U z&-hV~w&ZCqOrG(ioV@g<`#H#HFMJL9ydH$FMW6Af9(|CfzA$-W%E=3>|9Q&`e*u{O zg|9=O{;5Y>^7JQ6o|tm-!s>s~^1{~xuj%Q234Qvf9&O1pKEmXQDJM_7h1dTw`dfPV zE9h^IgL3K;PxbmY1o&$fQ%+rC+OtpDjRF3u#gvm5mhDZRK7D=NV#3{bPjL&-XMR#nU1G*d{e|c=KZVE9XMPIbhW=VOs7G7!v==5n z#mdP`U%HDRqrLDg=u=<#+vszBsYl=B>0g*UG3Dfm86Wk(gFfRUd@K5l59QP)W_;BD zZh*fB%=M(4y2SLaxqKge`WOBI`t(mZ=Socf>i-aZ#$Wh$^f$smIdzFSANB7*pYa#| z5&D~W_-^zWKkCtzJneiif7uV3;1gFgLHPdv{jOrG|%BTrnl zeah~_L49#Q3Ghz?{4-##H}&X?ypP3GPM(>?!9h87i5X9g?bqn9=i%R=&v;VKu@i6L^?!># z<0OYD;$1nUx^f`XY zsY}f9tN$4K9KZ0N(C7Fmr!Fzir~c#UGarQij6TON{3QBZPwLT@Jne7NymaoT&}Y1apGKeYqMTzRX1vt@ zJNk^5@ITOJyeOwGG0(65|Dw-$3I7v)#>?Vz34P9&dbA}^dy5S}#md1ued#^}j`0!x zKlIo3@W0TfKkCspd7e+0JnbnbPt15p_gVC*FYdXCxLgQu5qKRO)Ppa>``GeEPM(%?Ip! zR~b*?Er1zM9+cNhGou6^X z*o;17>|<+NMl6iJu=E)tt^+asSzB{{&Q}EmIEvSord3#`vU-;#~96#iSC+7I6i+*_rV2)q-6~K%gBdAve7A>6@}%@XT%DN`ULY^bb9wPu|DmX%D&KrBC0KRjs~o z&FTyH0W<#4Gy3G|Uzj{G z^BUI}bKc{g_k*9m+l?N1+5J2_dyO8_@pc_=pZ)NK-@N^cD~}#|+56+Xc^9J{Vee1z zPVky@dgNvAk6zZJonh}!@h-6U2R-t#?_V$L(XQ~i4L=n2{#5<|*yqI;e)HBBoA>^p zN51^_R(n|17e5^KJ{0c;I}d!}H}8JM=4JHA7u%clX!q<_yhq3TWIev{o3~%FdC!9$ z`SROaZO`-<@73|%u;<4Ye)BC?9zF81``eGbM*DWWU&s4rKYZagZ~x-Tqeou${y1OW z#ppoT`%`=n?EOKHyzKqa%X)M$?ENV|1or-*M_%^*>t#JU6!!iU9|n7Wicf@nUVPy< zZ+)?O?+<$9%WrSB!?V8l2-x>kd?dWuoG<+5-H*Cp^D_D(wl}%aQJF738m{x0KL)lx zzVMs({Ke*F^vKK3KWj(F!p^_=ICxDM-QFYF`R96ce8(ri{4-Hq??|@4&(EwzkAUr8 z{7BgT=#iK0ub1`cB-r;`d@{VwoE~}E=h4f0bPDYIEq)a2`z<~LcE0$+Z{GT1^Q*w< zkuSf!)gDdW_gVayj!%Q_k1zb@TQ+Y!dgRM*Z?(s!zxdSj7e5j9{o)J1dHWZem(e3% zY(0C89+&yz$9Mb$*#7v!Z@%TqqeotLKhMryqbGIz2x<&L2JUvd^QJ z_2?ql`4>MIcK*ee!OkCF_|02iY~J~!N51^_R(oF77heo-mR|Fhz|J3E_|4nD*u0D$ zd3p1N_2^RAdD)-6N0-CStN8h_^Foij?7Z}{9$f)Dui`6V=Y<}5+5Pph9=!l|Ud1nj zomcS~a{VKi+c3$|xZ{Gci&CBSKFSa-9(MvL4 z{8HHd#V>>Hk1zb@?N@AGMvr{4y;+Z5p84Wez$-0yN3VqKk1zb@olmiO89nmyh70S_ ztKf|qz8d!XLAUovUVmXddUeOINk-Q@lC5{Gc1_3ECZk8bIBQ4OHGX=Hu1`j{cd+-< z_s6>)-ALa1S^Qdf%{e{tviDOj>(T3A?`QGr;dSQp$ji=4FYD18VDD$~8{zdDejn`n z!xw(@))$-iexgUd{PtFRQ`Q&18Qvnj=HCMQe({Chy#0&K%jl7pw_I3{-U@Hk@Y~_7 zVRU{pj`{6=&_} z-Q?|GTzPbRhu?hG&hJ=k-R#7-#>R_^nTd;T>Jre%{e{tviDgp>(K|{wHy8r z?0rU$yu9MVdh}s<-G)B`d!Oxvcl0&b`;0Hxy!FNASAo$N`K?v^Xx0~h4BjHW=06U5 zpYes?y#0&K%jl7pw_I3{J^_25v)AZT@YXQ8y@NN%wc01id!Nn8pN6-c)92ayoVBCR zlHac3&%xejbbF6v`)9AwXJGGhvGv|(boYvWJMq_Ad$|A34SxaNWloR0Z2#PA^m+K6 z4Sy-~=$>U%Y;S5uUnIXPe~Z78e)Px}XYJ_At#LqChUDK{ubi`?j!u=7Em zXXlf-(XYt2{A<|n3*Fk0Y=6(kvy6TV+rRjCu>H{^FWXNq>(TFF=U@B>*!iPJUiNwP zvL5{r_P!SX33mR)|A3u8zVMs3zSz9;M~{5@?XC9btS|mc`iuVxJAZuPH*f!9^D=tm z^zJA1+O`$M_zWGdRdSD4Li@`|G>@@J@T^W z)607F-;VzWJI~CI2e8kJFZ|}MPma8dZa&!a=XyNE@A-?D!JJ=R?^x#iwH}XP&OgsA zJiof$vCMw89#7zP8=k_Pe?HIN-Uofwj?G(NY@Yj|9{KX8cRb7b;yb{rrq`M~!puis z^f2#!#pY%7$QNhr_)eKGzB6q9;=921$LC&S^Y$w?FQZ4kSTF1G3YjmyE43`^eV2R^!oIpT?q@M~Gw!E%R-f!y zay{m5#{DejZpQsA-URkpyu;pe^VS!e_kMb(&6hvD<9m?bEOUC8b=;rIGt20QFM61F zzhd*&`;M%aH_x@&J=34LG4DR=i+T5pcw^XE@%UGW28=YcQ$=H0K@yo?_CVtcb5KQQyf4}$Gq?DN_mU--@2uh_hd9(lRu z!CGy8UNU!M5pSOLo~82kD>m=`-YN64@7L#NR^u&Uzn$W(V81o=$jg2kdRdRRhJD|~ z+rZup^vKIAF09AfcKi_7_iHb_l_T<-Scn8??quYBd zd;VOHcZAn&cqiENqw5{Z?w9NF&hWYo?*e=N%I^j{Z+zB{&0Ak=ew8^r^5swOc-O2i z=39yHzxZLW^TB8Dv3dI!o0t8@%*(vvT8|$NJ1_gO_jq^Mc@^&gJ1_Le%g#$L>+znj z^D5p8c3$X_m)&14>+#;O^D5p4c3#ED!Oja`_|02iY~FdHN51^_R@*o0i}!>5-ir5! zofp3Fn|Hrr^D=tmi|x&Nd_d-l4}|Ssd=PAZeBn25zhd(;dgP1k&3b%r=8F%3S6c9n z4~6ZIFZ|}6PqBF!J@WE~3+wS=u)pQ*MQ(gJ?C(1IJp0d)%#DvA-}2GP=+=&9>s_lI z+3``y=#ekZ+VL@spI+l*lhN%RygGkJ@VzZx7x{BUwlgXiysC1e({Ch zy#0&K%jl7posYd)j~@*?pW?^B&Idj6vh&f)di>aqPlY#_(<3k2-}CV-+!kQ*u4FU&96GAN51^& z9X~bw#ZT+_>9FU=XYaB3mMf1QdD;E#$6n)Sc6@fn=VU*8;Wuyp;>x2(UiSVtZ{EfD zJlOkF{4Ch}gC2R=`=gij_*Avis|0J-!@vUd7LcomcToVCRJ|{N}AMHt)R9BVT@d zt6h=x#aE`k_yw@@!WVw?_AfRsqeotLKK5okej)69ieCgfAN0t}&POln@rygY3f^E& zkGyPu&&RWjUkcm5_+_yD(IYS0UoY$N%VFnV{0i9lqeou$dGxX#zY=!-#jk>$fAJe& z=Z`P^=B+O_@BGmtUw(V5T}^(~%*n6r_%*Qo@rB=f%jT^|k9_&F*Z7+B7hjwH;u~S# zFTUtu-u}hrW%S4wThCtO>oQ+_eaAPz_Qw}~^DS2%J@T^q`8@13er?CE>-hEA4`2Ar z+rPN-=#iJbKhB?bF@7WL{V9GEyyl!9d3o*hvL3&=s1!))&7W_C6H919l$xqKA3+D>g5qN50toSdZVC z`Qn>k`xpDX_Qw}~^Y$w?FQZ3Zu6eLlo1d3_OUJiny=SSs{ff=IKXs9peZM|Gvl_n( zUZdf6!@ghi$jfUjtjF(xec#rS8^0G`hrj6a?EA^w_(>wk^))#*e-Z;J1de}cU8D*hzAUc;Y*ofkfPjm=wM zY~FdHN51^&9e*n8i$4u-mR@T<13NE#(Zjs`i_OdEk(W1LSdTvoJ1_gQ_xSU$^D6!V z?7Yw;FFP;2tjAx3omcUfVCRJ%dD;E-vL1gKc3#C_ft^?JH(=+5FZ|}MFE;PI&?8@d zd#iml>x;kE@z-JJg)jW(Tdq8Myc0R?wgq;t*`MmYT=A939kuQJt8uOh*eenG-Fx=f@X* z^DS2%J@T^q+mF4*KkxYeI{roW!xw(@_AjnHdgNvAkMrhTjDH1te~Nz%uQ{hjUiSXz zWj+24ymrICg}p!Mk(Yh{dRdQu2d~@k?_uvx<^Kcwy!gUz-uhzm-XHYHm*3uMf5`gc zKf=DR;y=O817G;fyI-++89nmF_GUf)bM`C#OUM7ndVJwGZ@*&mo(Dbh<+r!mU(;Xw zw~qe~dwzW3H{WvQ(IYRrzx~*2{Lha6)$zZxAHMLLw|{Zv(IYQ=f1EGxV*Fp&`&0Zs zc+ELI^0N0wFYCzw_Wl$PVeXH5`oeFX`gu0b{h6Rg zzWnx98)tnn2l1b0)oU_^nTNjco40?lc^N(O^5zTc$qZ&*wfE$XF!P${JHgCLeZtu$ zGV`kSu9a z))(I$c3#Ewih1FS9_H;|Y+go>yzIQ(PkklWdD)M>C!8hbRm`^(^YV=5W#^Ua3GXWA zRZOp#m+uhoM0THCPk0wGuVUUs%&Yhwu)Ta|p3A)T#pXSW@6det(>vj8QD4lv41Y22 zGUnwo*u%X2i_Od40rT>v3+oB*dJ*%k{m*Bp>z&B%WpK91`fzjh$vt8FqDNkKZ@sK1 z8^G>eydi8~^vKJePcQ4qMzDJqZw!0?in%K>4}9S_Z+)?O=ZPNq^4nW&6Y{>BVrH?3 z?*se3@P*%e%jT_jhW0DJYc*yU{^EP5zxV;LcgY#+H*f!9^D=tmi*v8ZeKTKtzmD$@ z+aI52o0xC8^5~J5-OsbI*W`g6Kd9peXFq)5H*f#q%A-eK_U<@u-o<2d*!xqw1?>Gn zkG$;t(aUUY>2T3%o|dyTZN?^vKI=EvzRGg?%5^lAAmXUWdQv^Xxn`H+eXD-$(Iou<>Gi;sapkQ+z1weDH$kVsG2~Zm_}Gq5 zg!SVKzxkHUTaO<3^4nYOxbznv-|-2s?*m`>&9_{6^vKKZZ$I{$Jfh=Ac6?Iy!xw(@ z_AjnHdgSHRSvwzQH#r6N{T4q8UUN>5yzKkc%X;!?ci5qZ!o7vUbesI<5?zW!1gbG3T%J$$jkQE%X)Gq?EH(* zf}KBl!}Mi@BGo{S--v2o}Tr^&ww{>^3Q~w zKfdsrw|}vD89nmyCj7tWo(r>9i+S0fvrNu`omcU>@S1aa z9(j3n*7jqs33H426h9AkKF-s;?0j-Pxfpgn#h1X&2VL((_W5!>xfFIj#h1a(r}$N{ z^TB8B#Ju&z=A93EmvJ{Cs%T^jdQT>^$&A5A*Id~w!JuFQP#3t;;f zzYw-RKKGiKw_mY&89nmFdRb3il=m3u9zF81_tW>uyPjMP`~4Qb z8eVfwkG$;t)XREu4eb3az83a=qDNl#dGxZLyaryk;p<@UXXW1sJ70X^H*bBhdG9BB z;@rB>K{ff=Y z=#ekBH|xn8GGF{g*#5e6hV*Pu`sQ;M_%@R`u^TY+yr|+i*JU#pXia7y`OqnPi}#| zpT)Pr-cR(%%RY}@)|1;{?`QG5VDD$~$6@D-FZ|}MFE;P}M2~#=?XC81@~dV}eox2m zhwYCq{N`IWZ#{bC%b&d_?@fR4`_f!`>hC$jiQey{spnfxSP)pM}?J`0KFGi!c1`R%Rt zIr6^$;?H;dW!V1s!f(E1^VXwBzWnx9`$GDQzu57YVCRi5{N`J(JbL71_qQK=O}^6c zS3CY%_QMx`^Y$;UJbL71->>svR+DeQzTe_+!oFYh$jiQ8y{sqSf_=Zm--dm^=#iIw zKYCeDz61Myi@yu|ev5wu`@HzVZ{GT1^S)p7$d}*VYTwKH;_t(orPurqVCR7^{O0Xn zY+go>yuA6sdh$cqdD)-6CqIVQX!v&6d7(#MUTa}J`3da2tR*-3DZCDU(dXIyGdKAe zdFNI9bJ%$m{|0tm`0O=hMr2 z^2d(<1Ut{l{~7jq@rB>K^~L68^vKJe-`=bze}O%J@n2!jj~;p1^Xp|j`5Wx{i~kOL ze)P!8?x&aaVnP7==1D+>`iX+U-D}-{6Eyv+G) zJ-rjm`RDo0u;)jg;+@K#KiAW{z@EQ&1=#bW>z&H(m+R?WVeZd7-wpQsm8ZA&N1wG* z^VS!e=RT-MzWnK(-aYG!sr8?i)oZ#E?0oP=5A*I*+n6WnbDc%ZpKIoB`eLlUcr(46$r+6FK`4sO3J0E=EH*bBh z`Bh-_$d}*VYTIUg@k8M1ZGJm=wK-q-&D)>4VDmEiJo_wJJKdgq^~O8h5%z58_73*` zxmMeOd{aB!33e~^$jk1PwbNZ-*TuWS_CdGzRJOkwwbPy9rdQPKLyx@u^r_jy{md0V z3{E}rvi)N4_{~r@NE4e{to}?LC#v+ne?L ze#Luc|MX7f)$#d`xEs^G;WZlG2lhUrM_yiQVLja!Uc2G_;B{d1Dc-5<{mAunf7tt6 zd;q*&!$-m1XMEO9&0Ak=-usLm`SPcCdLViK*;IT`$A`i8#}_@!w`|^e^vIXr{nQRl zfAJw59}2HF=L^62mMf1QdD;E#$6nLJJ3gZ0BeNgA@SC@Papln?FZ+I-2eXG9-!zr`oOzTe`LV4oMCy{6`^FE;P{ zMUQ;>(>pye>x&=J@grg9fiHTPZ@Kd5k(Zs1{n%@IGVFYcPl25edgNv2qnGvcQLytV zel+ZS&?7JVe0o_=9|Jp|;>W`4HT+E2`QQt`dFzYKJ0JAOm*3uMr)GWeY48^5HUBu+ zdEg7bdHWZem(e3HZ@I9ZJ|5nx;U~gd!|3*&${XZb?FsNU4L=Fqc218P`5~F7c6vIz zUBhR<+t2Ctp33&OH?`9z!#gx={f={b z;-_XF-Lp)KvvztG`CZYApPqhndrynAcKS5(_AjnHy1m11K5OUqD}F}yPw!OrKKuT; z8`HDlH5xt#_CBLWUS4ZqJv|rpK3hv}dLFzEf6?c8#fA0sS>(OX>7AYruh;O4;FV!~ z_KLjq#pYL;(<5L0^iH3h^~KMDS6%2ey#QVf#%J%TdG{+eFQZ4kSTF18g_$qD2)2Lm zb7A}A3%_~$6`PmQBVTN9*3;)@zW8Ezr3LTw64?Iu!f)RB6q}dPBQI~bu%2EDZ`AN* z@CGoty{Gc}3+w6S9bcJ@u6HV1?^^Bo9bb`*9{J*|oxY&)(`)*|WORE6dp~`jyzA+U z$$LMGuY$dw=#iJbpL$tOUjlnSi(d+RKhYyEueh+Dz6|z$T1#&Fa@hM>{2JKzhi^V_ zeX;pfVCo`Y{_Hh)y!U-U3<|6=npdgNv2<9@8CSHo*G{A$?wphsSI zK6+VCuj%+&c!N1T^0NIsAI~zq4z_>s^|1ZXBQM)uFYDzGzY%u+_`+}A{ff=Y=#ekB zH|yz}GGF{=*#5e6hV*Pv4sP;^s=6Q0(M@-pM;$kdgNvI*UNhPDcE@xe;Qt| z;cvsv3t#xnTVHJ6d7($X{PtG+Ox72F7WR89{v7PQ@P*&J`xTp)(Ia1MZ`RY#XTJCg zu>Fg_2-_cD_|4m|*u0D$`C@yso_;Cw#b1W)U;Gu={`kUg-hRdAW%S4w+ne>2_a6D; zufZ!Vc&A^7?eG22Z{GPAo0rieU#yq)^c$Hk{$|JDO2!xd$`@B2J@T^Ov+s|0J^c>s z{Ve`2?EOTKyu8-Jdip)s`)Mt?>GxspC;B}5JeiyRfPBk8guS1Y{}Jqb@Y!o>UVpK9 z89nl{=hw@6`eWGh7vBzhe)P!8o?kEP=}%zKU;IVnPteWA~@etWC^ANkE1{srv)to*OCAHMLLx4ziC_2`k8H(&5hf0=&w zA~*eY$G_?Lx3KR6U--?pTzT}!%f1iK%d<{@5Bolf{{Z_w&?7JVKJ>Dl{t@*+r_{wI7d^5|2%Q`vdCA7`EZ8(yQ~|G>@@J@T^i)XRGMUwG|? z{|7ry^vKJePcQ4)0A9D@A?!RWe`k2*IbZnAQ*T|cdFDAopJ)8#oh>83YQv+BX%zkO z*<)tj{ff;~uO9jGr*}3^fAOT_Da`xQ7d_0kTzT}!%kFPK_L|+H<2!bIr|gF>{O0Xn zTzT}!%f4S{&+KM*fqlQlE5N*8^~lS--&)V^3iE#F`ED@pSAB+eCi{MJJ-a*1`<-WU zzTbJKHl7!swKMb97n}F}s+li;dS@$TeKE6(_f<@7%mZKaFzq6m$=2q2b`RM1Tf9E(8eQ*9_IYwWyC-a~V$Krpw|EoS+2XTyX5RW@ z^UlR*Fkk-k&Nj^Y;*C1q7}G%w*@2>)E|w z=Tm$i*!iIAoyk65u4ng!olo)oU}sajBkX+eSvxateX)7xgC6v_AfRsqeouea$!At5WH2xo55Sd==Pq;zB|`y4~DmCcyoB$IX&|7Lo!e8 zjJ^E#cdOToy_Ri1r`tQ&{`RJJ#7Dd2F^ux|E!(gub5|# z{^^~`-e=#xbKu$IJ{R-sai5)ud3mjc^^94>ea_sBXNmjljGUY7{mAv~p|JP4_+jvR z4Ic=v4CAwQX5RW@^WJCl$d^C8vxk#kHFJ7)>v&Jt{`jJY`IgOFj~@B*XRq1r=`Y?R z{l)vjt5J(DdYHF=v3VIi^2OG(*KDuM7w_HiKCu1qh2MP3l}C@f?0!BEd(HOic>j(M z$bR_3Z{GgJl}C@f?EP{6yo=dE@EQ#t4ExU&^vKKJAHA$+hrs?G79R?)Gp9#hUNOC_ zXNSSwA8X~qVee1zW8jrxeDithi_Lp~sEd60v)Am1tS>$iUUi|@>?qiI;ENvS-LKfZ zj2`)7_hUUfI`hTH!1gab7PddW@SC?^v3VIi^2PRMJv%P*#mB=dEqG@q!1l)%e)GuAWb~PN+4}U(PU`sNWORGYinDh1 zsK!sP*`t%u?H#;2e|?|4>)B(;dq0a$g}tBXk(bw6SkF#_y`R>Sn>`NpexlE_^UB=p z@#I^60_^>){1aj4kI!B+^ZJX;%jl7pJ-=SovnRoxzxc_p=SPpc?D_Swo}CVR{^B!W z&yOB?+5Pmgo;?L#x8XBk&tLhc!`?r9;Wux6v3b8Q^vIXr-fCxMeeqM_jnixXY4B=u zzVMs3KXt+8W%PO8WH1`rJFnswz|ISwy=LaEFE+o*oF4h|r+4$vUUi|@?AfsM!e{T9 zdG{+eFQZ4kSTF0@b249i0c`)`3t{`?3%_~$6`PmQBVTN9*0YN;U;JEnr3LTod9eNQ zh2OmMDK;;oM_%4=VLiJT-l*YA;0<7Od(Y(c7uK^&JAQsLy55;=y=%40I=(y^J@Um_ zJG-Ls(`$BRGP=Ek{hocFyzAKu$$LMGUj%zU(IYQ=KlQSny%_d>7GDK>KhYyEJ1@Pg zXD@-hpT#eQ*K7C&*!PDo{N}AMHoppt9{KXyTkU08U;J{|`&0Z1c(plS_|3Z?b;0Ik z^hIoMas` z!Hu?6`ZiAu|6rT+Zkd{W)bwthn!RN8ZP9!V&#)umd3HeadF6)^yAZpk&$WJaJmHg% zCXOVIO5eV$ncLt1;=uH|J~;Km(x>N;)DLa6!_n{zj!4bk#}e*yO!kvcCLT$gls?yw zO8u1d$!8ExCQeW7G}ccfoHCu1}xq*QS1B`s7=Pn~0m!=lZtPZ%v>4 zS>n^gXVT~T^QnI>eezF;9}~By@9V6;Lwt+)cKTd@H}xN+PtW&K|9+$W5Dm}ZN2%HS zXN3FwH2cYaA^t@C8ErTI{vYux;zpsvE2e(;Ip0uTn^==rE47tbuTHE+tdX_m z*Cq6>lRlZ>hT*0JzaO4&cn@MDVgq8staZI{>YJoy&3y^$`CW-xnQwTwJ+T8CGacTa zco4BU@!-_tE$4W+S)*;8zAaM2KioFGTcu_nHND%UW-nR&A!rZhuV>hq@H{)A`E2rG z#396?>Dz<#zQo?dKIwD4U+M>@PtX3TAJAwAq2U=GoSMClAl&Eh>?a>f98DaPKG(;m zeq8$GClgO3o`mN7P9#nu9z~p-n*5kK9-h)@r>5`Gso@_!F1?RU%|2>+PfN{SvijrE zoS$bngYZ13qd7nMY~mTjGt=k#+|NDToL^78iFhmV=G5f3 z&GGOp*^k`t9jU)Pee%1An~7UeyNUHX3H#lent9gYeRpcNrKXo2F)PpY0m3uBAI;gz zpCdj)d^UZqzmWRp(fcPC%v~&4J%87tW+&MC9SLjY z1Bv~K0|@803t?_|!d`0fo(-3_H<~_v3&W>>UqbI*shL;PyH9H7W%d0C>pa83gy%Vk za8LPo;#lH1LhVrEDB=j>NWxmzM-%GD5Ng((NLYUYVXgc);#A@^LZ9m=5bBR7^vUNC z&m_(!)c6fs_B7%w;;DqSuAfe*KZ8)S<~+jsa|vtZ=MonZ7ZLhgUreY!kI*MyMZAc3 zF`>`(O9}Ot5c=fniPsR<5o#|dUQN7;xSFum^)-b0wS<~AHxkz0Kv*lkmv}e9Z%Fvw z!ryliZzuSj3ZLtn2=!YD`?Y? zM~IIS_H+GlLj4njnl+y#tp5yQt^7^m>%=z*eXhSvsDF#lC;x=_F>yPg_FdwK#1Duc z5!Raj8KL*5gg*IK#4m_n68c>KhEV@Cp-=uR@n_;Mgg)1QC)EE&=z~XNVi_@l@x^+Y z`ee>GlKD*<-IcgoYIk6L7vj#u3R!EO->LAfm_B)9VnbphwB7i-3b8t|7O_TZ@;Y-o zTC>sCOW)e5;UBG^-gQ&6kDA_lq-HN!{hnw(n`hXR@I0HKF@xay@%KK&ebeXq0fhSf z(_F_8KG(aXzH|CyenUq5rj7hPabMSm5bFM}xUc+3;zZ&RsU6AsIN})M*sOJZ zeCj8pX3fcj^(SSmd?s-Q@s#wreroDxrB8kiaX#_v)Sk}zT;govoUCo3S!`T4|U#O3L8eP!xbq)&b=aRYH<`d-HRYT}i|tJ3HC)v3QGeR{4*{n|#m4h_%f z`qb?Gdcu8Pm;K}q5bq=2kG30s-$=ZLcsudd)Z}-}@#t;YkKE{{)cvioR_1qVbSrUN zYPYcFcPRS3D>d`1!~5RU-kqA>{H=>~eF*l9AH?@C{>tAVzD9gKYd^yJRR$u5VBM$LW)QNBoBPZE8Pd&F|Uh|A=2?t?OT<{_E7N`2%77@3U6^7x53` zpQ-(c_1_47>qdXiTJ!%Q^!_`2@OT+9Ack|kSo8l0eKhAA%lyWS*C5tR-(6U*NZgIM zd-`0jl=`aa)3b8wt2EkbXn4k}r)KYU2=`e#`^onrHX$}mpX+<4{^0bsXCqJGzjd)yYC$WAE@hIZaS?l_-sh^sfHBTh0 ze?r#E&mztx&P$)`XQzIC`s7Q9=MfjD&-G=gUz$GoWyDK}m!|f7)-NJnK)f((UB5W> zt5UP(6@>LK&szCS#2bh=rqA`8Q@=TV^1F%KhLZGHStS=Z#(+A=64|U-=t6eAK~}; z-}JGb@QtcZ26H}nawp;r#2x2+vA#>{cTS(YD#3R+$!{>$tEbMl=x^9Wz9(@HV*S+C zWW6r24zXVDVSYnG?*{3U`Q|2@5u2yaHQ#0ETc%ImmDrisC4H{lU;Ux!llLL^BKA(7 z>-|#SH+}Nq#G%At>2rN#>PMtc=6i_$&g4_lcQVX36T07wd&*BCPAASt?Nru$Ba_Dy zely&ISU-us>Q7G1nzIP&&&*o+T;govobiTtqxKeXcJ_{o?e=uO?na zT%A7G*QS0=`sAC5cM>8w_P^`H_hD|jOX@qPPu_#@ z+3`fbhqC5-j(+qH&AfZ*-7~e_Q`3tcdw4Fs>o{Y6=fTII9Yq|SKG(+)>c^tlUp|F6 zi8wiZt{8_jb)mUtY&H|_r}duTG>xg7s{^UlCE-+JhN`_{@&C(b0! zN{!!v=^2Fmo|>9@*5Q3dYEMf|FFj%gp6fiqGxGZp&n90*yoh*l`ku@BGU5{A()78$ zJoPKnr|0>pU(skUK*KYAVQTh%Dd9dZ$$s+n#A}G_QhPaTeut*7BCgI_*Vm+eZEDus zNLYVE*2?cC-c7tGeQ#m?PU7vvJJRR+rqpjupPrjjzopS`L&GzDS8DctKjA*_%YO35 ziH{N=OP}jc66&8wpZs0o+r)R$=lc7pe=mLV?Zl6WAE(dtPgDO%`s6_G|C>H|wgPcy;x2Q(Sl=!6yUzJ$@_NKN z#JZ`i!g@_&bz+UIb-h;VYo}(-`h@lO$XfYc#3n@kHpQCXrO@+tZ6@zb>`3fH?QZ<# z_iFw>bNryx9*DL%0mpxi`Qf?VGW9LeCvQ(|Lu{Mc)~vT8HX|OAnt9gY-66H@QqxNh z-$$HlSN?iNe$V{g;y;ghFnEl8`Vqtl#EI#1{m9f$ zNuQpRQa`!T_??>JnLRo+d-Gcq`<#~jF?dkv`Ywq<(h# zj*ymm0ycN6s82ZOubj^?xD&o!yEEDWo$oH`WB=u9Tg_VgwOZA3&gE)bt!i7XYU{l4V1?YH z)%;!b%&m5}^zn?o!$DiyYS!*UzSXLhGc8x!YE|26Ra<+)gS+P*t+pZ>zeDr=^{tdX zo-xkb*0!3p2jOY8s^v_})wWvIwwkqjGpdE<^{~}+n>D#;O=RecEll(J#zIG|ieS4q%_NA`f*V_G>J)_G_MN;J-%bwe^?K?wJ~UMNiH)U)yR`+iF$I*_W$rwW@8ks;x7d+ps&I+D1!g z8+T`JYwLV%ZL3wS&*==?+E%N5Tg_VUu4m4<5AKcT-EFn|q;J#C=iO~Jecs*b1^b^b zjJw-v4?**;@$Tb4qljm4|0T2spdHou9*E{WxNle6Yt|m!`K9wny{sdVZg^v}04F#{W!Z{AVb8Z3}AEHY0jptCi0^^qJc{Yg=ur)ZDN4wOaXV zPx=qG%-UAtU+c#C9!%c)*4Jv~t3Bx-|G7_}t<}oM+2a{nU#pd`_JsYcJ9&3~Ql z9-KYS*7{nle9p~1_0>#Ut$h6J{^fkFrq6GtdEbK_&<^Re9n;7E`=IVc`21#CO`pH* zd3OfzU>7v+8uzPs@X+j+c{%>`u`+_!nI0nZR;X*E3Yuj^WkJ;TTT&dr(3*S4Cq?$K&h%Q=^;ZMCXxwW_T> zeRiHT_Gq>J(GG0->pLKQz9-My*0!3pCy;Nos`Xj=+E%ODRpxF@~B*K6f#JOjR~sBJaAq0szpve&$? z)yh}3=J>`J^;0_{HGfa+*ZNvbpX~k1yBzslO*xtIxaAYWg1CY3}QtX!o_Y z*R1va)LHcNzT?~XH_LrXdrY!A{bx_Z_gK~^5WVL8ZM9Pu^XyD&eMe3I0keyLF0>l6 z3!is2&k;V~QLE|m9hJwKbKZDIt@dQJ)5v+J(=&km&%wq2{z0qxK3X5o8tOK z&cAB)`Hos`DWCiLJ~-2&+FrBPcU1f8KLgEg+;`%>rMYjNgZ=}aE6&ntk4GaG`JRwI z-={s>+E%j`@1WJHmUAvw+iF$YYSuonsk1k{oc1I%?@RNn@xKG&H{EJ=uTDqLnS{^# z(rWs=FXe#;Pet=zTeR8*XzcC02k99+15FRPMcUq-&+ntxo{r`_I;*MG*K14p+}FF& z?rUwYS-W3z7X25Y?ayDHG0t3?`!>%tcqTr7hgle0x_uUXq`Rm(Y- zt8KNaZMCZPIouP^;GEo})y`c)J1;e!ak<)7tJ+qpTJ~SAw$-Y()vC76XYTy&d}_~L zLVHek=C-!Z*VeXL)jA_*)z-FJ?b~YB`n`D%@~#Y?i{|&%YR^mGg`Lmut=04$)_CB- zC1`$c)nD!TXnr%?*Fky)moA}QwuE*$ns@EuuC~{#?KNxtj@WKVKMpEGT>7ozFAg2=k?In!3t=S<534_=JsOk3?`XwKCA(lfYf z3GF3IXfH)`Rxj#md(GNjv({O$|Dt_+&DvhG)|u9s?ePk<{rSr?M!(XWmCuztcqKk( z+G?*t)A#bu=S*8opEIpq@Zi;G&a~C8LvyB_X^@`5HA`sME}^{!%~@UD)%KdTy=JYm zV*f?^_L{Z5X00==Guz_^G-t{)M!(XWmCuztxDlT-ZMD~;>ASx3In!3t=S-^?Ja|2t zGi|k-&`w9=O!2=*iD&R8v@`fiZjt8fTi+Y-_1YWJoZsu3T7A8?l+S&gU%Rihy=JX5 zt+VLA1*K2PwfdbYK#X0894v(>7Wb1qlgYE|26 z)}Gna!GpJwtmzrNJ!@O-9ZP8MMDt!Xvm4xsc1Wk)mcEkJOa*tN~Cfb2be|_IdAJ4d4ZL3*(0-jc@TF$gwZL3vnt66(uQwI;e zjrNF6`wrS8(RiMCE`6oZcae6|Vjg^#+Ftts+R6CX%V)P&_ASu29SXOazQY<1?0>gK zeEZd3?N4Y=|1W5M`#fXRmiF{yb@~r}jqe%!rEhp!jlPStXD;T!Z>T-H(|(Vp@3%zu zn)kJuzH^#dbH7_c`(tXHGy1i@R?~NGvnT!IzjLJ5%E#G#_W18H`9s#Wn!fYM`wZ?0 z>kW-_F4CR_S1*0|m#NV=`j>VvoOjIkGx#e$?{lmD4Nc#liF{|_^FFtlzQY?2?0w$?hQ@c~Gsam;bKg1} z{fA?G2XxwG3GI$()DCHAPoF<(d(GNjt6I*vTy3jWZL3vn?TKeN%{^LewuE+v)Of}? zZ(G}HRoiM+%l^yNwp!J;TGiJ1%-yLwpW2<5(C*Tmxvj19wY9BQwLYisr>$+Z+PBrL z_51a1`v zhpXf3wKdR~eOotN2aVY;($-DiTIkt7e9per^f~+Tz{7i>Ir~=I6zu{u&OS`fa09dp zVRDN!=iK_%$JcB3M03vTHMRPBO`kJueeSE)?rUwYS?lcUEc)+_=InXKICE(iC9Bha zxDmeRcG`w$&c5*sH(o;9BsKiiYu?vt<#V2Sj^TZ%^d)%5u;s~0@H zKbr5d)wW0TeK3n*dWH{JLVF+os0Z;~83CtCg?zr2lZ6reB_ISU%1k&(Qi>t$d!@J>f;qR@y^y56-@vuhq)e zoI`)Sk5(%mXLrBYw|uRp?|IFh@Nm1-s;;zy$$7u?UJQ4_=eN~rJEQ5_q4OQuY5M%O zsuw)m747g&qrd;Yig$CEp5epMj)&d5)%*rq-$U{B+QZQNwsvW1_4V3PKKJ$8YWKCa z*R1v1sxN{8RN{Qxo`6f!`<-p+U{rvbiO^($NtXEnatO=nzi<8wW{Tu%hk48 z)wWvI)}HWi&)lQc_Ch93D>5oh5Ur?`&a_-@t5t2QS$kqr=bd0C z{`)HPz0`P*%O1&Jo@bcno!98QNIPjU``>fuzpv7IhCILjzKVPK?Dh&@ua&Ry4EY_7 z+E(KZgyvkc*SxRQ%2&1K4(j@;@tsCL@2dS;U#sc!oA#dOy&dwM#<$;UN2Jehz4ghh zrqA!ZJn)czw#7H&Z$k2rf5ybV4G;M(h<#g)--7TRGuOwu@VRHJ>9fE89M$7*2=^k+ z(rQmcb9Rkq$X!`Z$S(E_NXO%tyaG3XYSEmKeba+b3gZOeXUl$Ivf3m zkImXvdt7RqJ)WWUwOaXVPx=o}Yx?EchUMezakkdiYUT6H?g=k?w$dJ-dvNyUe63c# z<{bLteY9HnIJ^7BzU6B*ea~z5gojT^t?Ekin{MvK@N|5BldX0Jn!YCyd4Iy^x7BL; z{I<#i56?vNJ8!jTpk0i{-5jQ8_%yUjU~-GJy*uAo_$lnO+iF$YYE@h3 zGj~aMKDA4i(D+^!ow=>8^R>0DR<+K^Gq<&^R{OS^wSGt5jl3(vE7APkTI~htdw%Eh zduug)ep}TG9=;fDKmKwz6wQ5G?Pck^3O(z>=f16`&+oiE@bDFA zo~6~ULvwcSm!9F(XwHP(BF%5Q^}P~bue}P*d;jvLR$s5_^ZRXm?yJ`BYi+Mt>-_30 z`fotpP$CveoqYE~^(jd_9`)ven*$=G|i!!}JW_gyuaXw@CB7wZ1pt>$Nwc`QBdF z)avUseMfgb_w~KC`&!#;*7`2%Ec)Mv=DXw>uWWAev`E){fBQ~LURv&Zy~I2 zeXXX?-vayPo_d$lZcdH;m-DrnKEJidw3_r4j_6f8j$>Cq*>ot9Q<7xYS7;PW44>q++Y3|$m?C0-Q zyRWtWUezAvK8LnHe?7m?TAKax*?q3zM{|!>``8lN$5Ugk=-Jk`TGh5%)pGXbYFn*p zTdivAeC9scoloskOK6|&&fM14`P$l6t6HDa8Md{pR{OS^wcg!k?!(Wcd3Rgw3+ekT zdcL3Vd3Re)pLe%B@bF7$-rZLF8rmsny!&B#hF?W{6ijZBws+_IGQM8>3Yz!ji%qS* zUeovJ&gZ_~({^8Ld(B$!Zk<{GH_-OyFV7ffF6}YN>hzy7bN_cYhx83^tI>CncIslD z^6kYta!%QQ_)WA!;8y!q`o7-z4uxAypYN!8!Nc#M`Houc2WZZjXN>>*9C(J`T|)cb z6599Coc*`E+FrA^*Q|B+?7wK=UbD8>to0q$neFi-G~bcu_gPCjcrg!ujL(@ip5g6i z`hK`j7d~g&YWkdM_O@>LQ#5DVYQI2prktrf!=Ejo{d@`S|InP(PrBM(v$of)byn=Z zXy0D5w%4q6rgdg}{0hyP@{G~1G-u_rCl7y(&zZK`Z_xDpvhz99R@3K9s~0@{9hx(3 zwf~{*iN=}Y|LzN(;UCfV;xD;HnzL_xzsJ{We?W77zin#u_1aQC_jP{lzSj1dwa&E8 z;xh~hXX^QV*3#TJpWWvg{s~{N{Tc0m&i9w}v48YzYg^4)`?Xrta?a&yTditat!nc< z4B_ElbB|X0Tb{Yq{+>RbF`l)pZ8d8>bE{P?XIied)vC7Ds@55~Cp`Q|?$K)hL_4tQ zukT;!;~AH$Z8d8T!qaM1%bAv|ZMCXxHEZ{1>fqtOm(c#Rg!W&w-SP8`@vK$bYu5H! z)pDlgYFn*pTg}>oscY}|GPFZFZ8Ya2weTJ2JCtZOeTOw3c|siCX)B}goyRv;p6L?W z4DG1ScL$iewrotawY_HT(Vfp)zWrtH-`4h;wa0WmYx#|sKeIhnCH5zLM*Ed^Y--lr zk+>7lYb&I7Xa4rSRx6)-=rea0qV1=4x76IP_qAI2YES#$HEUaK#ne2z^{ubf%2#{Z z|L$4aYUT6nb>8x|TKQ^Exz5~bE9D-Z-Ltj6Rx4lasjp_*YUT4x?$`QSP2a&p`@UC$ z`DPYrtEX=j{?@(F=Qq=8`uJw%J@T3a-^?Oy1Nbz;J6E1{SU-+1*J^vit#2*3*Vabc zhggHCT7A8?l+S(lCE9(h?KNxtX6h{ZHzM{Yd`9;z&3)?}_FtFiwe`>*-}&y5KKHk0 zTia^ZK7nYps`Z?GZL3vnt6BTRrcPde3GJR}-k0`SH-^10t+q+}Hso*3M4$Ji)%1B^ z$|K*4IK0#D4_{0;@ABN2^(BP4R`YJOzI(&Hb{{m~$EHNp>gzRqmv%n)^=`ELTH9;Z zdSB`+`X5MmUwlUQE$y;ob^G6sxSX(Wui3ZNd>?JE2f@CNR(mk4?*T;Zsn7S(YWjR1 z<&ifhd>^g09ee@d^OR>R))x}yTFu$FzAfNh+Y-&$Z$?zDzFyO}Z|8GgwRT@?d(B$k zN1a9g4utQ+XLR4vE=pFn|2D*PJ8f&&nYNy7m(U)Pn*QolzE&$=dyXBc@m;psPT6mJ z{?>W*`7T>c-(ihM-i7d8R-Ky9@B45T<=J%!?V)H#b-stezN4Lqwzk)-J-YK*>pOD) zwzk)-J*M+n>$|Ko+vC9W_>A>jYR3|4);yfpjp(&KQ`?=ty|2~ESN+WG(e+bvp6=m( z_G^8uR=(QP{(BMDwwg0h^X%5QzE&$=?P-7CMfIz*mCv)6R=!p%U+pQ^nOn_w;U1pd zv$eifD_`xYuV&h6H6J z)(Z&lS9y+L?eCwtR`dL=&;Gq;f1k^9SFOHY)8{N&pZlt{`&!#;);g~`i~i#YpUY=- z-_kBhR=2<3y}y5THnk%=*5fzcder=O)$~`d^0iv|+H;&hO+9O!?ZoVN9DhsG=UH1# zpK~aW{7Aw%wA!ig#e}mc&&jMWAj}`n*G}rq4T69{EXxcc|5# z3BR219hBz`)~_JUwc6fr>w7ZXYp0{_Lp+hFT7A8?l+S(lCE9(h?KNw?Lv3nCU&;9M$*0!3puOwQnYCUIP+iF$YYSzB0sgs{Z_#0Ne z)SgNlOn9#HJbel68L8>-eXUl$c6R4da|qFD=ViaM`CBvA=lyFnecr$F$mbJ>cbfm) z@ecX^%Jb|cwC60LU4Z7hd=}Bx_L{Z5X07+i{oC4Jv$of)_5RhF?cw{`pYR#&SDNpr z<>$f9wAK6#*LM+7b^4rXtLby5<&iHXoN24Q2sY=L%5ypEONh!@r{?Ti-(|4>Txhi` zVCQFj_0wmMRebl8Qr%uzpFZj{a;OZ_gd|0*x6fKo@w7)iYi~gF zy}g#GT7A8?l+S&AZ|%O;_L{Z6%Q}nx_Y%HKpV56wbKg3L{oh3N+MCf1=zMQUpZnXh zt!*`H?bm8m>pA<{R;$`ptJFc%fwVs>P*J`(< zroVcXuhq&|wdQW?`l-DuHP2+f*4JwK4kp@r`#$)PPJ4g)-oxLz-}?OaTTP$eetF~% z5{Gx%C*cbSzn$`Yg!P4lxmMd7Zhaqud+oz$ehVKUs#agG>D#ySxvyHgueH5q?S4d^ zMgOM>zkQ$4eM|Fqyv||&j}g!9w2#8h-rDkfd1tyaGF9G{`aciC#6&3>Qa zZ=F}4@3PhO`7X;Nf1dDNw%XU>iwS2@o-eY#gfQ1?zPHx*1^B2=`!c)_@j0Su^GNIIS@eIC@Ll?h?pxYr$?ER;CE{|zzOClJhOlp|c|LoonfnUS zYhPVL`&w$XM_b!!RoiOTUP^e@cHdsJw%4rnPJWiC=l>RK?@+6K8`k#?qV;))T20?! zjYs}2;T@_vwI37b6TXA;{DAed33ILH9cq2wgM01!X!{V~A*xniuj%u?v_AJ$YxlLb z*R0)-sI%z*39&!nGuCsdJttY+{y$to`w^NmZR>stJNs7qS^940Z|$ki*|(ZL=Ug87 z7ldc_OvS%q?Y=Gl8g}1S`%U_O$zS_er_X&`O`q?)Jo4`d&(dmthA$@UQ=UJvzJxH> zYJ0=2@Aq)8{Q=GQ`&*)F_4S&*OFN(Y?n|`$TH9;ZI=?!L{=X8wbDz#t-Tr?f zE+_2UYxZq5-$&c)Z}1_V_IFs{Ux?aMpYNm9^!Yx@Bma}|eKgu=8SMM;dCK!|)|U(h zk!v+)-}?RqAJu99g`NFBh^p1+>|4z}dY`q-e$l?x9@A<1d>?rh{i89wKjAa7@4V*w zsB^gIe+b`)ef5^+@3?(i&GS{Qx&INJHX1CU4O6Q<+S*2&uWdDJot5YEELGcSQQK?Q zdN-QqA5CEIP^(Snd?T9FoWjQ)TBPaY4$XVu(H+pZLyNS#qMeV%I~b*BbZ4|@!{ioe z+@VFjJL2oLJE1Y>(X6S}*K7K`FRjmg)!Kcn?KNxn!&hh4e|I$Qkk1%rF6}wV>hvGo z1z)eNfX3|Gy3vYg%zlx!Qu^+Op8dn;>|0Hrvo8-kS_RG7x7vni&X2Q?(lc6h32imB z|3}GN0L^k$ZQC>HnRy_%I|L1c;6Z}~jq5pacSEB%A@1&OC+;2~5C?+0ySux6*R``( zziakP)nETx)zx=jeJ|N-@18srQtu{>?@=&k`miRpWyWsW_>6U?)Ss%`GGqOIv(MPI zV9~Sho(^;Ntg++^ws~d5bdTnG48E2Ax%$BK)-pd-YFSGDPP4ah7O8IJUUHIs|tRed{3!gn(ljo_m1$%O>LGLrZ%)*!Y z&|mf=v+&Wo^-A6FWv1_tRFmiKDV0TBFuyI&cRd%~Gw}IsWwtC#-_shO-&SV&HcTF# zyX9b>NzP$cpS$I2zVzHZ2WH*Oo?Cs-zBi9?#b@2j^!ZIwt9W?so)5E^%w7dMbK29r za1MJh>@0lvyPB)`TBgt62>S}(3t)cd&-?#i)@^;}^E=OV&9@dTYWVIoFu!TmSn38d zU-Uu#?nSjmW-GwfX?!oQK5CVmIW{w6t&v&8()&zoW>Gh@h>iYy_mZY(*-PiJmo>fQ z*yu0EW)`uo(;4R2%%W~)5o?^BHFU42y=1oH9QMk}*q52u%px|kh-Hp5v6)3|X2vce z&NVzn`aEZ*&+jdA@!Y*0=J%Ev=gj{-)AQO@PxmI+ZanjDiuoO7 z-y86??2Rzb-|JGWzLw4NS=VnX*EP0f#;%om(Z3$d?~OHZk0KV zxY9HAOHO^pnlCeBTNbhOITM>%#AX(;uA}zWty*hjw%Q!VUX)(wb0#*kh|MfwT}xXx zvxvMhr^>iD~%Ql5=*7$fXK7XIgnPW3E z_H^E77P0g>6PsDYW@hXeDURoE3)sO8+Z^V3v9Idsww%MZsto^(FSGEa*>&3y>vLw? z*L>T+>x_%f{mx9Ea|;j8-HtHll-V9I|Gr}Xy6Wk6p2OY@I|~o~DPPOmXvu^7%pXV>vHP-VNHGH=h%=5<@OMk)4m)6zoQfp+kE6i^)v$t0twMtHU>ti!B z)*6{bEWOXfW)^ibi`eMJcW-HWmc4Zjdt1{>j*b3uY-SPbI-OyT%`EC>7O}?3SwqL) z5M?fz?KX$)UK#r`6PsDYW)`u`aV9pih|SDczhCEE&q}v9Y;nWhS$%spKEJoj^!dF- zE}px0!~EVd+ZT2KjPu%6Pxn69fjskWiml%G-h;1Y?}e>_?_DWYU(59Q9c7<&Wx1}g zEi=~dEqd0!Kg{oqHI|-(9aNv?3Tdr$t%Zzm|qZj>0!Q4yt$-2REF4sg&|9x>s-r~*7IT#Z5+i zNpF2@X2zN?vxue7nb^!CHnWIz9oFQzJGIux>@?WADZjqctB*C##AaqJe>=IJ-WRd- z)U&9YS;S^$tbZRxzdUzmz_w}FnK1wUAa7ZhzF_2?V%tqWcl^!de@~M=-MO&sVbpT% z<|@9Hg)e!!vukW-=TwG2a`nEt@ULVK4<3|=(qb&$`>qr%woouS@@zR&*m&WXZDfG=)J5V`!Wk(>O+6Y zms$Aey{y4};mb_lBHsCJ)o-o46}Grxw^iRq8{hgoXQpq1YZVqtBm?HzRdKUl=AuRvvb%NDx-eM zmwlP(^LHrLP~Xik>*`|`Wv$tlnLdB7Vy!%PpP$3NRGD)#SN3HVKJ&Q-`t81$@&yYY zvzYN^7QU#-vpGx8nSHr3dM~|YUuNM;edsUwG7BHQmo=C#e3|K6#5;d8>bKT?9k#e( z->AN?Hoo% zH&d*>mg(E6@mY6GSgvbq%Z&B+DtgiX1K8T!S!3xr*v|D?PW}<|_5TNAN8aMijJ#89 z*XifcM~QWhoKwx;{Sf9JW%i@$`+nndk22He9z`ymyPv?^qs)E-I{?NSyXxtF4m*%% z-c2$0Bl~`euVp`jxgS4HvHDu3&)H|6b!EA(u`M&!J&IoR{|@FJvBuJKu!HKeoc!G{ z@g3Z-U%;Gw@^rtN!+u>E{>at)G7F#ctbKI9C)T~p><=~HZyTR`nVCNKGIH_U{R!q? zX7&%*zA$FdRZsU<*nT|oZi=~Y+4pCBE&B`1efwjI)z>n8Yc@XX%5q&}TV||#8NKNL z7tFn6jiu*c``2eV`Mba2YuVpn?xpLmaoxXR&OWpMRNp@vpR> zK982F`O@>KgIPB-g6(fKyf=?=#Yf#KrjI?hR`DQ46BxZru}8zUnD&hB3)>RLyHSlR zjQ!2IMho~_b{`n~I~u20eJz{kvo8BPRoB>-8O!|oUhMO+F!tOvmY#!IH?3i`G`^PI z54KL@yMOgjzvQI1J~lIB&6iok(&tQUW)YiN#JUb^@;rJ#t&!OSVe6*+`W{q$tZ^nb zGh?^HlUc;l(@bn;5u2H@TcZWl-QO%0=7Lq zYPoiE6<^E3mpr3~)!57)UK#$#)%!9FU&Q+Ekxf3?qbj5Kk}vx*)8{wsIj!e*^f(yb z{uFzB^*yHXtduCbdnOy63t=vn_WVQX_|jiu*cN7QFIHAhdzcO-Z67H_aq zcqZ=@v%i>$@19C*%bqreJ-xE1kz+H9*vyP|R`zAj5!*6jTV|~1#h&Z>N6(tWUI6pF zEZg{6rq45!a*du1^9*IS0_zt#ed&D|Rjm%y+hrI~4I)2tz`pvPK zMQmmfOHVVgnMG`7#`=5e{Oa6CFNG~`*vqQ##f{HzAv1lR`^d%fXhoQN&OVRo^XOGI zUwR(B8fM+hUQ>Oqyf=?=#b@2j^!d$Dt9W=Gy%uIKnY{_-@3?uYXY>ZxVLbD0in$-z z_d0wndp*oEv{H)I*D`&FH$LmG3CnejZJDvoFM83x3T$oetg-YQ?1=g-C;#Y;_>SaG z-r~)Syi?3`;7n?+(W=C{ADOKN)3_=u>z_?t`sCq`MtNcwZ z&&=6pA9E`Ekr{I;v)3E3`rMDq=J~Aa>~mdXTV^a*-;4gOVD5)?tQ#zzz59M0d@WlS z2JidTw_f#GyN%7vSl?$BvG&-;W)`uTMQp_JJYv5~jm$U~mP1I8>y z)ic@?hIhofDdt{g-|qNYwg+qteCCXN`dT*6XI=L#*EP0f#=4i$i~bM7+)MkmZZPXc zALJkHg>RjP?LCLR2WBkwOU{UGnXxU4So)la%`9Rwi&)oTO+2G_)*6|;YYuyNWvp=~ zHnWJ$EMloY6PsDYW)`v0pYPt=^e21Y9QOXE=NudT<=D(3))_gg9Gh9x&CJ+E#H6z_ z+84ICVf$6zKJa>`if?_MGt;+0^6)(3H?n+h5hptY#&415byPhgep~$Cdwe&={I;_1 zKzuFZx2l}K{Zp(y&tzu${JqJ{x_(=^uCXmM*6%HP(SI1s?~OH{)Y<>fEe3IvnO6Wp+gM9SW~~6rX#PnLhU@JUow%f^FEalVJ95 zz3Leq3v&&;n_{aszN7KA>=>B)ab$|s*D`%;Ha_dha$RFvW~_S@J?lRO<{q)e(sMB9 zp4K%w9v}NM#g2nHdt>QA&*+3X?8M6O&-gM6pLJ^=qf?1>FEcx><~zCZxtE#gb1x$o z&!aP78#e3`*qJb9F{+-?S##LAF!%`nl&@v_+~1s!{ADjsPqFiub=~vqGoO2z>l*7` zMh)Lx26Hc2W9cuL`O>;ZXV)5;oda8^@m)}T&c>XsGh#C{)*6{bEWOXfW)^ibi`eMT zcjq-d%g&#}mNdQO*yu0EW)`uo)Ai=q%%W~)5gT)24WkQdFPU95hh1D5vn?~uv6)3| zW)aIAXJRvp*vyRe+j7qJtc)(7!>)(?W9N!0(y0X7Slu zX8LkI-(3T9UyLy}nBQBu%>&&I!lerZkWSvgt>35aV9pih|Mfw>1if5vxv>i*hR!SzdHBPhhd8w_L1tl zx$&*fb7uNHyOE3M^53TTEo62(z8zrf{iu4%fB&*0&%B#r&L#UkMr_M&g}Ilvq*#3| z)3;OOyA9^q&2^1!nX#VT=tcjXFwgEsxm-uEo$Irl{N=w}+67JC;?0b_Q_THHxkh(U z!~MwYZkWD1xN6Pfb3ZcE=YE8T=g}u%?nh>yfgK8CJ)`OweF}CM&%B#r&OZA-iLYh% zz?}WZQ>?z0={vmfS=ZU;y2iH5Sob4((f>J^`@tGZ&%utU&vNpQK8^24?&K}r%*Z>% zoU=2jxkjHS*8Rxr3ow13ZG7%WX8PQZ$i?&MOEC8%v#-INDeDpH(& z*VvXB>rA5;{lA7eQ`T5|4rbl7uF((ht<$g{&S5`=8B6_=lio^f%ZzPV#M0+XY-SOg zS;V>yYvLLGsMg5r$8*?EDr1c^v6)3|W)VyMnb^!CHnWJ0{(Sedra#%w=dfQiJ?Gfy zFUMvUvCha@<=D)kZf3^%9eFnDSsDEX=J%G_Z>#TDjnD5bGktzKXkB=J&?CDdxA8eSg5$vOmH+f4@ty`dT*6XI;OoT-Vr^8M{{MMgRX`es8R? z^c>8(X@RcJKVZgEzvQI165BFkTNbhOITM>%#AX(;uEUymMt`j}GW**c z_V>zI<4kO35t~`WQhz2kvxv5kiY*>B&ZG7wVoS8nqx5&lwxP$qT8+4!yBJ<`L?F&8rw2s{obM%`&=5v_vRW)&%utU&vNpQ7w{d) zoxH`H8F{A|`;l{v??){AF~#l=(|2F4TC@1rk13{){pdYBj~@tQKc?8jVC;wM8CTEv zAu#uYcTavod}$%( zlzlJ9w@JfZ3G@8DG{x#WykXX8ea8B2<+{dh)-Zi*!J-%auY>u$vBuJKup{cToSNem z@g2#Xyu};r6rRaD#q2L;;=5N7+p<^BVXvtyYUJ3=A~rK)ot1srbHujH*p?aV8M5cP z{_*Q!o}tX%P<^j$e4e4q^lgwlJdfW5^9)6P*=jJ)2755Bp7F|a*eY|_sxW8z#wNC9 z#bA_-mKp0Aik{6u{&I#`W62lHnbvjc8I!BbG&6FQ**k|CSA5PiGkwl9 zJodnEeVJ)y#Fg*Tdet+gkMdobGc)sN-@5o(wjNB6-$=yjYni^o8=rNZU#@Fx%ZxR5 z^rD}$QfA5;OWj~c)MvT#<&2i^lDy7iijjAU>9{Dl^uX!GC z3iFId9G>y^FzUFTvCnwMo6lie!>WJE*D`(XQO>s+%-L$X1gxgYd773)l* zhVOR7=NYj7=t*Y2s7e0u7IWB^bJ$ju(Pzx0kIgJ%GmBVypNY*ZVl#`_=-GGMH2ul8 zox`?kdd{)YUyjWzVx5sQ%(0n8-OP;jOs2VycY=9#GuyfPc7WGCD?ZO-X8Jt4;o*6_ zE6n}QY$2+4&<44Q_M4&eQ(CsvbVrIFT12zeJ#`Hnan=x%5q&}TV||h zH+s>(C(N_U8cWZ?4yw;`@{iw+@8E{L4YmlDJmYuFVY^j^KXUcH%);k9Yaipih;=VB z+q>r5qw%?yndx&cBNxx*zuj^#GkYIC&j7O+S5Nux!<-ZErkMMdeeWi=W$%HxZ|_X8 z`dX%svpVH_FU)<*b&YMAvF>H`qW=Rh_wrp_t|OSUOY18CU7zQNyv0k+o{@KoxxXpb zcpqxGADMj+rtke+wPx|TADQWMKf=TFct6;N4dd*UnX;a7^^EtQ!}$JYe0+ap&igj8 z&OS3^TV||t_Uz}nEi<-d#=0NTvpEidxgV^thx=Qo#q&NMT9 z&NMtckB@*k)6C9*tqP;3arKOkhONdk@21%5jqgZ&EjtS4{0>jC`dT*6XIl)iK zW1VUAtp6;SGi8mX=U~=N>lz<}uVu%={5vT7j;lWEmz?z0$7W`%`7(=G`kaZ)EMhZ@ zSl3}qp2x@68kwC?d(P~{>SK*Fv6&fb&zVImJVq0cx%OaMZ zW@0mo*vyPwM4acep6~J5u*D5Kr~1xpeCzX^nZ6B@hv)HmFn=Q=zwBC=zY%<6#YgX}vWU(1%jJb&k=SbZ&<=dJ#AX(;nMJJYuqMyrOKOeGF0DOhc3JhY z#+lg6jJ4;?B9@+JVl#`_%px}W<#~L0t&!OkFn=R5yR!ONV_9pC&CFPTBQlFvdYXyN zEMhY=c9j&z^Z2Sc?CLq}8raJCS>xD#BerG6wk%@lX(l$ah|SE{Ma0$b)-yl80p@Q+ zW;a&fb@2Kf6raBlnd$R4B0M~gZ-)6Bk=flae_O0qJ>w70VIPJ0P2!*OwM^gYO}-Do z)_~oVV&^gIwm$Ru8Rf5FU`);0b}t&!O+Fn=R5yS@6TRdUi> zADfx6*2pYk>3t?Pv#6U{#6~Z^`&iSn?AAH#wx*XH8~x?j%p%rxI>Q{BS=7xeVvUot zhVdP>m(1>*!|tk#eVK{PEMhZ@Smrnrn_0wWW~|?vbFOD){7IPKTW0rE-^UxD-&ti#E*vul9-e+Pni`dK} zHv043mzw@$U!KFh()65TqrV)RS;V?dXP9F%i@KQ^>)EyEI`{E6V4mH~zFB==YkZ#F z%=B%LJUoxT4fE_qe%TLThjM4{$JI0b9_+At-%T;kWcGarU(3D=^Spd3<#hl-r@pSSEi-m4Jkg8(AHh7ktg*xfJEA_z$v^%+z9YGlw|Fxn?-X-C>Ws`k{xPxc zM`k~P>H8s9tyz5TM`rrmkMQt3{u#{u$m}<;ePPzCp7AeX`|-@XDdz06@8|eh_6wM^ z|7nWV*D`(1Is2?D%XN)ynX&Fi^rHWFF!zHsmY##{U!UdVAO8wp%YF@Wrmnxnjeiew z_L=>m`hMH^oPB2coO9&jdHiRXJ=5p7K9B!e^QGtU-(c3w?C;h0mwWRVSA5pZOrLvB zt>WQ%{7;y@BsN(JcEGe}{2$nXgMshHHLfuC*LuPJg|B7*hPl80XnZZxw`SwBE^Ml< zHCkq@^Q*n+pNwGcIcv1Po*i`WbK&G4{}1264f`+5JvX*`CWAR_SQ-8qUuNM;eN4v0 zx|f+z#JZF2|N1RH_Hv5pW8Y#;6P_pcg)sxy9nX{d)qM56&y)MZsGB{L2UOqEaPss% z>t?2pGt_%{o;(O?TiuEpFHo ztM74*Z+)IK)93k%Ts%*ffqDKidnSzEF!yg#J(H)x4&<44Q_QoLeNV>MvZuh@-zTM5 zeJ#`HxynB4%5q&}TV|~1FM83x9L)2_8cWZ?_zlZzm^>Zd!3}#F%-I`D4|*ogn8TK> z4F8NTv+z03+Q;PC#JZQ6Eno9JtMR#)ndx&cBNxw;=fd2}%w7m{KbXa&dM3}C!=68f zy#TfbanEUDTV`y_jCGHwKUKG7##^+2kdkIY6 ziyEIZ%}k#&ja)oWUIuffnXLvp3Pw+p>Y1zvJDO+SO)+PmeJ{t?vRA;I-%C@hzLx3R zwDDQj`Q^ICw#-;(8olTzf0-$3EIkK1rasHbKY1m-W4V*pc}y|#PO;;rpC_*(wq>uW z?A8C5tM_H5@A#C@cdwnpURN1?mfo^2Gkqtdn&h9XG>2J3AHBQwN%m!???m1^qgqq% z40}Um)SvNXrthSb&v&n%!`@UG^-I3&%S_+Ncw!Cp-3+s?K4wwYnthq++q~A)!}H{g zbJ!}CIk(oAS@@itYoOoB$|+y4@G*-SUuNNpnmn7c^qkqMmC<`yL-u7BzSM{Qk}tFH z(R*2g`NEf(zD2zAH=}-Q6TZ{(dzBgAY4Q0xk$wJNWu|X~&)=(D*I4sJ4c~1BGe2uAHG+jl zPW}lqE;XVL+2T2jc@`hFO3oacS;S@*vChA(n_0wW7O~Nv@7S}co@MOW3}Y{*dd{)Y zUyjWzVqK>_=h)1mZf3@McI~;&eZp@+IlGx{QGNb%D*HUUnd!5BczB*{1@r7?wjIo~ z#@2s!$i|5HMFlU9A*uD^zPax z*_WBV6M63%YE8W}Y_H0wKjX_x-$^N-@AjO--dP#-OTO&OOy9|PVh#1(47092W>MCf zeVOU=@0VCB&y&69uy17QBH^KOdy+mwC#;cMCcFn^mqm}2#{Y@W}$YZ9018rw2s*Gj$U zKN05d6>BU#2eWQk*W^HaEjtLdPUAbc`lw%W(pw*!nX%@}EMn<%CN{H(%`9SFhc$Vg z98zm!b|`G!lwaRr)yEoVVly*#D?FJ+EIrM{W)`uT8Oz@deH_n|!(rPr>S{A&kLnV_Rmd-+uJ0|9qI=K5HyJ2lI@k zbxqE~=Xa9XnJ{N>EIsI%oIQt~QyKmlUuNO6M(ty=gjn}7vkPjz^BSLfnVCNKGIH@e zxd`T7X7(Z27BFTpsh-KDFn`B+H^tny?7J9W%PxVrZx^OmeJz{kv#$G=>l)iKW8KT> zMgK=&?j>t1JqNRHTG!+f z%DVIgBkvU3Zu)t09kDID5$10;wOqToimzqiOP?)Z~-h zTp7KWeA$Zh`sjXZF$R`*7p)+s{m&-+tucd2%ak!-m}n+X2S6GpU}* z9k3mF=G_!qz46_KuVuHx{1!fzV)eC5-%gFsx@*F6U1M8j>{|GuXZ?4>{PtO6={eZW z^;u5-;g|94f+lbAW=7sA=6*PnnrrfLV%?9-J^|BrSL1U(GSlaNL@u5u_rTnb%)SX* z6~=ld)ie1FY&D*FH^rQN_I(Op%RUWr_Mc3#`dT*6XI*EX>l)iKW8IJFMgO;9?gwiu zJqNRHTG!;W_*(Wk*gB2x^VLWFl9S&0*vyPIUuF?YpEI$UMQmmf>pHB-^W+P)MrL2E zJ!kf%>SK*Fv6&fb&zVImJzxB9--_}1q+GkyLZL@u5uKY*>po$qN=6`O@>`$1v+=_LJ)S;k|i` zD?aOHrf-Af;d$~in7vTH#L0dKJ2XAxnfwBF*uC#2HLkGL8{g0IZPKt`!90^c<%(E+ zhd0a`t}Czq=looIUBl!Lu(f%%|ICi4&vjjsU*bCwMo+~{ABCO5oxD@b z{$dTj`!%sG`^_Bo+sdLwj?FA$Gc)#ZYT1`PM{LWCZJDv2-L(G6A7P%M%>Gn;zlS?( zW>~ApV{O1&_uVwny zY<$+0<+{eU%-FT?MbG;G1M>{A#?o`J{p+)w{F8s+YuP_xi>Biy|AjewXHs)b{#Skf zPE4P(&rF}QPjL%Fn6uC9ez1*T^lolF3*8*H0Na>%_@{g=)8~HVd`rQYJ+J;Rb{?~C z>oXs-pQ>xDvyU3|wD15Jv$y}!UoiJPt!rU)Z;dH7p2H^hvIT0zOd@uQ)z~Q(vGhI@ zn_0wWR%84Ae0QIwKiPffu%(-xQ*7T~ik)H+%N~@Tb8Kd{u4kn88SB|~&UNk!4}^Jk zGkegzzJ>cYKF;nG)5qEEbMd_J5Ey57iaj27DvZ5fsGfy~!%pLwcT&&6Jcv}XN{%jVAf6RT6hG$mOT=-S>tMdj6tkb37B~`D2YGUoiK`b=BvEXW?_EnJov?w`}8crkUw; zrjd*1h2>$+G_zO3j)KwCLiH>>4|X)qyqjXqKKq`7uVv4LIlpJ8SbZ(iw`t?EuJg-v zjcu8+&NO<_zY@%uvc}SLuw&}8ocs&V$9F7u@)mDqYZUPtBm?H zzRdKUl=AuRrE}OTDx-eMmwlP(I~h-`p}w18*44)>%38B8Gku%antFI%c=;Um%F3Ku z>&q;B&dxQ^@4|{HU$F2oiy2>L;ftC)o3r$s*{dp}_p*lU%Pf4U5B()yX5pjvvIg^o zFEf3Mc;|0M{ni#<3tQZ<*HzzZ8sGXnXQpq1@!Ig>s%Gh?lhS;W%&Ol)RRH?xS1{(QGu)3b~jm%7L*yu0E zW)`uo)Ai=q%%W~)5gT)Izu2GBOJ-}&Va&bw*q1Wn9Gh9hW)`u`aV9pih|SDczc=Ss z&&mSdTKV2GRH$Xwk6NJn__-P*|!nC zmTe64{8>Bl>1)|MpLLygu4`<|jP-kqUi9w@W3H~T)D33c=!5(V{7q7N$!s&2-&jM^HSS#_q^49d**q|Y`f{_1>*eQC9`K?2iW#7 zYuEh6*Rt>>&jPthY-Yq2hTp!-)%!9FU&Q)u$0na_r^?J@zU<3PUvrih-VF1fwVug( zmKWYqeY+&4Pfuq0JS!<~;q5TL8NL(q_`I-N&6l1Rc86IvvpuTs9sd_seAdlOpWl7W zy}UKyRL!f-u)qO@n%NeDdssaw&q%RAF+!Y_I{YY_cT8D zBQt&ON95vpVISCr4LcU*H_3Vys%K$8nBOGtrkJzOz7OJS*}gDm|A7>%uVwRm)^+x| zuCXmM*8PZH^dAp%KUibwIhb|Rx)%1w*Rlg(>omRttB?96C%yHtnHg)o%p#UPXJRvp z*vul?6PuZ_Tj9wpV(DonHnWJ$%-F3{9M21f!nSGH zVX$pstf#C?Uoi4cvF)ax7Y--3Wkr!^ILt--FD~n8VJgvGnPDon47FUuMR(EMi&DOl)Qmn_0w0O*{){ z)*6|eHHV#D8EY*4=Ge?4HnWJO{!DCU5t~`WMt{CLx9LxI-W+y*({qlE{&H+)5$ihL zpB$T6)Xj|8|G)pY;#cXFT;=~3lD-c*^4%BBc`X0;C^YA{{NJO|oX5eCYn=2X=|@(* zRPw09qYh7(JnHbM^<2b##I7gghP%bxbO5bcv#X8Px=ux4|Pg! z=2Y98vsK%hoZP)f9UiqFYq7q*mO5&wt)+*!-lGnWTF>R&nZe+3=qsvzJo?J2pHcI$ zhvodZr)tlSJ+q&KC!n3J`iV(D1-+#5WziQ@{YyF1A10OUr_Zc(dJ}NO5M=b4Q<`4 zc~70e^OJsI^~hg^c2Cu>Mqk36b(DTW+s`$X+mCyy$A0WzZ9ms?r~cptNxukf5Aqd~ zehJz+$j##!P@BhE%%k)i+Mb=W+;z$6tM{nGqtzvi@uV;xF4_=4%EU8~#{hWo8(^&ONiAPRy4Bi0u%&6ap z_B_x_$?17en^V3Lm$l?-xjmC_@Fw&XRj-V;fBD8}dmygVvVXO;%tMaBDoL-3_Wa0K zL%T-xCTQ2F-V|+4dM|ZDTQ{_I{obiFSUu@A(5_d$X7#H#OMG**-wSIhy@j?nbIR?_ znW*jUysGU@54{Z5N_uUy>yod7b`EM|gBxot=23bMZO`VE+p{xQyGA|q)%(@qSBGC6 zdqMAmb&0)+yLvsed#8?^%&hcpezpC(Mz#IRsndJZ;Zf^xUDO{eN_qpdd&F!?4>wnB z4{~z!TpgTzgT>XOUO(v#lisLm)>rD9Qyq2bWe|RK_{rUS$X&E^W*&nr$mx94TcWS5 zdfQryJuK(vGVW^6kF#Pw2U{h*HTpx9Z-e%1>)8(N*;a3lwr9>`sTU_51vLADz zOImWx}mKb+Pdxqbq4Q6yLamMCH=wbmmi$?A!ujF zno4h>?aiEWdvhjgdvh&nd(%TNgZC%>fu#3Idf%k?OL~8_voeqQor&7~_QTA|x~!#k zU7=l<`yzJ^`suy*tHZAjzgoZh$ol$N=2zBY9<^%;y-(7vCAez|?ON=OnuA!cI@YTW z4>K$M=%-Gv_o>=-(c|Dya@wc*Ftq1QeH7X=z|6{?IzzR4>K-w_!Qn|Ck@S)1CDn5@ z`hu#DL7VenF6)N2Zs@2>ox!ni&zt%9MYTs;z7P)G6FEpq4ua z)=}20CSR|~*K7J39FN~wsZT&(S@kJs`=@5<*;;CQwg>7APE7iwq)$dWD?O*8ot64D zv^kkUsTynmVO_YL=d@rM72#pw8gzq|ZtET(q;&LtkZ9YFNq13`$++ zqqc77s7svzGcL1I^S;c4eoI~Fqqc5n>pCmBb?sAaUHhj_;m%4ecRpuxSyxTIUX!oa z=HeSIvr@B%Wmf9z(DrX$d$yL^o_$Z90X_Db9?S2W9B-MG`g*kIN__*`oIYDOv~@#U z*WV4F>AlzN=TaZ7oc%0*JvX8~d+M9e&dqpx3vF*QFMD&IYJ00QD7~4->ngbGRqJux zYWp{*m%W9yw~tkBZ_eCzuEo3c!mkd$I{fNbyS1+-w)Q)?Ch2R@o^3thx1K%g_nlhC z`@zlCul^9)Z$W(z+L^JwGDGvJouM;ft%DCI{SmZtkl%u?`#Ja&+WnM&8eR9K)D3Oj z(AKq2>I~S=UNgV4kMdiqU;UZHKZ|yrtf}-C+TP45w>M|0wl~+Jwl_WWGPo`2+tKc= z{0_8pP#f!d)y7(jd6b?*+p{_4x1yc7`kv~iuimc?zxvkdSL-(qy$|k8`YyCR$nQqG zx9Z66_uyLWN9|h7qjoKF&t-Vj;Zf^xUvB04c+#Ip`ja&eIZAKlRNI@gRok1K+`UH~ z9vL#lqW(PEv#tJN&BGp+^W&bXJwNu$eh$8n z^cT_2S^g#TlB$1%zM$$KqwS6JSn7thZfNT|f9edrob*@Fo-z4Xt6%+-#D9u*hODXd z7TVs-DYv&vtF|}SBDXg^^fLHb(qBirF8Mdm&OvQ#aAU2-JW9`@?b)1idv@k(*QkfS zdcQjS>hP;$FX(;nO=A6~)!#y2QFY{GW~G1gtL@)4s_kD+o!+Akk6MrGqW<99Nq-OR z9x1FW!>QRTE+`Wg~MLTEa zG58sN=cE2P+Ow_xQ_aI3mhiv1k?BI#eEud4i4XwSBuKchX{>c61vne$lc zhPG~K>pENN41SHiy6WFnkNmG_&$;?b$HZzuH#O>gFm2es9Jt2 znt7DooE80+*wFez+oyTRUGk_|Q(24skZM&*U5Y4}HbGddRKx z4<9}NZC!dSwd`MQE%TFONIQXnfbuZ#Qb%rCf zYgCVuzHiMVe{kXtL0@uj?$TRmdow3%ExkDtwY|9(wY}l7m*FJog{1G3^wLS+FX{WE znN`VSer8g%`RT{L%DSwjc3q)e7yDAUbI?!kyo&+y?%KLUMQ<&Q)!srs>K&Rg+64sA}(NU0mzx}mLWE$R#(mGq;lNB(#;=dgI5 zfObBtqx2KnelD-vemG;rV?Xw<-+rt`{h{2mp_c1seWi!c_FzuAeaflRd(`1k>v5jc zFWOkx@QH9|u6|PWsFy`M2YM-Mv^TYDv}gJnE|c_=(av1{6tpu}KNIcD)z3nklbMye zp{*NwS+upNGkhxAIjEmjJ?iDq&RqR$v~y-1rJvCD;~M4m<9yWiWB=;thx)^(!|g%+ z475F{_1lA5zj>D7vN!Xn?af+h``1JL-lGnWT911{U;UiP*Y}{d2Xo>bE{}GP)X%B0 z>Q|yKOJc4sP$aOoqmTeN&02z>nndb`i82*&zy$#tVeC1 zw^#iNv^iN{sdZ!3kza03W9g-=*I2c&`pG@C2lJ@)tC>yV?wxwY%E?`NxP!aeJbLJ> zc;w_O-1)GM;cM}D4%M$iUs?6y8q2)PzB_-l`|dt6`{C=8egoQlk-rh`IaIHY_8h7= zK-&-JsMHN@-O$#(i1*YPzA5RI(Vji|D%G#vF!7DhohXQrLH;EQI}qZ;a7*B+`Wg~MLTEaG2ED(p*KN$w&k1FTI^vt zKklj8^Rt9I`#IbUeL>ZmS3hU1|p+BNEV2ii5NcSD<#-b>xk z)(vglt9ehI;r2=ISUvLH(XL&+2io(-I!ZsG?dO`x?Z-Lju^;7CH_ zAm2IZUD3`#ZXVCD+C0|UhRdEq+p}|)yDm9>^&WM2)OxPtPQSxll72J#`pVyezM<;y zQ*&s~deruLd)046o0IjGS~pf5`Q_#`mR`zwja3_~pWH)xFppZln%NY7VbyP|oZO{{ zJGiUOqldnRdgSCQ-1)GM;hu2Mp?WX0=TQBg8q2)PzB_;Q9`N(HGyCD*Nxu_)XXWog zFRA*yXwRYk_o3~Fb5!bvwr=RCOP%4nD_6fC?b%a*0DTE}dM`bMwuieaw+GLj9(%B7 zwLQ2NY8LKUQb%vBsnm5o>gb2uJy*-`=1!fW^}~kyz@4Z1gJ|ceJ`(L3>7}g29@MVI z-l#v^H|hP*_9ovS?L5^-p`EAtXtX(*O{p8&x}mLm5$~xpJOJ$s)dwbhX!XmFN&Hx} zvt>=Cx6t-xPPx4~XSKb#7PYvCV@&OtxD_kMNw)!|p`zk)mK>thcP4v$)obEdC;&aAcXL2VD_v<7Fe?2(#1FR|(mqwU%LoQXZFor&+=1Lo0d`YL~` z$nlmvQhx;PIaJ?*HmA?l4Q<`f*1ekdKGS2bSyS=IKZ^Ezsy~KyU;LKrC$#-sQ@Q;( zH$C=K*IoMYZxwTKPRm}XuR_~{njF2xTjn4)kMmHwE^Arao%AzvaxU`CTbzUi~SwJ+rsvj5%+$XUsFhJ`Zn4yYK2dlKwclp7G(Q(VlTV zpFx|GGh6D0wr*(adN!#uyff*$lD<3XPbB@xr0+qS(>$L=dp6adLwg3@C;PXS+Wtd- z9PJsD+rR5o+rM-0_gC)P)pB!^uk;++oMx)@*XpmJFXYZz%M4w++8Lh5opletnDm#> zoU_8ejP?xZ`6}8op#B=#oa{-d8``>|qb_xZUx&MQ>TjSuo9GE^E_0*z(t|Tp+k@v+ zZ4Z}LZ4dS=w+Gik&C;9usFqs`Jz>9>az3oD%)y-KQddr$o}&x*j8n64`$SLteKuC@ zIS=jGGY@MWeiQA!sK152vg+@loe8~{IoPw>Ik*=39ez9M?ZUbwgX%nd`BxeX6Z%|I{hmnXBc_fprY6t0rHs z$=7S%4}XluS*d@5c2??NpzWWUrDtoY?b#lvGyG}NKTG=OXlJG8m*@+s{uSDs%%Ic_ zZQamOmpa2=!=07-H)v-xQ^qYjT+&vjh(`@p0hjK03|hj724>hPP-p7p5h(>&^jR_*$%bt9KL@~h2htnXc~ zv1()Wo7*1bYW-@x!Odq~{pyJIz1lh0@1wb{sQNM7Id6qOh5LnE59e}!owM5gb-$eP zW0QUy+WE^L&$XoLr*ika$)Cn$PV-qev~@#Um+!sQc|y`pMBAtJpTwnK{dBI2t9}Oe zOSsH!Z=vnYoN{}+v}${EEpmHvjrOuk(oarWziZT^-~86G&(QYiY~}VD+I8u%m+-5@ zuMWRjzw5PMwLKeWZ=vl?ZI0k-xpj?OmdiZqXL7l}>gRAdL)YXCUAx*Dp2y|-o|W`+ z=sPQaHh1?|&vUulU-k32%xOOBhPH0#sB4|&D_1|C%Y9V8fXi8#!yZE0!(ElzgKO7g z5B99K2iIav`T12xZ|1YE^HE1X<_@lw-_2zmwSMm}X#-&I7YA$E1ehruN zG@t#1wjb9hw;yM#wjcXfM?cnoIhXm>ui&x=wSIe0>o<>e?9DuCd$X3>{`FWtJnHbM z^*CqyiaG0z9@O?=PT#M@~}HO6;-d#{mQC0=YAoVb6tX!~)Ea{KYTsqM%9)zOdjH{~+F zdNVG2Q0un`wSMzh$KK4Nwl{03?O%`e!=nz5T90$Kub8vm=s|4{=JfqGT<(#2TP|bO zyK>pHJvtM6Ryz}Wv)}Ew?yP!y?yg0?BbR%melwSQq<#yRIn8I?(AEtdb*-}l*F{|F zos!-;>0Qvq>Uk@dd#8RIm-}TM`@E}a`!uI~M=s~7wolikwolhv3K7V150e^@rAfHZ z+L^dU*RfmDyQ5vBd=IWARlkq>1y#SFyE)Bg-O$zzZCz(;ojsG@3vHkBy}9(OKfvXF zsrTV>ZsxYP(Dr6dxxHOlwY|9(xxKkYdwD09Yf-<8E4cpP`ps`0`wVTL&Q@-pp`a8zj|M; zE2}<`yRpu|nL87;Gqf^cW-`w_W zEww${gLRJJy0hvdx!Z&MXf9`^K7s3ks!!x@PV-qev~@#AUF#ghbrF~Pn52(Q`Z%<) zdQReUw(66)F5$9{ecn~IeVS8lpO;o`pRP-8pRU)w)7WN^)5H3_fxp!>QlMgJN3C-&cxoGiEB|i6W8cEPD}c9 z^lg=&!F@^9dfhwy&cK}Jvut%I(KJ zQrnN0I{LBx*<9vVpTlJjYQ6f@-pykjd-I*z-mImze?8U@k2*YRJJM?*vwb)ddsaJ>=-WAkb~dgd)?tkL!(7IwKf+~B-+2#h-O$$c*%)hv zwqNVX?N`707OvnQMcaq(WTCxB4d>^j$KLE;ZEt$KoI&WT(Dv^0)m+h&vB8bChBbX3 z+MbP*+jFeJHR|ySzdHQt@T+4l?E4z7o4C~1a_Lh?PV?D+9sm)>C&{0Q8W)-#zsFI2*O+$62|b{@#SXy*|tD;PU*cKgs18RNup8PV-qev~@#U z*IL%Olgl})@8Z&<{uGzzMg3_m_se|t6WV@UquhSnf3^MCvpV{*{@q;WSAU$#9@P5n zL9O3B*140*JZgKhmfHUHSU)`K@Tm1TXZwmd>x~}N_Fzume}>CFQh%1qSoK%A?Aacj zi9M^GiM`qH=eW$L{yf+DRezDYdnErFmwTlCI+r=kXWh`&4IOo@^98PpxYS=t`pZdw z1#PUJZ*W~)^*6a+!et%%bVh3XG^g4=ou}G9U6AY zcar`t+Vds<9@mnpf5hFrlmD2@oaVD`XzPZyuC=W5{iJ_@)+7H3mwTlCDVMV{pZ$ck zAJ-_iANNRYKlZP-A8T1(?jEV-`pst#q3yw(a{H89Cp_x#sP#Bc>#L3R{$eBjVomw^RYz~;v+m_w>gdPZ z!PWA+xvZns@BJTK&QtwQF6W8vmg4Rjt?63qLG4=X&HDf1a)#=EbJ?5vzg*4}-F4iZ zr+UQQoaVD`XzNzp*R{@nxGv&S|L^}aDSO@x(Z=HG#@wB`dcxf~TgN_~v)Vq*skTpN zjxK$=F13BSUivEBb*ZC&^VysE)%I3(=`FOqIcGd&UG`vo{Z$u#X#MUHo_-G2u}}Ai zd*S5t-YwkAi@pzR>xQ}ty{aB0oot!-? zTCSh*%r$5xO08E9v&UKV``ch*t* z32i^uRBk`cN00s3zuJDT8tms!=u)79e4Weo{;pD(AQVK4Elzu!_S;Ld)A}2&)chhGTNN1uhhD+>c}rQr?K=> z)@!WVSpDSg?7=*0{c2`YxO=C5O6BA(J>0=vZ5}=JRXlR?74CdkNB2xTp`V5Jtjd?G zvCO;dyYpAO@18Mc-#r`c8B;G`{hX1K)4FPNhK`)%=$-@jEUBN1_MFg5$$4JY=9H78 z=W4kQNxu?(S@js}eDoV@U1nC+720)$c3q)eS7_H|U1MFBd#8?B(R&v& zQO8Ww#$Lf)9e(E8xkmG=T}x=!656#`mmYh+I{fPJtMxm7a+f}>tG2E^bMIb!Hob{+GI;tsC09u8}(3 z%4qvnuafi{)h}O^_+qrPVojyD(Dr6dxxIOo)b{3D)b^%_UblU_6FwUS;N z?X1jWerKXKzx^T~}z=<-W+BgMNDN{p#?m!>`uwKC-?(mid*nm`CkeLc5mG zt|hc<3GG_!jhbDoR~_qBhliP!e)O-FG9Jd%oc5{S0PT5GZ;kd0Ftf6!&QR^1 zx<|~f+c4>k(9TxAF?vbW+n_yf@@>)PL{6ZExn3+neW2ZEvncZEvoTUb@ZEu0_2?;`)Q@H$O8deTKGA&y3tYL%S|L z^wRs);a7)W9rLH(ZcAdFk9sS#b5QGXEzGI(>{`_JEGKu*)pGN&zCNegoc2k+ZU?ma z)jOg+59&Q?EHfxGcUEd=Ze8Zl?Sytd>YbB*GurbY-xKY5Q16AdANH`+4Q<`f)^+cw z)9sS9;2RHneBM*tet2sop#BccR@F)?NCymfHS9+rRUa+y8l0+rJ+A?cR~} zZb|Qsc2;_fbv|lit-BMK>k93V47f7c(pS zDdqMc~kF?HYfXE>V~#%XzN;wI^Fxx?w9%l)uTQD z?Yh(lqMZ-xDE)-CAJ-_ipG&K@AN!X_Kh*E`f!l-ngJ^qD>$eBBe)F*I(wlkI_GT@$ z{p+EA?@@4nn&}>Vs>n`Utc=(_@*5J*%CGz0q%XNYaO* z@2vbVw0orINVI#TJ_>D4W?brqwr=RCOP%iU%GF1s-52#SX!qhEE_(=V4|i2=56()D zJ=n9_9$X7G3qQZ==#6}(uJchxKjiMYT7EZo>J+UX)*TCXp6cV!?uD9plr_>zS&KcW zU5mX@zdJtZ6VUc1KN0Ob)fb?-ie_ddC$lMac{X>c8``>l*VO4wO8R89^OT=b^QiIm zocCq6tf}&OvQ#aAU2-JW9`@?b)1i zdv@k(*QkfSdcQjS>hP;$FX+8HgV>w6tItHcck0N=%u4^}SKGg9RNKFtI=x379(X6l3HK#i2(n}Y9b@<8M zd&ph1b7meLYc2Cpv*$~Bw$-=PTAoYKk9(^2{5UIX!+K4=^6#DG%)6Xz^+(ahsXvDH z?tWW0v~@#U_iEmIj_IY>u;P*5iuUex8`}NyvY*iQb4}&;<9YU-{n)?Sey-*2yQ|Rd zshYS_SAAX5*Q1?-+&tkokF|Vf&!O$vIm=y_+~@G9!=sLwdtT{lDQY{*?gliqi@q^w z&eu}BE8JK;?w@^z)^8rxS86$Db>vsOM!9njuI3vmvFZ=ioazswjdMNLVn2%3A6$<) zop1DDAF&p-{d@QN2-=>#!mpO|Y+UJ~<|sX=eP?fK>X)1{b;R<%=sM%>cKptjnRR!d zotgS8XnSUF%NcXu)SfZV4Ex;One<&~*DJpp?RTU8D%vxz{u>^z~JLF6l3zJqvn_)x*p> z`wXq$Jgl$Oa?a|=uXc@cdg;0P^EFoe#hO$7CA53ZoJuYCQ>{O^9&=Ksiyo-cuSISD z_NM+a+MemX_p9ZeVP;Tz(62VXn%q67e#xm{9kKLYw0psvx^EKenOA=cePz|(L)$ZZ zTh7>JReQ$HL$lA_x0C)3+V4sJUG$QwzmLA4>K~xZ$(b#6Lt8g=)TK`ML-@tq)jvY> z+o2x$-Fdd@z4Q>;9{9Ekw+GL;9(%B7wLQ2NY8HNe)pBdWM||7mtg^n&e$0t3b)6e^ zdX6sq78o@Pw@>tlxDu=8o9{Jeq~s@e_hYp4SN{a<{MElfI}>^@bFgQ%b8s#6+x;}@ zpP`vc;Xg+^e?7lNJAd`>(B@>WrEX~JhPJM?sMGxd?R?a~tRD66(auW!2ek8N9i^Yp z_T#MN_Tyft?dQCzqaW&bzk=I?`qyZCQ0un`wSM!k?$Voi)b?gAwf*a%e(zC-N3F*> z(^o%d*4p==wg+?K?f!^%kJNvvvFd;PKVr_p-K%N~*Rc78-QC^YEw*BJcVKsS2X?oj z(g-Mmf?}Y6SO>)J?(Ww6zGKa~?)(exJs@1xHO4o_T6^O;oO573<1u!kXW5E8Nj`6=?-;w+e?0aF3 z@RM0T&S+de?oHN@{$>5xi~bS!UC74GXTI=|Sr67Uu219isdMC66^AB zsw4lGa^OZM*gfDeX4ISPjP4U(jn1(9lsi&B`-qx8D_b*hU6UG(F2voN%t83aOVo5H zvNfHH8g*PYj(_VlnlRr?a0_O};MrjM`dswFf3Ur%Z;eK~;K|@#DLy&OjFB@t%#6Wv zz}Ccj^v!JF%=YcaIei*a6g(x&yb+%&<(o5S5uXd@o3VCy%d9tR8rK^;3D%pl7}uLQ zcxg;s@HDV<8J`w*53)eT3)CdbXx4r*@q~?O^xJJ{ogVb8zB$;30_@NxgW7@%{Kt zWxpSH#rtf`3y(=WAM8$yF97>(%ZtK&5-$epneQ?BX0~tU+?PI$`HA=ADlb^@LIp1j z_fI*C7rZ>|dt|=wIX1CAt!Z4J0~71hxs2=6dGQtgolDOCnLE5$U)Eb@y=B&$dp5_p z^g#c}&uo6?h2e22C+~qBgiqfQexr^xId3dMzHxcUf|r4F&I$#u2>X7`UkP?b=5DyR z%&`f(OH`(aOw+*h`*v&-iDu4VJ>#SX%M=Ir0Nz1*|xKFz^boiFEn zIp@pf`|j{wKg*uNn{{Np$<(OhvT>iKUOYRfx;9oPo_P&;NQ$osyIbaq-8!%AZhd#m z+E@$r-N|dGeBMLU^jX=OnR88QG}a;R`;ym%2Xe(r)O7!{HH}lFj?2dNOua@|*tz8O zVExM*!}i5v^wPgMl_+0D^|o@G5a3q2$5wQ*qZDn`ewFoX8ZQz zoIZ_h3f>lWpT@UK`SQL+d_Q;qF=h&Hne}E(<9c(?vfi9U)|)waX>4Ec4zP0>-w}2X zvd`wY&)SQ9gy+n9wx)4CyK~tY&B0flFXwzY=gawB;JvXE&yL_K?+g!4oNKbP@Na!t z|IR4u-#C5h969I6<~SGq8@m*|JM24RH{oGqVm%nAMje-PoO+F2Q;xh_!Fv?EXJY1y zzSfj;U%WJOzMS)^UFT3c*gdn4#{SfFKk@_lUcAHjetf60-;cZEeKrm(_#k+6 zig$y@}n4-tgNvuHfTgXEA;P>^qXrhWjKw2eu|V zj=q`go7ukp%+jZEBJ8`6Pb&DdlyCgpB7Pq1J7T8rmRWDsG_E&yDC^BxWWAY#m&VBj zpHlFt1)pB<83msSyDRHh-<`&VWM*;z6>OJ--u>@0etXCu!m=XvFv!_LBw`ShuoKDAFgHqNJ}KIIEwzg77X z*l&QH#XEI}vhUP)#Qqu=7JL!>aEf0H`@Nad1NM89FNLkiHxhj_+c$IWOP|JN#Qomn z%i%{VZsz^qJv_J{Sr4NU>%s5JxE}N@>%m#*8S!3;joXWO)A!3#|I877GV5nhitA@! zV*Tjf{OpJRjq8c)LB0XjgKWMYWb>`V+~LhSvfk_^>)#yu*Ew>|k%p4jHg1D`NAm6IS(#bGGah3ndY0XZ-tgPF1J=KMXTf*Fz9ZxKjdvtdBWkka=<7~o z`)0OpKVtN0^ep(Ug6Uu9(7$GS#=G-bzKfcfwdUcl?~%E~zrAGrXV$-aHm-l?mGy59 zejE6$nOMzyJMn(a@mcpH`>cK0S;nW*=xfYtuH%EW@pLlEcV4?oiFEnIp@pf`?Evs@M&M!zIwJUvG61Foz^_G$a%2f zhhV>F^GCpbhrS!%X=dN4@5uZ63ckPK2MQhr`+b@}683k9{4ngidNn_@`I+^WS#N%~ z=05^EyF3a`vxc9{`te!!;=Gyl?@Y$^<1BLa?Zrr}UN7=r5lkKa2`b6CKB^!4Sk0bla)C*4a>Nw|(r^#`L@_5)?$uGeAr)PM! zm#k+!(5LZC!Oz0R)N`=AlK%(0D|rHJO?D7{Gut^C>P*gx#C`BJ8f7MtaDs zhg3T}xHoh3pl4YR&O*xEf^%_r8FMBIJKFTw6pehqd; zyu>Vekex+u^l!Xe@GAws3cF8pUWfZ6egn29yNSM;?VCCGrBCBc;_gs>3wCe#jlP-f zo7ukZ&>Z{fQ?{@E=@W5xC>wVV%n|d-)T^0#HRG%CHu>&Keg}3}@|Upw=^38wCF@xa z^l7|X@O!WxjK2@NEBPzfUCCd=)?^3KH?w^++t=Tp^l5wmyDRy_f@4Q8m+V}b zoy+%S+&!3&_c~wB`Et&e&G$VrU;Qlmi&?BAJ4JDY!sqcvWHGU}gN7&sO{|WYclmCVN z-sJyaYx0dm-^}*SY+pUor|~oF_a^^R@E<9kc(c?0jW;{P?uwbhTV}mk)41LSCf1v? z7}uLQcxn7v@NWhGUhtm<|5fnc@V%*y_1%dzt*;+;7IWE4cCO6M<@aXXJ(!R8I$zHD za?Y2{AIz2c>Sx(s%wiqcSu#6IW@pLlESa4}Z}e>BdF4E>oO9S&_%WY8HPff|iN|IK zHT5ZXf&Jd($zZ<$b{6l{9m>8_-x2$3HVP&w{%kegg8km)$zi`Yc?y`C){DNG?VCCE zwNJC1;_{R*-(JM0g4u~R!b4^~j7@Pp@LffY9`GEj2WD~B@W%HLoV_tq^yR%&a~xOt1e;H+IW=+i89WWlK7(h3oe?iFiymZW(Hs4n(-u4(JSN4bhkGZU31**> zKQnAib`yOw+c$IWOP}TpDK5_fyH9ym*d5|MJY?2``!ucx_bKZ^&$1rwP>b=J>#pn2&_+e(SnzN-MR4T!&1~O(oYSYdSiy@Iykx;k6}&X; zyYSg%U~9@N7x7hK-wShxe|yP`63?uE_i0@J&MWKR9Q-zyEqJ+tmxtYzIX>GfvCrC< zoyA<4oh!3*Wp=L2&ShVpbuQnXoOgxyX5NXMcOv_&?^n+Gi@{5z`m(dwOLmsb&SGCY z*7se*Y>reA<^j(O1v(Z>~y?ad|b^Z&L0G_vOmY;(fV8+4trC*$@`DKneCf7_oYvBP2$cguU+su1+SZ)HD?3ZdF2gZ_r`qT zb8KRLTGO~b{jOwvI+v_Z=fzjVolDOCnLE5$U)Eb@y=B&$dp5_p^g#c}&uo5X^T(x} zya#p=K7B{{jXKukytxti#^sG+-<{kI?#q>#VkgcbyAx+*j^-u>Z(8tXaPO3}IqbVL ze+$@}>^S;nwr}R#mp;uciTf_(tzdV8-{_m!zM1XoyD-PT`jqXffBHn+cOe^h56lts z%G9fwdNt##xi$IjO5O%`SMrXq{^=Q>?Ir735AVekex+u^l$C~>qp+R;C*5DY5XAA zeahWoYqFc@o7ujZ?d#7qeVTg}ym!I-6ue)-`xkrwY)$JN4ErwRLtyvM+~MC|vi>vc z-*;hL|IRCShus5yBkt_7aci=V@SIuCnf07m&zbd{S*PJ)tC?q%^Nh0jzB@VR zv$LlDorV52(?8gJ&LiJmJl8xl)sYW_{RZUYV0Xw4Vu#KyyF+)v+|9!aJ^~({;zz>0 z6CV%z4VZrdY)#%t^v!JF%=WbxeVRwX?pZ!M<;W+({ZsrT*c~!Q_{ppvXEd%K->Ix0 z{ma=8{hP-S*Mod4tOwbAJ;>${?P~p9QxNea?X*>anJaw_sp!d2U!o+ zB)54o?7NdsNzck>z_pG9JJB0{^qXrg00Dpqi<&W zX3l--(>$BF??OHYcK`T|zM1ViD#h*Ve$272-emjgpFRl!CURUt-uzii+0NV9N8RuF<;$F&b?%N znZt8+j+}F3bKEmKXxp z!G7EFaM+qXk$p4UH?w`+KYg0SY9@klNubv)-&}TyK8cvfi9U z)|)xr^IXm3MqlIXAiizcXZ@~ZpS73wdd{q8YZ}+HJC~i&9Dn|EzMS*roG<5{xJPP4 zO_}!=9^@gg?@`Y6-Kn$KOLi9h$j)Ni`sxj%<#(?4UUUHYSgR=@C3G)!`k+_v5!M`~9T% z+2qgv*e&n2`AET!!M+pYPs8q89uMn{?=kvjwr^(px>x!%M-}{N!H*aGM8TtB--FLS z30qTsriecayJzMO|MrsgpIQIz+xXM4^UC@+2fxjy3LaDNSlC^eszWb+k__VKVUp;eeJ_j3@pNIY4=gU1;14A%LTszThlsk!0thQ6CS{oxx>G` zWc_E>zu%j2{X4I$fA@gjh&#J%+?wnoJZIK(W<6)tb7nnf*0b-$I(ZNHYUUZ`Jfm#> zV6Jk`XJ<|QI}815rhl;coJYRBc&_%^PNHvS`)1C4>C=2a#pRD--=q8q?B4Jm9y05J?>^#s@I9KN z2R+Mra29$--1jJFZ_E^Z-H)98P`i%H#{F5LPq6vKnx7JP&+=!m`;@^_+z z{AAXTGaA>AyOs5$e_22FqJPBQt!&(U<_iy*^pfXA3oZ?ZGGPkc3hf%_!>HRZF9sOht^H8ba$)M)-jygyg@cUb>;iJI<2 zwx)4v)N$Fk{;Aje10J0CPuLlWw>n_^;xT&ZU$&R^snPta;J;zN8RP%J&Iq@2|K&og8fdIBm896k24zAk9&Z_kN)NChyIkQ zJjk74J;?gkgKWNam^-{#N7kFY;P7t_{p%b#=fIKUp7B-hnOSQOvL2}Ed8=LUWH8T0 zd~%q*=`nVK=U{ij{`K3M0`8M|%9PLkqNdNv*36u1Qlm8$arW$vTT{c#<#SP!*@LZV zoEmjpHm-l_wWfjj#)7AX^>2I**uHp-Uiz2qWqoS2rYm@Qn0FEJ8DM8LXHM7|<+)&M z;ywCiwr^(p_T!vBtr-iR34ScaXHNO%%niE-c^;T=%-Z2Cv)-&}TyFyt>&;n=>&-fN zY0UyVuWXKU$>vy-xx-s#y*-}ddK;A3Sb@Z_1bFgNLN}Vz4`BzSyDj%I?seFl%f6f){}K9TM>cVc)Mgi^INO zc?sB>yp!mg*}j?WYcKk=7J}Wayl~2qmxO&6@=~xnWRCEYSwGHbTtB{3SwH%hvmg4m z79p+&c~MvoviW+D&9@G7hd1lUdb5|Te{<+x=g2unHpe~VE4VLLX01KQdax$Bt)*e# zsk}^jR$c-2SvXtQ}orRY+wD;C*tm0HtrsnBj%NIx4_ya7Bs@rDI&3_qCSK5Gs;Yw0ty`PN~+ z=w*F5*O#5qI9}?wyis~q-XztOH-&xI>?wLZk=XnkH^-XvX=M-eY2{gD{p(HM4AwK= z>wMYxLtJ?-JeV(AU#4~)r+?HmU(V0sJ=k}_o?2V;tlzx64eU2B?*{9ccN^cB?^^a7 zyN4_9xwS3aEAe&(?*#Ww@!es+dGq&xt;shVeKXrPvwizr@Gb@K z3R}}Ud&2&V$a}#9xH5P6x0kH{%=#ag;`(=9bM)^X@Y^!(?6PrdvXAhbSaXAXO#1dviXC#$~m8%we;^S^skxz!RB)w`S#+u*50X(ybtU*ARhp` zLuQQ~I=k!+-3fEI_APk7g7=602Fy7S_8X87g00CriN2Zbn>qKTPpdm|-<^Cg?5^+| zeKXrPvweMc=Ga%CvVHYWpNRYJWaI9EIbvR!dNot8W_+~{A>SR!hr;ekJ{{ISJ;SrT zWIgMFKCQzFJ{-O;#gBk{Cq4u2llV;7n(QF@X0~r;`}+HtKCL4QK04(XKMQt;^4YMv zVvg{WSwBNlTtDv69R28D){lQi(7$yQ><;B)U_Hpk6?{DG9*kQj=Uc~K>?1s9*0cLJ z?p((4Rp-b#N6tIPZ|hj{hjEopfQKhOvEY;82UFZ<&0%LPeP%Y_I?NZn+_RkP%g$&V zFLhi#DLpHnl4{DQ!uN4yPtog%#OCL?Io704D|?_%J&UY=y~(G+dd7R5FB^Y|>j_8? z=F8TXsa?nEA2rRF^Rqk`?7Lu3t#f$RZ(cqZ9+LPvSkJuM_{MzKvfr5B4DY#hUcu+X zegnoYfc@s>>tVln`3BgUe6!Ivvwbt$w;$*9X%JSj^HX^4*TxpT$7!Jf9uOV$Z zF?JIkMkdySacb0YImfBjx-#X+R~39s!Ph2czUXUBIrqg&E9c8OpW1Z}wS(O=`)J)v zP4^?;0{d;tcc)&w!}xxDr?TIVyW)MeZY}sWcyx;24);z>ZhYJ3v$NO(-(&REzii*k z_O%y%T6e(iS-vyn$kdMST{@$85Gt)9g7Am0V+K{j6x zvia6w?(k+ES#S1|^=}US>l``f$mY0beARnq*4l%t2Wyhs!dJW_nV*$-R(=@Pvp(F3 zo@IC9Rp0zRsF~fwEXMH|??`?G=9$PJ1zXeiXWz{B&1_%p?bD)X&G?P?Vw`U(^5sX1 z_+zl|$Xa^KtT$^K*PA<(_2w+H-po<#uV(%XiMfpP=Wy&n_F3nXeb!#|49}VM?74A0 zyK~tY%~8&nIp=$y^X2?5^gWdP5nSa5V1K^kT+{cY|5UrBe`hqVf8+YdIdaaC&2cX4 z4lDQ}*gg4e>0xAIJs77(9hY;QdadCpM}Dwi=7?vFQ!kj>(bt-C?u(aJ&X;pOwd)*e z2RjG*Xgy9%=a8R(^)8P|y?BT5{kR+1@5f#7K3n|0srgCR=ZrrE`~AveVZT9n9IR)) z$LO2czL|4h`m~-V?zbS1hkY;jjlP-fYi;B9^*zh>)u(J<{nIDnzB}2td+_(BeP!we z>ydhOob%Q*&@NDdNWS#I!Deq zvN`r*zPgv3d&%}Phv(`XIp@gc4C2ZTTJIJ7K0GAFKS*_Whw=T~%T@OK@w?@HwmyXY zZsm_sKHpl@)Proz%(*5tS|1bln~^_({oe2rHQkA9P2<$4^qe|gY_?e z2iq5q(M$iby{u1-*5~k;#9zREC&s^mol*WCc1HOJ*qV5czM1WtIrpVc>r3K(OY+wR ze^c(hDh6>;a1vw!9eZ`POfmRWC^ z_2!<uwy(YD)B2;}KU0qJe_`K|{2%PDm?QjT z){iq9*N^W=){p*W{n(5C5%(R*#?5EG@Q_&#)-)|m$O9m&(eyche2e|iP$KQjUM8FBqHZ?OK^gL{g(m_69IHQ7ga z&aCImdd{rp%zDl|1Fgu7HI=k!+-3fEIXDE0^*qs=k3FhZ6cy5?)Ab1|wn!JX1!U{xZVaP)|;~!*PAoqr9B($Eb{C{ z-25Cj-}>wzd}h|CyEU%Q%+6&FUg~@~=gT=?&ilu2dk&s;Kk}Tgdyvg>7WNdLokiBO zacb9b*|>F>udXRuQ=imp&ktK)UH~4Fc#-rhJBXdTE7_gfmwmJsEO?=U7l!>F%vlun zdyp4{^}{=izM1WtIrpVcdvW5vYk3LSed0IzX0~r;`??2n?5j`NzWS$6#C@l-areL+ zZTrg9tC@N=bKYK(9Csxz1-mPG9a#VL4A1tG^{fZ_w3jY;8Th^wUlw*(=Bx|%N!%5- zCOe3}neCg|zV~xZpZ0PEuaI(#uLrwBd3|^QSLO&mne{U?#r5M3&C!qkW&J$BmHzGJ zVRtC62oc?Y)?vQr<(}nSUv@_0c&X#^>gic|%~Vrf3-(>Jr|9)WV)Jv{ z9Bb02ojuT}o<-Kb-sH96;au@v=gY<);>r%fgZZ-cWop-P`bSOk<@_w(gMAn5sl5Tu z4oU^SrUiE^c=Lj{fc;tU*)3sf$~zSC9btbim^=L2OV)p8{rfXwT>s81>)#yw zwzn#H>w>p|-IX~$>waXPwJ$r1xiULf=57T$SB^WEeSOxse0Or*72exEYmS_EBKxfG zSI+tDwe5`7mz^cEvt)J_YvQramvg?H^JVkhKefZBeP#RVnQMC|*toni?Dr-g0{gz$ zS-dZIDEq$LKl^L%Qt+;@yEVQW+&l51aG%77!PeycN8il$&1~O(oYSYhd%=6a{Zo9; zlyA=AMf?cZT`^O5%d9tR8rPfOlB_ppk@aQ{UfO#Vyf^Gz#`l5UgY2_8?z8q{AK^K( zo~>zI&+c4yMsx60=gT=?&iQhF7kF>)%d;c6%KO2-J2}^6XW`%avi_Y>*1vK3)H!m_ zko)bH1GO zsa@w#JJ>z5kM@z&%zPB=w{86B)ay{#@5gs4`~CRc@;=+gz<#&#u_>S2sHr#EnwfJ= zYP63d-k+;{JUoyqUZSS&Lbj%HYSeMrxSpxkJ^}Xq$|u75mrsT5i^u4tf7xEvr$+mv zf=`AYPVrMP)?K6lwqkJapcZJ{Ro7ujjQry1IYmR;O zDce{7^oh9No^0GbFh|TQQ!n^YuGFjJ_-da;zPpmohTWBX4Xl59hG%=pde#Gd+UFE} zF02RR=fUnuz7}>@@^!E^*+KNpY~RfG^=F?x?eh!10CsQ2FHHIJ^+o&!*c~!cc+0Fe zYZ}*^`;_(OEVACr!Atw1f-i=h%lIX*dysuL$9>jb>?1s9*0VK@>)Cf9JEJ-Hs`KTX zFXwzYzl-zW9z5&sG5J#1cPHnX>@56SU)KLUiS=)sK6Q?qb7XUzi~jA)3ceEd9kHA6 z;O{Y655}oc$K@QSUiwGZ|s5ZG5YFXwr^(p_9I50 z_HA(g#J3lG7wkL2dw9sK2cI>r2ftxi51I8~4n5m<6ntmFJqy0OVCJr$W!Cu4tz&)n zBU@iT>?Zu%OV)p8{kwnT&TBs2>wG!q%Q;^*-}l9Q^|S0LX0eX!ESa4pv$JG&mdwtg zH+r`7ymFpb&N=KX{FqOln&~6|zy8;(UGe^O>e?9|j}N~+-$Q2K&oh<3m?N|EWp-}a zS$h`jE_2-PEA#GZ_UGm~Vt(VkPxEE+B1iV;@_DZQ zJaO-g!Tv6P0rqFq->Lsg?C)B0_+1b`_nG~DAp84(pZ%KoozO9X7{7z!_d#azB7RAA z7Wr}y;uE=Eh`0}*^2G&T1Ut)lRbJ%C7o;5d{FHNcl@~ekxhY3J2X;UH*~OeuQ|2u4 z<+EV-b2{Qae9C9Q?&s9%EcPRxM*KytC!$ke--~>5!6(7?Js$B)^p(kreE9^}zQ-c& zqpy4%Y~Q1+v*;`H&rVVEi0Ulr$VU;^gM1`x&BLnus3{+wY93OZMNRoo;=a>^(7^?F zPrN_z0}DO?_WkZxJySDz!TVHaQB&TRxbJ#T#C`ab_bzxZ*mu2Kl@~ek9w|rOJ>~3N z_^@%J-a2mO~G3iyj9}O z$l0Rc%?s`ZJLATPTH#0DwBSur&W6OP75OrGkuPrqJLCEYe~}||7CG_;urqc=+{cXa zdayIDQ=P?(^18$)aa{|o4Zp;7jp{z)@|r2WS`n95Pw`b?@*-beHN{tk$?JHT>xzi` z@FTATzru9|#4{bQCN}3acsWE~)RdQpU+20k;+c*&5_8t^CcIR2AA69OA^uE?FAcl5 zB@i>m-emG3M_v+kZ;K)B!>7DB?A{is&SG!!qUqTM;DrlbsNe-*`_6}WCi==e6Mg0R zVf)U5xR1W_ys&-es?MUXJa@{O9iFq`Ia2GyuRKe^GsDjko4(46 z9C^l+BhLU^XIg}h$d@^be0e(9S*AwZ$1L(Ru(M2AorO1fs`P9do}%E%3!W@-7jjw! zHw$jS`tOXWRWr|oH{%^J{^Rce>1|FhXXXTVg7NR)ReG=g;D2HK`*)p%E!cs%?& z`m^9aV0-;ioke~5H{$k^e@!_*!PE^uGG~z^{|xKr2gH5&k$;5s^Idfoe&p}dvtPmA z7W_@YU&C*4{ssCHb`PH+p6PfyF=yey_~)>*e1hl^`7&pbBYz6(^CQH4_>@0}_4z?{ z7Cz+MZKW?-O@F@_Vo~^~8PDl;44^$&o|JOr7gLTtxQ`rpLc#coI@aPWa%5(znH|I) z{Dznz@@4WON5)s|VI0hT%px;i>|qSdee6~q3*)c#1bPZ~F8RrVN5k&_F~l>`S0*p= z<;P+7KMHXledR}CXMMOji@x$BDd!=0WWgg648OEc0YGlXR#moo|JPZd{@CeQ~vG5Ig5Px4pO$=lNWqxbryZ)%ZU3P zFGk#lPq|0Im%zTq3#z=xkuOR)@`Wkq+$t|}-r$&qmA^HD%5sUp@zRKW8HD z!>4=}?0!zK&SF3E8N~Tr&^i^J2K)Qt;3|GX4z&TH_Ih`gvN9|gb8^$2t{{6=EV{O6_k-$HQ~dpMN) z!wWtPc5eqGW{$ndxjXFM4yevzZ}NfZ*?r*s3*N8bePR3Vg?J|V z$~+T&<-KA1?t!?EzVe>1eRr$QqOZJrdUhvx*MfH`c<031k+WmLI~2S<>^Hd$qE^j3 z6W)w(3+sO?MDNIvIg1>5YgqqVAnwDzyd|vvZq-@%mp3Qw_qHk841S60#?^hq9KW}<5P4BkUK{p%TN9BN z-vN9kEc(iGre|k?XD@iR zf@g(aW&CaOZ+M zRr&sZ=Xhp*@V_t~ntxXJ_6`1%S&hONuhqE_f^CCz71IB0b*Xk@h$iES1_U6yk zS@e~EA#UFvsB)XOSa+3OnORi2Lv(e+)b02h~~lkv}B<7T5RC`|#Ub757n7ez)Lv zV12%ccs@MH+rtU-@Og!@-*ctm*XAzeNrW|=d z%IQ<(MULDr<;Zqkt6pmxEJgm?n2BEeP!|@U%nf54|gK&V-Ipq*gf1{oy8vH zJ5tWg@NEU(TJSBfd$<9S7ro@03ce9`57$$b2-d? z50@bBV-Io<*gafSoy8vHi_^2`!50>MLBZ$4&vAYZ z;+g0x^Gx)W&xP%K7UDko%4fs&J)=5{zVeyG`T1|2f=-8@Nqkztr^5C<3Gqzym3b!e z<&$Cio`ATIzVeB%eUGcoqOW{>$~hW7w%}t@{*lBvi+uSgSPzFG?xU}KM8SuroI{Ay zBYeu_MZSC}tk3R<`|v3r4D0j2>MZ)o2c>8Cg%2or|AO~RyeB#P6ufuAd%=F6yCZ7V z%roK5_#Ut`?uzIgIWlLFBku<5e`myf_?LHq^}l0v7XIa(h`+^kd$a@mHrH*c`=}{z zNBo%--xk)xR*2`rgG^rJ$XmmD*aC4M9^@@yJ#?$i!h^hddUj)Yvw}A*coW#Z8zP>G zzB125UwI?gzUw3Iqp!RHY~QZcS@f0HOV6$auUqgs1+NXepEb~$uT6-1B7mpO|Zc~w}SDwi&1t?(l+UhrZmXJO*hihP;8$d?y^opC{gzsQj}iyV0&*cs z@_fX{bDay#Q}Eody=Je@qP{#Qalc7tPDSeRzWa3Q*IL1gWQ1i&>8W3c#t`Z9JvF=L*u_HPY=PJh~uI0Pj#<{;D4)U z8-H;Bcfo%Z{Ac1{$oakC-wOT}=G$xhgs4?B&*&}UKd1Qj)qTX}A5)I}1I&z#ZxLRj zj?7u)%iqDy_%-4_W|Y5yo$<@+EM}CyO3!`@e^Kz~1%How}goJGFOw-7VFQRT&q@|(n+@ioMK%qYK}o_z^^ zwcu9@ei^p!i->2Uugo*iSDpmB+X;yK=qpcz?W?$tzViQI{aTmj{DvFjtNW-av$LA{ z7Q(|g;yfQ7Wbz_M=9>)-{k_8uojWpD6foSdXI+&qQCD zyvUayg`NFj#C`OYAA#*VqB@Jd^2qe;aQLBuAB6o*h9b_QzC5hp2U5+`r&{1@}#SH#xlv?p5$TuruC? zs1<(Xy9(}^a&9M1t;mm+Qs+u09?18@`~rkDBsD#QmP-3t@ephj>0b$ecxvd_Js)a}f98K|UAO z!&%i?c#zK~ZvE5HnFXIwu-?o$37uN-DFvSl`#qe1s8uu1#IwdvOz~r@yok%kryTh> z*cp#T_=`F+XOS--13Tl9i2ImPJ_>fm!>hBHQ9dH&90DI!@S!QcJ8{k;Up^St!vTo< z=qn#o@PR33KjQQVpE7xoFYgcQb05Tg_>}jB^|@Dd7JcQtQ_k-2o(1obco*Wk6}&4v zf!NN7XKE%dc*p82`pP>IpU8DP#C`abcPMy!*!Q?~l@~ekwkb#6Cgp5VcfVkhP zygsalbrH{p2br_Tk-NfrSQ~L49^`dkJ*-)sg$H>p;?`WPI*U5;8pQ1@uMS&tmFhlf z%B!ZDD^+JvQ(ieeyF9#N!7ISe5?cmw7WL)j3SKtlELr76j=Xfrk(Yw?yf~s?)RZ}k ze0d32&x;{@W#%mQut;?lp5;Z;vkSlr7raox3&Osy`Oy5Zv&@5dCf=RQS$L4=h4ncX zqDSP*oJEd2H>|fg5cjcLc~02(HCuHSKIPd{&dl(v1<#Ur2I4anJR>}T*z|~JY9=pu z+UhL&%F_||XKE_MefX58DR^qw*(a~^B1fJw<;YW{oK}?=Ir3yFM{dLJr-7I)YRa5N zzTAY}PY2>Ye9B#5_tU95i~Y!*iBq%7Kh>FaF#ndyvNxx3)Y6c5kB* zJ!5Y&XOSa63G4T9#C`abpMag|(dsPrCO<~py*+|P!R~Ejbsus0;S_(Uh|42Vd^k*A z`mq@_BOCOi#^DL zh`TrWUf8|$N6Z|1lR1kVc>wI*`XcVbr`!*AZ@sIt*qhuZ<=hSTD)^qncM`v=;GQtw zP?tLp&(us_@NLyu^p$TX&Ue}6X2gB?ly5Eg7T9m_hAJ;|*3t$EIi2PrJS?ia|%8?<)1;Ev&fgvg!OPL z;y(Jyrx$!$$~l=hJ;J9MXp;+oor?fVVDqtAe*oyeT=G7u>Dj&0zg+ zjHp#J&xAMQo22*#RbIs9jZ%)hA?&W!L->n2GG~!5uMa!px`_LjQSJ&md1=|_ZyQJgRQx6bssh5MN-WLtFx#n zFGSqmd-J0O;P<%BSKY^-t@Bj(@#mU6FF7x9ovVn;bEo(mFnN(L&za)0!Q@3;o;}59 zg~^MWGG`s{bDarsA9KmG6g)HR9%ewy5WAJhi+p)T*gZ^#xQ{)^)5Gp|n(8d}AWutt zJl84F)CErk+iSAwEb7Zs5O?SD-Zb}gZOSc{zN?EpI^bA;WPLzuFTT$d(|)U;W#r#{sZB&!We1oi|XW2Y+A0yAKDSv{j`C+=Zru-2akNA1$_@H3E&FJNvo>^ah zx8QeR{k(~+<&3g<=F9F(Kd+~I{m5@1{k)p)^&`KAe0CCgrQnwf=I1i%PUQL`vaiqk zjJ;*gtRqiA`qYO#%$GegN7jqie5Oy?=R9*qxsU8~=8R3(%;UH>|EY9l4|xo-))VR8 z=jA6+=FzY@kD=U8HqRXSaiq^t>E6EbqsYDwr+fR#k05g%LL&iKI6}Y?3s1s z%aJ~NAbXfEduEP&DS94VobL50UxM^`VY=6+d=c{5bI}C_pI`8Kuzk-)K4V|mXM9#Z z2if<`bZ=kzEM(u))4hG=Gmtr_pwkLI6*m7Ql}sjVZLmh`SQ`oS&m5e`jn4EdOIxL>rFl!`D}M|Xu*dRd@$_0J`f#*?7Khm z8Q-<+nRVm?kUsZC_Ap=e%p7??q|d$6y*}lAkUsZJ_xhCgLgws-_9%FF*!*3PXXeYh zB0cPw?(Hk@j56;8o3lN#hdyQV%#n9M`rJ0%>p|WQ*>~%7Z(n&EGzo2qwn8tV&C|Vc zc?)EGGi07|xf?RR2{O;PyeTri5i-xXyfHGqAu=zsXYZr+kY~;!Z&2|1u(Pj=^zH1j zdFIPqk+ZLz?wws;2RZwi>E7ApwU9Zhp*0F#9X5Xz1Q2EB!rL`xyR;lEP{Mq53+gY z$crL9ER^o`ATNycut2)kgS;RbkLE@57d#(quep$C)|clgcy3q^vm?*Uk>@OU4%j-g zBKw&yn`gc}8`AU4>0ZzBEJ)8Yrh7fhGaCp`6B{XfiH!e?yj8BcsGcHeqj8BQo zGcHesj8BftGcHeoj8BHl%k0_vsEIst7P(z;3wHJ{Xh!7hvU%pq4dm>d)4j9H9f;XG z|CjEWJ-8G1xU|CVy(UsKMHRbJ%C zKc^h|r`1(@R#Y? zPq_cQ;Li&F6t?e2)icpo=9%a#e+=9A1H^swl|O{-`(AYxeP!RdJLKoK^ScGJ?>hcA zY~ME!&qQC@pLL&=--PY^T6G_N<=2VZ_m%1_`pT~or(frnJ9TP31-}R@?xUvcw`qNO z60C;_i08wD%vt2f6Jb5rkNfZ-v*YmK^W29AncR5R@6T`9UNYC3{as=lkDbRAJhtF5 zu;0LF#Pc;fm(Lo161L~#h->7?oJEfO1g!r@5%=L=ehhYwN2;^%FOMS5UOJCN55w%M z^F!5r#N`nwKD>y_52pA7FnN(L4@>c(FnRv{Lg)Js_u)srALbo&9)ftre?Jy%j_-UB zA}?yngJHgd&i5jo@!vNFbLPLl?A*V)k3Gl(iMuy>0PNoSB4&=g$(%)w+z)ndy%G1} zQ|<%1w|lCy*qhud<@AK_F8HpLe+O~SB454}*2Arc`{*m*Uhr)x=Vs#c2%j=}kuTo@ z>+?p$efX4bg7tZQbryZ)8;INYn(8d-$k!2fKk~J(HLt4fqo#ays(D3q7B%H7)3ZI` z%L~4&;7b!YVDh4-%vpSUOT*m9 zT=KF7F9W-WB@r{k9%S+&UtS7!4~rx2V-NBYuzOgvI*UEXi=~`};6(~vIOQ)uoU_Q6 z7lfbVIxpfr`pWYcJYUM0n>anfr%Ybt%k#kcoD*>$KIOS!ea>E;MPGRi;^Vo_f@Ujt zR#-nXR%cOPo|(8mJMv5^XL^`g;Ya2ya^x9c{Y;Cv4?psBuzsel&ccs8O?q~6c&dV@ zEO?5!nHxLt4y_GhFEqE^j36W)wBQoK`@7jd~G<;b03_R#5H7=P9Y=3>6w=|7km zJN;eVJ7e%a#F??vpVhrH2LDCen!i=|QAhqG)s%lvHGiq{qNe<7s`(SlebkhHPVw*I zAK^({Fmu%We&V-@zgzG-@C0ISA)cw3 zyx=#gv#2A#Nqi#LR}uH&Q+~Z*b`Z0?ROLmE{7TA^UrssJ;y!X@-b2kV!tTfKfZ3v^ zOkU*3c#r+KBksee%x+>oet+D@eq`!J&N!I&Q1keL`5yiEu$`Vlpg)u ziyZmMf=8#EM~QP5Ir8HLKbCSHCeB&p$fF8=1a=Q25Iv)>OkU*6BVp%$5OE)K$q&JP zC&Q|<*n>PgJ$oPgK*2)`z8~ftbQ*%l3%<9ykG}HYlrIm0-ARAMedNdk3myPFOJ9VK z$d}2Be7PU|JlEcc`*1RUzOmpNVEbN+cqaPFJQIE8>tOp{jku4#@-?u1udL3Z zuY6T{_EPwYf-f)lGT6SCAfAc7GS5U`xd&|DixBtGSH2jw?*-La^p!76&z=LHU+{Sa zpPTp$a?URJtb)&k{Z39p)T)_h!kh8aQ~cyAFXHm4DMvm9cE%GC{-Tb|S>($n!OnO* z;yz}SPk^2A*y=21l#e6M?}AQ8p<@a@8n)Nr)mhY+k0ky~iXQ-j?c#wBXIXl6-6ufid?TPPL@DA_ON}9Yp0rP zRA*6BUX%DFuB)Nd;g`6sQr$;fUNyy6D&q3WDZTPmzRW{eKEv+%q}kuyU#_cvzT38G(Ecjyl}w_ z6}({LdB~Z+;Q0!k7k-xWxe&E#=9%zjd~R6(b0B(0j?7u)$aBK_pAB&z{^i+W{m)XJ zg@1Y0lrsZ7bHOteJR|%L=hGtcqL(~9C@0QBTt=jCa>}$N1ig}$Wx@8 z7R-I*$deV^hTTI0F+=o~$%}ls3G3N^aUXk-yA<4E;Q!yh+Wk`R&Rq3^x0ng<&eDlF zo!dRQ7xxtVWsmMP-Uoa4J@{Ta@t!*I-h7Yoo_V+N9q{hso8jAt?~CuKliwoWT&MXE z-=yDMC%)583n9Ma_|EyZ#LtyKXa3yzGwIKwKePU<`ZFCr|NPA7 z-wFP_`FkRMr>xC2{hsO66xIL(rkEy^Y>M z@1pn6`{)DoA^He?j6Ol1qR-Ih=nM2E`U-uGzCquj@6h+?2lONQ3H^+ILBFEk(C_FE z^e6fY{f+)X|Dyl+&nCF|pH=vuUBrLp(cs!dE!0Mnp~=w{Xi78{ni@@mrbW}C>Cp^m zMl=(e8O?%bMYEyV(Hv+_G#8p1&4cDe^P&0C0%$?B5Ly^5f)+)Kp~cY>Xi2mbS{f~b zmPN~<<VzT?Sb}0d!fD2K4@RGAKD)sfDS|l zq3-BlbO<^W9fl4^N1!9oQRrxN3_2DahmJ=lpcBza=wx&XIu)IUPDf{;GtpV-Y;+Df z7oCUBM;D+A(M9NDbP4K#E=8B2%h46+N^}*v8eM~~Mc1M0(GBQEbQ8K6-GXjKx1rn7 z9q3Nf6WxXGM)#mzs5k0^`l5cQKN^4rqI=OGG#Cv*_o4gIQ1k#AhK8dD(L-ni8i^i8 zkDyWLQS=yk96ft=(KqN@^d0&h{eXT% zKcSz|FX&hF8~Ppnf&N5)p})~T=wI|-^~ZikXRaNn3u>SyYN0lo3{8%vKvSZr(9~!e zG%cDAO^;?kGoqQ$%xD%gE1C_>j^;pfqPfuAXdW~#nh(v77C;N4h0wxi5ws{;3@wh9 zKue;f(9&obv@BWWbDw>!S_OhG-+S zG1>%eiZ(;t(B^0hv?baKZH=};+oJ8z_GkyRBiae=jCMi0qTSH$Xb-d}+6(QC_Cfoi z{m}mC0CXTa2z5sXqeIZ4=rD9RIszSujzUMHW6-haICMNZ0iB3WLMNkB(5dJ&bUHc% zor%suXQOk_x#&D}KDq#1h%Q1Gqf1Z^bSb(FU5>6mSE8%X)#w^@ExHa}k8VIWqMOjo z=oWMC&`9(!dIXI^kD|xWVg`miCU zDbSQ?Dl|2k22G2mL(`)f(2Qs%G&7n7&5CA2v!glCoM22 zqoL>lGz<+#52AgkC}~qgT+Y=r!~@dIPDqchI}&J@h{M0DXu)LLZ|~(5LACu=5sB zQk`43b~Wzq?hxF9)75x@0Kp~h?(QT&NJ5AU5#kB)5cekTZp7W)UG6hGwYclNXIJlC zP4)Tj7=QT2Gpf34uDRyB-nA>_oOAET@CkehpTXzw1$+r#!PoE&d<);f_wWP!2tUEk z@C*D3zrpYD2mA?t!QcP<>wcvIUMoOFs05Xv3RHz^P#tPOO{fL6p$^oAdQcx4KtpH* zjiCuNg=WwkT0l!^1+AeCw1sxi9y&lr=medi3v`8UumN<39?%ndL2u{-eW4%phXF7U zHiSVi7>2-57zV>(1dN12NJ9~fg3&Mr#=9~U^$!!C&9^Z z3Y-e3!Rc@YoC#;a3OF0ifpg(JSPAFD1#lr;1Q)|4a4B2{m%}Q!0RPd+zhwCt#BLM4tKzva2MPS_dqeMfqUUTxE~&X2jL-j7#@K~;W2m|o`5Ie zDR>&5foI`4cphGW7vUv%8P>up@G86pufrSgCcFi2!#nUUya(^Y2k;?$1RujE@F{!- zpTigMC42>6!#D6PdS zg=$b8YCuh>1+}3L)P;Ib9~wYIXatR+2{eUf&>UJoOK1hHp$)W!cF-O=Ku72VouLbK zg>J9`bcY_$6M8{!=mUMBAM}R-Fc3C`K`O<^)?7=4L87za1-1Nx4^A%8{7_ez@2ax+zt0YF|2`m z;Xb$@9)JhoA$S-bfk)vncpRR9C*di08lHh?;W>C7UVss@6`&$ig37=LRjNWYs17xtCe(u3PzUNlJ*W>2pdmDZ#?SM4;`Q*bb`*%1-e2v*Z{gi59kTKpf~h^zR(Z)!vGiv8^RzM3`1Zj z41?h?0!Bh1q@f5#!Dtu*V__VOhY7F|OoT}=88(I~Fcqf3CNLdlz)YA0vtbU*g?TU^ z7Qm*k8Eg()z?QHTYz^DMwy+&+4?Dn)uoLVIyTGon8|)5yz@D%d><#%;Jj)J3M5iEu!a11PkW8pYB9+trgupCZ=li*}H1x|(2;B+_x z&V;jI1)L4%z`1Z9tc3I70=N(^f{Wo2xD+mf%V8B<0awCRa5Y>5*TQvhJ*3~S*PcoklQ*WnF#6W)Ti;T?Dv-h=ny1Naa=f{)=7_!K^a&*2OB625}3 z;T!lCzJu@K2lx?wf}i0R_!WMG-{BAV6aIp~Q>m&cD1Zu35h_7tr~-UgwHj218c-8z zL2al5b)g>AhX&9P8bM=d0!^VAG=~<@5?VoPXajAb9khoI&=ER8XXpZ5p&M)f-Ju8c zgkI1a`aoak2mN6H41^6~5DbPPFcgNta2Nq2p%Btg1fyUyjDfK*4#vX-*a#-VB$x~v z!xWeb(_j;r4l`gT%!1i42j;>&m=6nJQ`ihPhb>@B*b26WZD3p24z`CKU`N;qc7|PG zSJ(}9hdp3V*bDZCePCbM5B7%x;6OMC4u(VEP&f<@ha=!fSO`bK(Xa>>!xA_Kmcp@c z92^hJ-~?C>C&Ec^GMoaZ!f9|ioB?OTS+D}mhI8OtI1g6B`EUVT2p7S{a0y%rm%-(* z3a`Qc7H)xi;30S!@GbZTeodt+)P$z6 z0kD@9Mgluif%~t(9arGKDsU$imcVjgP8BYMtKlYi5T1h9;RE;<$Xc-?)Q1+(0|vqb zm*>I3Oa47<4X=*^Sxf$%Ww2@_ogfm3*I!;Z@H&cUJNo;#>0kEap|u z!Aj7c-Uj!=3&0Lk{Tj$ytui!%*3bp|!*F1()tEswcB~rT1J!nfgWxDQ9WI0$;T~YF z)n0`U0N?6ltO2OJ2;0?%jlOJOze993tps&oG8oWJ__ z@K-8Tqc*UwHQGZzz_SJ!YfOi^fM*SI*4Q2P1!hoVIq)3USOs^$L$DU!h0lQ*)T{z^ zpatMvvpe8l6aSi%VHWV**4!1CK}}{*^LSVR7Xfpqc^5nluK_ctiEk}tTZc7nYC&suoaIv1Ejty_UP)Or^1tn~qW1wX7#OW|ZV6WFoZ*8<+P9|62;zX$l%;m+#R0Cu5HXBYtNL7j=feCsgV zI@`nEa4;-^6X7%$Xn+l_$8I9%QIFN^SW(-ovk|%*x9;MfIF+ZBkTo- z16k`X2R!SpgiGK`xDy_L=K$}zAHz5BYbsT*D%1n+tzLH+0UN;r*ber9BLUxfX8@k{ zu7=xy?}U0!zze`LQIB1#_a*$5O4Y}=elx(gK6h8Y5ZHzK>_UBJQGW;64ai;p7&r@f zChFe=_||_B@T~tC{GLiR;Gt;H6xu>p;F)MJ62<|uXuvbk0M7<{0-g=| zZ^L)td-yw*YQ*^)HGz)M1BO5mFoQ;THrfKn+vq@82&cd~fM+9eHhLP~0M6g&JNS)% zBvt_Ksxes`w}Wml6v){a&&K3zOwPvaSL3B{GMo)$ZOnZ&z8Bbm#;*asjekm|n&8=_ z1~h;cK-MM$fw?xB3gm3E3vl)(M*wqeaw1#+SHK-`KfDB-zX|7W@>eR=l>2ImU(?Re z7e)c!vrXs1_OKTm3ivg}uj!d^K3osC!(;F~d;s61Qq9QOtQIr@?!VcF!2LI4p3OLW zGjcX#p3Qi^nsNSSi{S)V37o$f=WoXOo4o++RWo_ASIw(HLud{7Htz@ARr54#0`r0M zH{TcVY<@D}*&NU2WNl8?=Ed*~yaJyB_tl~j)C2CRMR#DPEs9_w%z$}7<`(R0i-X`e zI2A61)xbmw;Vp^C>XjHh-m3ZL30U zV8(4bK`$tT39tZ|ZCmcE?IK`r+v3@ltZi?G2Y{SynQdEgw*3bFOr_f4*$&Tkc(!W| z9bp3)24i3rY!2)~yF+0aoBVY6JDm)8b|Pyho{LU*!u`N= z(TV4+(|dq#C+@Fv0pQ!Y8MKGqK<3Uo8=a>DGw#f@(HZZ~WbJ$!@V(giR(J%Shc^Mg z&ff#)?@|%!Koe*KT>#H6cy^f%3xM-?IT%g^=Gx^_;J&&%0MEgz!1=p;3O~c&sZ`gh zz-+s=gAHI9Fx#$kfV=9tFEG=t>_At}-}PeP+3k8e+y{@tv%n0xehi$y8|UxF`McrS zjUDJV3Z}tUuqzw@i{S(~9nOPQzz%e~4Y;pvcy=Rix6gnXY)~2MLwo1}8v?!?OoEv( zAGUXZKIwE8uzPLFOKHpe=L*X4_*t%mZfIV>e*7J(dAkdt3%L zz#V{ZkC%Ww=)nwnds0(JqOt)9og$w1bg%(W+T?fDqs z*%Qy6U%;=aR4?wT7a4o8SG|~RFFbn0(0$k6fB2xfw}g&7I+SNJpnJm zhrro;r=S+Jf-cY(@asJerot>>Uwg9)z4wD9a3Wj?*8rZqAB3lYx%Pe=J_2^O58i$1 zKvU=hyw?_G4%Jje_a08IZHz z{;&|10dwtl8E{|y?gg^;!>`{vK-PXg!QZJ=|Ju+5$l9OT_Q$jT6d+@NGWOpM@a=yn zoB(IQ<$!Ph`+>Rk$Fu+EfaicJP#?Ij0X<+tNW(N>R|jkldjg&V@EpK>4Iu9TW-x&L z8gMn-4a{Hwc?Y}?-@~7&)WE9H5bz$@8So$24@SdeU=9QEA9xTfhO^;PAnQP8Fp#VR z-+&L`3-}?G+OPuDfCkV4@Z4}9jDxALCF}|Z!_lw|*n>+rfn}6S)7u`@$l?Z!l*cyb8!T_#VJ>F!LPzE_?^Sr&2>|L1X9w%&*>1(J>+;e4VdMS)qvlShv69@=Mc_5v3m|I| zGblP7@GWAtMW+JyRdfw-UqufC_f_;hd<(y&Qls!4RS(EIimap9t5Nul!gthGup8hz z>M&RgXTbSzJ=_kQe-xghK7}9P?^J3uexq9gIY$qM(ZKzW=Io<)0A@NGztQ-OCgpT4uGQozcKiYIS(#{Tc8-8 zfmh%w;M`;J8_O)m_J*M_7VsOp0Jec$fc+oKJjX5p?td)j9=i%S_t^V@bB|?~V}DGg z#xcin^?`jF$1KNf0GxeX8pt|s8ZggsJXhlmfki;haTmh1a4T^3ah!eJ)9^NY0-S$* z1!x4Ee>{1|bNAy%!^SWZ$UL4IjNcEAhGnnlYz=$DLBJd)oCs$D_ch@p8Csu*Fz&s|Bdm{HYaU4vC z`9S80I|Fl`$ebrK=ZP!fdbkUo0(>XF3*Vd@1&8y2b0)|Nn65>un*ul zi8)MK4(!7u@=jvDlgK*>&q?nC87Ef+_F-~!Xbe3uFwyL!xUHm+W>p8@j^Hj&H|o;jjw?_fSemY33zV& zHhc`fq*7A~paHZ5&Oc=suwPT=0-jSi`;@~0&nb9Lxd_NPg^W`kgy-OOco*=T@-2{c z%Acv!)H=`+{?G)#ugV0$mq95!LLo7@BB+~fuL0KSIbQmN_HpdqvYc3?Vw)7jVQqhJPX z3d~?S^PSENrXLIBoqhpa0o>X2`{8AH559xHQmGj=fbZEEJYzG+Iin{G1fGK#Q-Rse z*a?`yjAP(*I3MtwaU~3GC~vw$K9x!bo5bW=(_zur2Hdhr@C>8!iKUXWatK zch=Le7Cr&IXIF%}&>rxey&+5hvd$*!>^*>-v&lJ|na<|?vo8nkel~kM`(eOy_N(wN zFx%O`r&4oj0eR=(IcG3%{yAivvnw0`N5M(JPR-$-=CD(9nCYB5fxDXXIJ^$*!JHp~ z`O+A3P7tVg48JV=A?v3N!$6F6a;Jz=Em3T`gd* z7VHDeVFCBG;AB_<7s3_5EEeFq?!TihNbz6R0y|LhU#?c>75|d|qLG=D{Fiu5cx?*J zK!38;NT)yL^?6miEmIBN*MwT&^EKz^7N9@ba(EwKo3zIFu1!%L;Q9HF9P+9JQSof7ao@xY*Gr6ksnk|p7(;3P`>+{H8JLYHN zGyE^K>aYFE)^~kB$2#X{#h$a4)V6{yGU1QkWB+WKK8eOW`JPX;fes*lzVA}qKs}4Ks4fwe`sIR^U?L5}g{KBkp`lS8UQ{QW0zqtCc)~T27 z)y@_6S>Mm|l`FinFjuah$K6+WzqS`Wm{m_02I1MF_9%(QJbU5U8~8g6 zw=k@jKXW5eI)vq6@=KvT8gTVW!r=B}IlkeyA z=1Ujm*)WrdzrUu2fU+r%?mEnptyXnIGc~$He{P*wJ)}wMs~K6I(AQ?^HOiW;uh+@T z>nxizQD?ATUgvYk%WF#O<#plAmCNorTdySh=J(3>I*oS`jDm27qV!2L-g7j*V?cKs z<>$3YH0C!Jzi}B)eHTQX$F%C34~_L4kKY8CpYhfAgFlD$HNASrY0PsYv=d0^UbGb+1wV ze7$PQL+f)*r^gIXFJ;o5`}6xezOKBqmS-k8=4A3{PqO7Oy>jPMr~b2GHYmqjmN+`^)lg#e3&pk|iIS{W#YFa3CC1#xrK-liG=lJHtK@X7hJt`Xn0j+y&2FVYiI` zzPysXWJXcmR;MhQV`&=iy*t`HU{6r3_1T~I%&Ejj`(|gbKHkIEsc)R#_uLEp-rzk{ z&;G5OukYn+RZ|{XpGi4%$JsKNHs3n+)ZP2@TCdVROEP~yYrAKjhZN64Y6*lhjnXI4 zn5UlaL*USi|1rES1MK7)PCY$g2ex7R{JNNlyx&SVLFoVAX(RPdsE_jKp8c797S-fS<9n;8 z?mz71HTqn47v|3O_et)}-;?df9rV5v9*6KgqV!2L=6M&Mcf&my|0j5T5;W^5Z>v+5 zIF0u%M!yE`1=U)gr+EJ~sF$9-@XX%B*Co;Wp7-H>KRf{5M?Lin+PQo`U$2_-(E3~t z(&Hg`7}QU>bnn?R`P%Z*`b>|I3bBzwBJTpRZRt5b>*eCJa3TW?Mxo+iJyzlVS44xr%wIfgttIB-hp>PJ+x0z87y<0CdyZy+7mlZ zw*J!SzQWJ5{k|-|{=DAT|31mH;CJ4hxsS>I349OXZb#{pXw36dJU@faGyXsD`Xgvp zqr9z7S>iO_`wR46f$FTsPrUyb)JNZC;dy<%uS=p=zb`>OzlLw%JMcd0sqZv9lkeyA z=1Ujm`POAp4&8B>AzO{Kx-)-HozXnk$1j{&R4<#U^(!q)lKLcf>hI2W=NHcWEBprG zJw@r0XuRj|`2GQ!XOy4UCefJRpZNWi@znDcbsp2IZ$32E^Y4HDr=$h7GroET{W+|! z>D4<`Ac>zpW2v=%Ec&B1%4iusg&BX)|K?q zPGsw$EbH-k-g1Ta8Rp9I^F({)cE%6|6ewM8ySDZeopKAC%Sa$~R@AdiC zsi*EQs$XfJY5sBN^mDO!wxO;qbj{TJGe_ye^G0dRvmKu8p##Xj8*BjTQCgj}aT@R4 z5q&4{J-YLA4^SU=blv{MWlN&Bex32{0=}<$X-}j2Se^9w)cG9ht^16ckLi+V*Co-bUw=@~fv_PA2JfSu+J8Hf@8|R8OBd!Dl*u%N*P)mu zvt+AP-LOoJ?$DoGXI2kslKN^!mM8SJS$d7KX6x&9@){m!EuVBz=dgZW=d+og*M&X9 zY_<#G>@A<&ducuOna`{oNp{ljp6&i9YDdE?2=^>XpG0GxWAGdc<1+rUd7T5=g(z>U zQCUs|@pa{;wLDYFF+Gz#|)SW>Y;s#%3zt} zG*Q0t)SlRRvh|ln_Z5Dg?e}Hz_2>1z{`X0q1;6w5%*`kJ0@xJ7-Hy^H(Rk0z@ZB7= zpHY5Zn?z%NTi~~4##7%_QRgwO`sPDpJ-5PdYuGE}tM4v<4(n@r^^Vh+=Qe1!h3!E8 zd&54UdfidzZFSPdX}tIL=ywF)V_$yW57b9@X8Em$ulIFH^y;?*sOL_wGwcf9M?H0? zQT=@0eCfhGyJRx$#_R5&Y|5j%_Ggbei`A-bk4%m3&}(&O>7+^Os~K6I(AQ?^HOiW; zuh+?I&p>PWq>DO-_47KP&HTJB>=|aWU9kCues=Gr_0(rRvvMTaNxyrx`}iO_`w;Z1G5;fYeIc zB~IhLm!LlejtA9RpHp~$DyWxsE$J5SPtG({k4yFM&H}K zEPuYWaT%3MI}nwrwA!-Psh4)b&K>qx-_P@vE4bEvoO)1S}!nl6b(9=f-%Hrwx$%+1fq?&k`6TnTqUIFl%S5{-FY zh3D09O~(IjUhe^&Kg!$clqF8%y{|=oJ*dul6!U%!sE>9&Jg=|!bxHK2-(UxvfvGo|frWJeBKq zxC7$)XvWH;I_XQxlq9$O!YuN=6MUBNo~_1bSB>&W>u2vZme>4je*RouFHh6T)6Oo9 z<&5g-bv9e`@@Fosm)C{&BriK-wq8k|BmcbE^Lj7d_rVhoJ{wW`BpUO)AI}Hik&OS7 zygmim-zaaZQ6+45+WZ6T|bHUzjycpR~Vv>if~o zX#L{q%UY*iy4P%d*7x&#~u*V_0ThB`2+vFX?S)^ zda~bD*zYo(F*Bcn*U0-iybWP~&EM|I^hq@4`3CiG!doE!ci>&njH0})PTDw4Ht+YS zeLv%`9kbl#ovluJa?wl6eyy8d=)1n3=bFdP{Q;Rigij&N;P1fnNi^p95uP8zCmH|G zc>NqShbV8WQ8yO`dq)#<2U#n)K9r|?|ue8ldml=teb@K%9G?+;wklNO8LJEp$rkRgdlpQe zM6=Gb3cgjL+CTmk8u8kgKHA3!?{#&`5~uOr)$y(QpFJux;pe8AK740c{&hWkJ^j{Y zQ?59@`qcpStOd29E;yeO55Bu1`}w@;l@DE*r%onQJzndBvZ;^m+Mhk@ELN+!2ALY& zq1Won(n*ulS2MCap|8!-Ym_xxU$2vw*I71cqRwEwyw2y6m)Dfm%j;}z-plSfORtjW zAa39MUS;;WLNhWphYk?#P?SE2#ynf#*%Df1{5$g633Sg<-d3k9aT@R48hsmR3#zp~ zoq68{)Qj&6dzROo*?ai9BzoVo9p3H1d#E4Z8oX~b?%1E#XE1HPb?T|R zkLp$0XG!MoXKnY~m0sPT7lbp7(kIcF=LUFohaMUK-n{k!%`D2>>Xao;OPt1g4@N%(RA)U3d7lRL(Y}S}_4U3kiQf7R#djF^diB!Y z*;#xatCK#TI-f(mb)QjvO_xL?58az(w%TmJPck<@C%c~_dW?c;5Y8k@pG0GxqwyRA zV>AAn@H!oI{wQy&QyqfzZ#<~yMlca3gZEKS z?Z2JL_w#x4r3>>+%4FJ@*D0WE%A>muvt+AP-PBBt?$DoGXI2kslKN^!mM8SJS$d7K zX6x&9^71;%CQZ~Cte4mMT=Mdo(t3GaICJH)yUx}t$-eo$vb~;#_iWf4!X1jzC()Sa z96aa3yo~=Aylx4)=O}NhQ$fSsn}M%a zFMVIwS$rR>lRlq1pF_QMpHY2Hmqa5E-J4~$+HAj1GB-abyPs|7u`TQh;Y_0RNi^oU z9iH36jv4>mc-3g8C?r?mAl*tI4;{GN`w9!JprH=Aw~@?mDc^_WLCF>F>_=<|uj` z4aY!uM^XAD8uMI)=VDlr@n6d8v7p&Ud0U;b#A&j5A4l!+8Gk*8;W@L_DNinXY4zM$ z=FoS2KhHIfoqHLXPJq)P%%Hj29hg3e#ypo(e3Xytzj zoC>Fb?{yYGuK@MaF4;M(m-&Pl+2KFGVGJ<=W%lk z=Wh3PHfK8rE{5<-QTLlfW1i>Yc^<6H_+P^7rJ%D$d0U;b#A&?u`RFeI)me|rcz-#l zkDi6_yuRMoCDB{I3-P@Oe7$<v+5IF0wd2K}{g9jMm&+{61~ zP%rI$cxLb6>yqew&+GAC4L5-IQBOSsb}rw~*Q=&Hv_993^tcJsOPO@%+4A_h^3qzK zo5^uoCXe>S&&B63y>jPMr~bFVt)LvY!yTX=+NY=tmN`xnv+5IF0vy z5dA}-I_vQS@1F$q(RW#RUSIF)lIX49!}vY|zFxibU1n$TeXLIUeCm7-_11kx^)+1* zjXZR3mf32v{XWUu{G9B5o}$Oo@G^umiP9(0nCCNiJ`2xh{MYjO3h4Y%-d3k9aT@b} z4z2uOfEVE z`gziO_`yKS}g6gct zhrIs?)JM-kcwS%c>yqfB`l*NZ-_GFuy*}SM_0;`E^(*bOBuTS7p2+tCw zPonXjpW*vCD0`Hj*Cx@J-xv6Oneo&PMxDpB>YERZ_52FIui=M`ulCZP!}^+Dz2h|I z`3>4{;X9E3kMI+yUUw9FTb;CV8t?r*dhh=;KmP*iq5HDz=I?8LT@roROa8jMC~wo| zOQSxzE1zFIlH~U1wE6r>?Qf}6#R|NJ8Kd+`G#SqdzvKA_{PmB2#frRE;%A*9%G+m& z(X8|S^B?~Tf8(8kg8%GQu`)kb$@J6S1?OJp6J}Jt()2NXOKP=$;ViP$<*b)C_qHy- z`6_cReM2AS;AdIl<@@CNdE6X>x!ZkJtjfJrgF561&ZJ$nT{3+V%{tHO)YpKT|M*v| z%WJ)V<`(6>u1;B)SE(BBT?=jP|Ljq*K0i0eci+zBtY2xL>2BlB>E~keY)D-r=$NVZXO7Z`=Z(^sXJb5@K+}wWCtf>)dX!eD zEO8p|-3)yTP@VPY!uzhEKJ4ha{fWz#M6Z6$K|NbSD`*40zj|t4qx$*0`O<}XT4yq~ z<+U9soAT(c!`Wr4RbBf`jqcE&TW3}eX_ESCMwTb^wOM+NvS#b+b@J*EXf2;~QRlFJ zUgxu!pVx&w!)&$-;p{D+-Fs<0^_kDC97%T4@1E^`H)=P4?hx)-ls<{Zd-lM$C+Pm8 z{Jb`a#{7EW*E{2>?~ADOm{xuBp|PHQ@aqdBGrsx`@#nCVO zRIfV?W<|Rd93f}ak~`m zp6%>tGL3;vAe^(`E7K>@nCDnL$HDlF|8!nwfaV_MZFR~Lr}5qs&~F4AgKDkMOy17| z_0o5Pox%Ee4_}u=Z#^gCI|(L(`cHwWFb%w?`s+I)s<(Mr{(NiWGAfsLz|X@nl~!BU zI`z^{Wb3iMpXV!Ac%NaeTt82;SAJ(~e`hnRIj}W^yAh>NqA}08c+P|Q8UJl~-4-;% zC~vD%mN<>~UVwg6*bG!_eYWHM_Ml$+ZV1opJ$zjfz3;g>-dlk8P(OV?*qMAUU#pt( z&|0P~@!ty6M|pJD*|JzozIB#Cy|oMe{MIuUjXZSMVQseGC%I35ceXb>&|^o~55hZ& z(kIcF=T3O;47+9g_viHh(CnkUtxj3uH0Hev+Fe0Cc85J+PuL6OV?7V#{Xw9b|3TYGz)ai?%G;bf&OJc#rx1 z+|M{XujZnCHBG)hkK3hi_iSemCetBsG=y{Zdu9408uL69&%@wwkpCiB44QkCx7A4- zr}5rLpg$6Pk0tzk45*L37c9T^@b$hfiQf7x#P=xh_3EYXf~Y=LCw)G3K8JeiKK=Qu zuj!I#mqvYbS7DDNx&1k9KBwY!8e9fp z#wdLfjd`Aq=NWKj#{Y6&SAotD3aO(9Y%i`Fhoqht}s>NsseEy_89Jo-L2BD=)3(xquuOXYy!I{9Jqv z(<^sAb?Sd1Tm;H-30w;5p?!+VV434IQNHrjp4fS^^_NEX6@H%W_hs?*=k>n+_eq`w zzw`FYT}Ae*;T{NgJ4&BKW1iRGc`aO*@h|3e4QN-Rysb`I;xyj-dh|Dd>a54TyuT0B zN8e@Pd40XFOQKi5)u5g?!p(3CcpvrDcbc8a_w#x4rSo}iqOWDTmG`&79iV*5r27u@ zWUE)L^t!|F+}0;oPs?;Wp2~G6+y!xcG-Ksao%E$;N|IZCVHWw`4L(bF&sO8Jt44XG z^|SXH%WHl%KYuQ-m#1mvX=j(laz^#^I-9L|`7@W+%j?2>l9!z^TdyR~k$+z7dA%R+ z2jC?LpN%Me5{-F2i04D_aK`^-Ue|*5H_F@UlqF8%y&pmU7^u#Ayu$leL4EWc7@pVH z`?@50^?MZ5^Kp0*o&xWqp87ttGx>f#Z@zRs&lB{uOi%Ov8F&tqPnmSzVV-RDs+C@M z7@phu)si#@Ne;r%+i4dJs9 zrB9+U&o}UV6W+@Bzr*Xhp#6>VwmM~r(`560kJ|TBsY+I_@4xV@*=m(32Yn^}E>_Y< zHTv$e{DFVoGZFa+c<)cqe+ItCH~jo9sE>Bd@>>sI@9UE2t>5SPegVE-z1X{TbM<|!PWpW6 zd=B;2eMa>)T@sBvbZ?f~YP0=5$=v*$?0&wZ$M^6ngfofKC()Sa4|x6vKV|%XshHXy{kYo`l`3~(w)Qlnm&nUoo7{it3maD{3|u*wZ%U@bVs50x;kYcN2wa`T?1`R zs0GwopGqxx-wM==d$TjF>*GCqT@tZMG& zbANuH$JdpY*77tUN8?N$?Mb#ArdRHK>eRm>Gy>&l0!=|Zv`>B(mccT|X`+1PsXg&t zQ95aKUtzv%zb}igKd<-ozfbZk_?@?Bt~J@)Kov+5IE{I?M=Sr1&aEuXyzd9<$9LMg?;7i6K4HcvdU@;K zEWi1#uTMD3uura^$IUUEyY8zy^nl)=yXy}FU?7C&i@NJLjrsS)uNSCKALt8VzYY1_ zAkZAbUS;tajL(pam!1WGUY|!fbYAPP-lgf|@+YlT&s?-+&1F4w&;Hy|=g397zW3zM zV>=j?sVusveF^W`_G2h>83w~6?!)he>1{^VBTC~vN8mdWw108hBpUN8#4nxk)N^C` zqI#=uJ~Y;|2)|Jx?@iE8hZ&$+>$5fQw*mFi&V^_89=ePQe zECA)$3^oV#&^|?Fu*`9qC|`MMPwYI|`b(qx3O~>G`?C1@^Lk(Z`y|hT-+6oHwk7*^ zup5NC9i>mAG0*Ms+yQpX`0vi^9-v)~^0qojd|{i=YFt%#(yENM}f{Cv<5q2g4zt{)fTga0GZy_1E)YXY{?z%kt-28<$bJv;$F@N~_@GmP@KI%SE| zWb;0O+U2QKWj!nEq3?w7tl4UF)K})&Ea{^j`Yy2ifq&jKd=HiM^S8S)eG-j%o=p8Ia4N`u1)L3ibxo^!e2J9O|w6jOuH;BpP|>-Ym1#X8V1Tx%oNS z{hUXSm2ee=Gl|kC(U|A?cwPV(XZ)|`^%~Imqr9z7S>iP2eIeS5Ks_#jOW`uO9OPp? zujTzMpuTzr!tW(?txnoFjrV>R z{d@2c_q^H-o=+9NxI_3<9QE{WcHzK`z*@FA%G$M6Y!3f@!wwP#Vi&CBxVTN{^A zxwHd*9+s)J+OpQEmv$mskM;dLU%A5j40GlBd6K>IJ7fF%HM9B#{)BKhqV!2L=J_q2 z-@*48|G#+s8#KcxZ>v+5IF0xI0sW8gE2uWpr*ew_oUVYM)l1(E;hDXMt4pM}oCA@URS3qaT@Pk z^*{Y9*W~9~nI3#6SoU@PzLs9=aw$`sKIm1V=X)U1JEP5)M&CCjcjfcbqhywGx!pN! zK9y_ZR|gtq&h5_;q5rRQL}=D|*2T9T)c?o7awA?FgL;%!N0wNP_ilh*HRj)h_f0`P z*tP#UtFQHSN%Uba`Lkz{XEtrVH0qa0fx-ggA`(Y}S}_4U3ki9V{IdT8J54Bp@C^Q}`) z-CtC{(mqRaPCpl$XD51fhW-$qB}$(}W1d~`>#Y-b0PX$TZSIA_0Crca_V z&!Kn@gAp13QM`@@%{|K7>Xao;W8TBj%6}vjLK;+Wy~glZR|5uurzST=nwSy;*+qU0DOoH%CQTLlfW1bW6+z2LS z{3r9eG3cyO-d3k9ahh!2Q>dMq@z*mDo-!5>_n*hl^D{lPKbGD6eXXxkrZ|1r zOM2~3l(%W~rBNTM*#@?S?La=(b0^;K4C<@>3(s$UVb(Z((*EkHeY7)LzqtCc)~T27 zHCvzc{XAc}!aECd<@$NteTDaHd$9|%+7)T@sBvbZ?f~YP0=5$=v*$?0ycS$H8zogfofKC()SaA$T4Nhh_Ya;Ppt*`J=q8 zjx74$gvaW#`7fmQs7w#-dwA9?btV5hclA}aT2E=UxKGz|zbN zVrp{DTG?iEG+F6anLhuOUAtg+W%@8jl*T+4Q_ndnbB+>!&F47Kd`hcRmN-o|@8hXm zmhsn)S?;(@%9D#;TJ2Ug-}U`G*F1La6Uek2RzjFTbG17#eG-j%o=E*ka5BjMe7FEK zhbV8WlQvG{y-z`ZDx3zs&xQPa5vZ5;$j)GWyoaw#qW3*d$NLO86TFXlYPX{L`Fhoq zht}shiykZBY*0Vt(!Ki`_)NaGytF>kIb=B(&dcP|&ScBu>vV?l(E2>`*KYZl_zeFG zt@>-fvh`ix&y(!D-)(!gE+*3@a1(?(9Hmd9G0#i!ybLY}`QHq;fc7!U+v=o^(|GSy z=&yh)!S}h9pKk;8()U()X7AzalIVTUtMI-Wt^x0(p8Ec>bNPP0UNz;R^|`L4$8~T$ zsGoA_-m_)$wdJMtnO2kK2DmYkOFQFd62*8^G-bPg5r$-BfLHe+Q%qwt5cRZjd|aV_8w4=HE=K7 z2ls<~tmk9Ae;m|T-(lhT%`eOvr!TGl1N3hL8d3+WeDf&_saB1H0Jpf zo=?N`8UMAsz5<$il(*F>OPt2MpF#U9JO}FY0=x(>fqbmztGs^=)K}jVb{^|#eqq)) zebWBwsqcueUtE1z>(op4YUc|3tncUf$`#&Om@C)M5nDp?$M6cz>_Yw@y8Ee^LEP`z*;h{akFG-_h%P z_yNMRMCp@gyyuVj{shV%<>$3YH0Jj+e!pZqwS!UTF|GRMLt{OE#qYONs!D~7ulCZP z!}^+Dz2h_)&&t1}{R95|$G=KNUMpqlbw{DM&!QfsXuNmjztH~upFOHn=I1II58avN zkISZ>G5U4=QuHYJ&%PzKy32?@>*_RX^~jgbKEd3y zW3~f!KUJ#oyJ}FKUcpSF^hq?{vq}xTYvRqUqx`%!iN^e@)WWZJ#*>|nI?p<-&Ycg9 z^{j(mU1*WN(ds{bW>*GCqT@tqvtL>udnxYN%YpQ3%*^!*Q=MFJ3EW-V|CK!Q|EK2x9&5l zuj!I#zNwwSzML+V$|9+3J)h7rnIFZ_6C|uJ7l$=CN}RCesj@1Yriv)$YLbNi^m;l=@*X z9OOS4HU`Zh%G>IsjnjDV5$FrS_n5-ZQ$c;SLzdrq_KYOpSyyh3qIh=<)O)F13yF4vtR8Oz7*_xL> zv(ILq`+c@^M&+_IX6u#YIr7hoJ+IR^+a|C8!e=8&pG0Gx)A5`Eb29#$^12ylf1|vu zPFdnK<~t9wpJ3=hk>`1KVc&x94>S&{?Cr ztxj3uG}*j&q;{u_zn;19oZ0G>Cl|f6daf*U=)1n3=bFdPy)&71fx{ropt;%|m_CWd zJa?sjH`pEIe>fZgnnRSg)kzzt@!or&-wS+?Bl&qDsE>BY@>>sI@9UE2)o)Kw&%I$^ z*blsqdTOtt`uV*1()m35(AP5U&-(-5AW%MK(tZ2$hv)J2s+C@Mm`#_fr)4@2Pvtro z4uQBnnz8bzPWsX^CCM$nFpGQ-1)n9nXRGnqRiix8`q_Jp&fz@dX62*8a}l14VM)gS z3|`L!?QfK~)hSDy#(N)wekmLasVFcP49amT zoCfNleTvFpnd3B3zVg(b*m<(`mqzy$exB|3W%2dr^}hc1NuCA2^Y+Z0P4;u(VhDFT zN}ohyp6B9u9<0pxU&8C9pk0mfwmM~r(|GUm(O&?nvmTf6{&G+seV2vj_4U3kiQf8M zi0?(<>(xu&Wp)y_D!4l1e;2QJgU%o2ZFR~Lr}5s`puZNb1Jzoedw5?A>ZQF8&+I*XT@tZxbI&gJ|0dexMN*5|sB9yft{DUFOBXi{5;$5%i`wW$2lROK4 z=k1wWL-u>&K?rv{N}ohyp7-H-KRl4}e~8zILAx5|ZFOWRcrk%4oBt!!KAP#F@3HW# zS?UU&%k)*YT9$v^~iK!`>V-8uX*lZpn>*Memggvwi zc2}lPqA|}WsDBck0{OoVZ-C|#t0`=0q*%_>l_waQ| z^u8x|RfW5%!d?AGfA!SRldMpG&*>61)uRr(C*sKLel1*Or&oXIe{^SK!r5 zF6~UVJibn6C=adABY*9dpNY@#ztF0`_A6W8_5D1_&iiM?o~^gZ^bWiW;SNXXlW4r> zd-%Q&+QBG4uT7#czYp;HFypE3q^R?lR(F(?t*2{dtjB)z7zUrkr z4f|xP%T+IL-J9h%-}UtgXBqa%_4BwnhI6<3`hm0k2){#krl|W(qA|~(@cbEm$@u@l z>z|;rMtNJEvcze;_pj)G1JzlNzj*&QsE?k7@Vvg>*Co+M^-~Y+zn#JRdwsrj>Z$vS z>Q~xlNzUo#l9^}KR4P@q0BX`RJWGT=iDsQ=1$-+4zmR{`T2PxlI!A=}x;kZv)0lT9 zJS#&LP@ig09cqBlX}ou1v`wHXP-}gvw&i_0kPpw_f1TNT__`!|-?JIs&B1%9AJ1Ln zEPSnM%0p|JTHxOj)JJ)A*Z$l-i`C>?XBpI6yAahg7mYl0*Ou36v;98Fefqnzy=hO6 z4$uX{JBrdL(U@mPJUc<>jDJ^NyMbmO>z)tu&Q_;9x#*?U zbC=C`eLv4NkDa>*nR-GYgc&qfy93iF(U@m1>U%>UkbfGAKy!%lwmNC!G~T-}`u^a1 zjN<3fpg!6m%Wpk=y{}86SHFIso&#V*7zEx&J+)U+{e0eh>3p7n^tDWbc|Qb(f$}Mn z?%SU~Jddwet@OIXY`R=MEz?jum1{VRfVe)IvGS-+`qDBb$t}Myi+o3d&l29V)%fhH zQ66di?7hbFnqN5Qa31nBtvv1Q^0b^$J-yCmYhM1$KAU~+_u0-FmCMeUtyhxg$UiUk zypG{)V_`ak&qkC!iN-v~;W-{AX8dRHIuo?NQQlUkEO8q1o`7~EP>)G488(I~ARp^F zi}$lZef6Cfp5Oe!ta19%`cI|LG}r`u7WLP6rJdJ$m*%DVvebnc)K~W%)l)O``dqYS z$)+=fJ;Hm;_ve1b;dwO|?W<|>{dwFjg}Y}vJBLhjVP^>E?Dxv_Ni^m;56}6qAmhIa zue*Zg9_4Ly$`YsX-kYM|3^oVVTA$r`zdNXxz8mZe*2jDJx+Hqva|^t;gss5)sHeUc zqWbxI)s%>P7lw(KO z3DiUT4 zPY2B>%G>IcB~IhL7o%SS$AD_B&l$Ww6VyxlW@oTI-ow`=(fgiD@je!g1Mj1r+P|oN zzFsxuq4l|rr^hl-FJ;o5`}6xezOKBqmgfX=oRrCB(mccT|X`+1PsXg&tQ95aKUtzv%zb}igKd<-ozfbZk_?@?B?kuvefXg7IcB~IhL&qKcwE&|nBANA1pSa?=no2%Y>osaJYa3QGg z#c&B+3f@!w_1$J?^S#Z>a_3tcmr=QN_feTjt1WAtdTAHz++mOP{XAc}!ut$!<@$N9 zdl>FPY5U|ptN24Im8u8@qmKM@*_!E}FR|BDWB%{|{;8Hes!Hd==RRLzuc;>d-~atn zV86H5@N=Ym_f-55e&Rj5onl5Mxc4P_3fl3(|KI;qN~J1%m>EI=uND438iX|4!iF3H&>Oe<$$o1R^Go%Rkru)#^VrO7y4j z@*Q?aFI?0hz4*;Og_pkBC%xl!jnc;~?NRu0rRM3yL+7QB-eX1K8Anw~A2aUa^ht+( zRk+{XyQG(-4^Qv*7Z<;< z-`(jWiw{h1*6pXlLqFa79Ojpm1@s)6y6Bs*^ry!gYoF-a0;gKAs!#``u63 zDSZa#8Z_?Wk~yp%bzC~lOctGSRQfjNHuL9a3U5Mx)7l#8E}h;iyk^hhYSKVz{;f<@?r=PxkMoI0j^XjElGw{zJ z(sj;Sp1zS;b)-*zBTp&&IX6edNvuy_CLpo2ltdKi>VGFFDJ7kKd=-;x_cWt;Wc7|2U#kuzBRj2r&i=If|)}umtw~2Qazr`-xQ)fAx6@bbvpRiPx1UP< zFJnF@ZMH%2EY5uXt4-4@`gJXS1n-OYSdl*A$)}3n=C00W2TmtLL6y$wXI9>rZnvnx zJyp@X`uoS}mfI~TK7hIE_jOm^SiG`N%k;zSccZ&^DPB&mHMi$jv(zft}H~SQ?`uhB$TDu&n|8Al9r*Rh-RljqK z!eV^BxnyloBWB7u*3|FvX;DM&w#uq+ipf;en7e7hetz`n!9^`QJXiSC^WPP}zqUqE zyBGf|{BWz$#dmGDpr}QwYqV=?>a}~Qs3rTwS=W%Gs2O{@Iq!SF+O(+6rw133ea&$6 zZFr`?;C=5#hZePDN2~3#dd-YIRur}G@@XNt)^P7d9agt5-2eWQ)(j?phi|VcwH8aQ;`n)b&HPXE2x28BQJ`=;nq?HBIg_g1TGR;uIBbtOI{d`k#OFS)U*82_2DTo&oo}!kiaahQ=Lyo2^Emv_$|qj$oyB!T z{=Xh?E19@K*yp)+8%(X4lU);R;qdvX(zUC$VW2ruyJT>+!*++!n=i0KbmwGCyXJ?c z^yRg#95&;yp<)Ma`8kINiw3tI-|2Ej9x+?dJ+|a3fma`lT5PI}oa8E@fBBK!I#)6@ z%MaW#N!Jg#?D+ZuFsRh|so{yer~LIig`ROakjGTyQ=!K-*Ld1v2ECc~&;9Az>;7Qb zfb?YlT;WkKGsYD}4#%FoU|KNG608sVG*8W%?TXKuZT40EMc5l&|wCR6-s@EOzHe--OAM8glU_#w0ZX|Y_ z1^EnI*WJAk7iSZ?oioEu_H^KgP2d^5vlMs49jmowkNCWIDBCpHc)EB^Bx1g#EBta_H1kU=a2xQczqr?;t6#a#hCgclA|9;5f4P^<0!A#tPq%m2 zE-W(BStIMWnb6Ct$J@Fc*v@arS$6gPfm=2c>zdUkpF6^uZlUAF*va>Gt628d% zE49)oZi|C47bpCij7_Yp`X8ql{uI|kY(V_y-#lY?e4-1H(^z7|9`5rIc6Bd!^))i* zc_yvm5pf0gCiXdk`%CX{V^i{h&%wqIk)`yv2VHN#m)GDA-|krB<{wKjn~?dw6*t{X zd|kM@1Am;w^W|$N!Gwj_{wLVUX+Hl7KBsx;`dF@Sg@+v6Fwl(!6E?784Km_h zyRe@};g)ak^G|b@x_Ny53)$Be4G`#^A4$ z&gCzP(W{a9(RJNzJoY9X$`@{6Z^S6YT$>r$&IJRm^E+(N&BA7XheQ4h#=g0_oXvs^ zbsgQ>tlamnF2}TXo0)n144%A*ZOXQZF%Es&1aR#nz5^C}|Ha4LNbWCRyNYirzW;>} z|AJrCLze!z*m*mANOL>ZznTk}uYBz%{Oe!g6MC+AgX@&1w~fE}p3RD13+HoT-|#ER z(`HRiwpq|S_-Jz{trIS|DSWpeaz2N>Erv@ZvX&!Lhr1QCX4`DU#6vA+xc9d$_nhj$ zk=kw%+^i_J0-tv4@YDRr1fK0a+Vz>u&DukF1^n0MCVuSRw%oaAFW6$}6s|0p+p^f$ zCe{Kz*dp*_;oh2J)BI=9je`)_Lh$Ny%v16&i>(j~g;%yb_I(We!DekG?*9k&40hY< z_}69ZaQl}BZ53iMG17erA1epvB$hkkiLH)5{KdLTWpPg z?8be=vnqi{2axU8Yg^me_{}n`K-4j zybw<2zy@1G`UD%wRzIM~m zMV_=YTbS?ScQyyQERH>Z3yR&gF!2Q+wE0G_wk3$OPakh<3-v2)i-Gx?L)Ob{wSKo8 z9f6s)B3$C7CVgz~+9hp;^84JQ_3QilS7z-~!O{fmTw`)k#TQ!({gOl3?C7a3_zBms z<(NkS#do+wCGMXGy_3(`3jD4(e*HfGmch14BA>Y{6KwVQp)G^HmJDict8W@-D_{?= z4s2+vHR@=~6BEa=)=K*?*(7|BJl57iF5-9-vAv4Ow+49lI@~ykzt#D?)>z@pgEkr4 zp7z#QTMd~ISKaGy^4i!-UswUvlTO_8T$UZzI8Z3>1=VxCorJ)-&k{>x=-3ibmQ+4{(+6>@%!xm09+ZTZZbtXKDG2j>}&Y^t!{PJBk| zt;l9`^YN_*M|U$7dC5 zgg>7DC`Twy9=8(yue>#hT7#bZaES%B8r%Zz<6b1jC4-0Jgx#_KWUzO(@-K8<4IToo zcm2>O_HU*WBkD8%I^g^gu&XfkUK=irPFxByuLo9tGB%sfTV;E2G*=#bZh*`;m68Zi(o_i-pjBs z@elh8&*2NKK{^UQMV^JRfsJ4|*kMZ$i&qdAisH+%ndSKghd+UPMX909dh(m_JA67n z-<6$6&c)zM;G(K=Ee1-mO5h7%XNaGx z_G0G5Cx%DhL{-QY;h^E8#DW^|QO*Bf#FLuHAB+j9mDmRO{v2vt>FB+|i04&f^*MWe zd}i{!@AL{qZWLt z%-XU4)PneqaEAN<7ieuO%I14qi*`LV$W{PP4v%;~WG=QEJ_5(}W1cNu9Z{ZG23(5n zzZCFUOHD;NZB+Lx#&bWVPDoA0>vZk1-&4h5gcTnbcsPQe+dSAXxkD7&wK)eiG^-94 zjmq}KZ;9<;X=IfHe!srRc)ylJ*1LrI=P7J5A3SYQ{rW*)>^v9s0(e0Lo-Cds`DaKD zvZYznq>6JLE@Z}2hbl`>mmm3%Lqz6r|I*<36CJjvY=wK2P!6+coL^UYbob)kQ$aQG zupDdee|~C&nxm}@PJ&6nv&4dm$ZhzVCINK~Taq~t-&J$=JhNlF4S{%&=73KJnwzk? zGPOL#eepfk7ib79*HmW+$T__|kN@&uFco_S}%-rMZt52?sKzV zxPaM0ZT~aysxbN99zH{@%wiv^`vme+xQ5w@&FG$mu)|~Q3#1c+3Sgh7$wA}5N!i}9 zV=2;&EzNz-P}9+TWJ{;nFCY)G<*Cn*51Ksqa$fvpJuKcd!Eay&_+>uiUaFs+1vlQ?G){F=e?5cwFNKSN*kzL&59P^=O7}Q2?=fULG6^}%OTPLvR9`);|;ydOh^69mC z3g$oDWVSaL%T+|uh zUs2goPUKBJHM+LdyEZp*ynJH&P~TO4+RKay#ZL>uTY6@U2nOS$2eJ9*!zTjG4K8V( z!uC`jI0y%(CMq8>KM`{$)U6VHj}CW&MHAQ?pzdZiQJ28KjPPs^>m5goFM!?c#XjKO zrWDt{K_|pMQyJR@e+@a7QN8*D<~9+o&?;%2*+^Ui>r_8ApOc@yz&i0mzi%}Te}Su* zt<)vwY+D{&XTJD~_-gPG&jr`D#}mxN20lWkZ-EtXP1&{i5L;S+zEvY#3+~+~UdDaN zvxDZyX&(Aoj~-{hgQg*qrTC6&NE3O+;*1fIL+ExX+-7l;J`w3fpC@iaUS`g#x$g?T z3s#yH=;H0st7XGRKCrIaIqeUb#mspJbI*e;h!x_Q+7l8_Gz-Bd_8wGIH|h98RrvUH zb#D>Ax(>BJ`fmNgTwQI1@j746%u%msfO|@Uy0QC>;1**1ZPBO1C%%7UEk?g?{ zaW~bYyj(Tb{()*}s$(8zos0O+2l(1iWFy=Cpvy5|r)E#W?-ib9Zllo+cI0csW6;g} z%>A(HysYmKzdylzwHH90#vI`OGr-87;8DNhLtvcw5j**ldFxr1SU;HUpK}3D2;Vew z;ELM+5}qn2iU&uuAER1IHspDMxy^z!p8D=`?U#W|UvckA;CUu&bswL90bZfn z@^{37NyH%4pSNNQ%lXa~@RVAgnTOB)hg=ultJ=M}Lj5i?a*;1zerv3+YZIFcm~InU zqhiJ-IE3QO*UX@Mv6;!$#f>&mH&P9%7;@c2EH2#Tm@UNIiHo)X&n?Y;@CTcR zyqCI@zYZ6*Wzfrf_)k9cR1BMVn`;ShBkE=HZ9T^p#Gc+EXJ;SQ7Qjwa=P5+}OL;Fk zw&J^r@!v5o)x?tU5sk^eY*~GVcwGvaP){&t&|wjDT?t)WfWH+Wmy_IxOO_g=t;`;R z?4lezqXs^F7!FpE+?e`=>aDg0`mG8s{E@WIR$x9!*sJ&q+}<=-N>e3Yiqflm$+E(cw6-Xs7_-Ib@l`B0ct!p z6{E-?EzHZ9p{{y%{87vNH<_%`{l z`IWj}J+MJobqIf}4Gzg)t`M{9qeo(zjpM#|;ls>F_+jf{GfS{h9uLf_`)pk;5af-9=iaH`~|M8{Bu5W|4ZzwI5+|p3B$dOeT1#!w?2ndUj}&Y z^-B(^m}c{VZ}aiD4B035dma1Oj{oN4Sqrcm@X01JuUD|MlkhG23C!b+!GHA-m=}>9 zerWTv?$?MnzmlhkgAM^_WTW!O?tJDv{#Af~2g46;fj`0=@+I>(I?akNbmH&dBep^043-&$t~ogH^<92aXfJ0x;0WgM;wKQ_Nqn6dYF`YWA{@ zedLvQ<1f^f%-7`0t-zxr#A#x;dO^%l>?UkzBH{_(qnpk2eNjI#d%#rp?1iS|u!rNU zEj>9;RJ{5G>jB?Am!@uHu+t3karJB{Mul*AmLBep_5Wo3&ek$n?5!F6Ke8SJ?am~sod`W0*!=7JT%6jOqqPZMW^)AAkt zdy*a|aS8SlwTEOfmIQJ1JA%9m6u9{KsJ2L$n&O!V(Rr#IbvFdJ3na?HaBjxcW@>A);*1#@# zu1x^*6)(yWD>4)J>%zNx5<}S=@%7?+uxG`iyU0n%$!#{Uv?@8(Ft}AVI8S-7bSyH~ zxH|f&#(d6@|0mSo=2sKjd<9>}?|odJf<5kp+tkH&?H$pwgomxPVVDQS1_3Rk__7k z*jpBT2L9KHwd5ve66fy`pBS8FzV!3hjw=%a=`fCGwZSiT!0*UYY&Y~Sd`RFkoxqhO zY=oMsZH*1Un=QC+E!?;eTq8HJyC=`72c}UkuidJYlik_m#^8)YhoOtHz?e z%-|{Xbd)vUm8E#F2$_?EYCl&sOyy3N+IsK+`o6?E*`HHw(DQ73A=m;Y-r~FXPLK^d zzs@`()T~})ec4%C6KcORS#NG^sWI4F0-dlQqW!{D>Wn@YYqsL1>db~d5X+cfQ9Hs0 znkTl8s9uzxx@~jfEcI1$3r^CS-ciM}>*Pi3ZAO)UNVcsTnke_Qxv=?mJeT_z?yG01 z)@nAQ$6(p-!I$z={5Pe=jDR?2wllw`)U*}Zm_}vCFX)Fdg*`zz6}@-_U!q=O+gJg$svwa?uVnU#f0Jc5i`fkoZ$lhN2>dtwN< zr#>v(kzBYdw$cMzZ;vlG1@Bse6>XUtcxYO~!8%<1%46LtU_@iEsxdh8BwS8)>fzX> zu%HoG$aA!pXj8yy)pcHk|6?2e`XlJ2HgkKKdxC4WIx+AyAPJq+e?u!!2K&(~P1 znFn^%0-u#ryaPTr;(p{2W;y;SIZsKQ6Gg-Pxn%6z9zQuJ@S7OJyv7Rswb(=qP#eSTQ3B!%b|w@ ztbGOkNG@%}%~pWbrH~VRPPIT=j#|PweqIIkf=8aKuErjy1^Avy@&7{NyXaEwO{Xfmhp>`2E|g{RlYQ47*U?cNkr?$A?y;59R-w z+X`frfv8@2tUY2g6gggUhO?48;a^ zVB7Fzn}*-*!pAds7Ik&)ZL1&3_D2^-kk@u_65RGYeGM4fAD>cPPxJ4|b&aVv`M9>5 z`8dT#_@r`b_?%6{W)~pCEyUfCtYay%BUiUW(cwyb>?pDFU*sRNv5C{vk%sfx`RL+j z;`l?@BmD;EB3x$}x!^?X_AqrS#RB=DaQn_|sh?aN zW(eGPFKd%bggtrro#NUk?k}7_LJs>FIuU0(0sf5QS;Q94Cr5L?BH+v|zWY$2XFP^q zhgXc^e!^sO4f{0fAhsHE1N%JxDn4W*uNebB<^DcKKM7vKU2QJxd^9{%W7UP8#!tm% zvJ>CO@!f*_KK^)H`!efFRL>Zg_7b+1nYg)?wf83`v({NrSoQ z4_*54L}4GYBEJu&kPC>1wH;SL=OxTc?779-)^&92v(%$%hg|(CLmUIV zvuW&)2uGB&U}HgVbWI**)6linrM*>rJa_^B{{>9wpHMf7Y`iabYqQFqg%LjV=yAU{DVVtCjE8=gsCMjY{VfLU;r_5&OGocdjGzEgT~2DYTV zcg5le7;MJF|2nVh9#y{j0$AN`#ZB#tsXy8FVBfzXcsGhVZ|AhM=u~_e8@GK|-1K{M z@BrI~vqHj2#YbcnW(OZ8;tTNosPedn;l{&G_Vnk}$iv0`qQXgdmAGPrnvKrNSqNKD z8u`3REMrePauuw52%X+Fb8P?JpRANCSm00r}+>d_2Xalax4(l|Vm?}&MYi*S; z4`zsOfIG_RBC6l$yOrT!`I$d?tgSiX`N((p)pT@Sg<3}`>{+q3E*wm`6?-1G`rcF8 zThzXbtqu>=e5iTZN`sn5WrqP=j^3uw;4giVbB!L?{Mo%nkV!-GS>l+^^m*d-9wl2fi}^->C;@rZ!^8FMV820MBb<+m+#r!n7*bYF*x?CN9BTZB2R+ zJ^%}p8&n~u&>H!UtwZhQ<)n3{A#s-aZ4iO0QOi?LNuW5C%-X5(2Gpv&Y{`Fp4nC4K zmZM&vT>eFPV`bIuSl5gAW(|Bo@%weSldgeLW+pa6{-E=LIuqkL0Q}DHwUj5mMbIO@ zt>4(N6?44mzI+vw7qdxCzBO1p) z_h;}qa>IXAV+{5aQ@UWc?8^k$tm%mFQ-k&A7VpBAaiTr z&LER%TqidU@GbK$n3j_{Z^UNcG=4r$VH+P{OWCn!TORv>{`CTem`CO40~5EhnWBtT*gNxUxLA9T*r9B#KEqCAnD)tsfyIgV$_hmVX}9KA>^026e6G6FaEb)wJBn24VBH1S~D zFHc3Pa38II9X2Mcu8lpdqyK?iUOf(q&8eBW=l$R{x{PXGs(;NTehG&w;R6erO;kRs z^K&|EVm$4xBfOi%s+A+PaaeMd{xI&y4u> zmLAu_PtnCa=zi;*r6KXt%W5O%;J#TnVq;BCHph@QZ {b6oXDTUg(I1-IX|K~(0{Kz16LTECs*=4t0l3!_uyFH z&_^o0EJyz2Pa*lG>hWf6mg1rOaT)9XjM(}C@p>LMMc;tVd>MkO>ikYTHVO~h#JUPF z-cIZxC-didl`nlttT}~k!p(fW`X}&w1#81DTo!EXbujTf{iT_h1AUP?-{|BQlhM%) z`j%xoGsq#YbB>?}y!r|5f0Hu^#aK63q%$fmmCwtb*~3&HxamipaE^ZI=3u_^{jBu# zc0gAB;CP%dP`|78pKLDr_Is1BfgQptC;t_8!FT-{-yvS-Jl@vp7g?*0ABuA-)?|kB zD87?#tKTamxA3*ULQfwL!F!!^*PcJPmX48asR=c zzE<90{y@LtW;>AA&GS>$SLu6IZnoPH#~;-h)#sqTQ|*VS-X6Y&E9&0mkMniUmZEF9^31@yFS7Hy|$!B*E(=*XyFQ3>*Jk=Q=)mZkBKW5hG-rzS0Y?GfV25H9L3)`yH+_vi$+y|Epg8uGHdoI9unmf*+gV|LAvl_PGJq6c30U(Wn> zzN0Ywd=c@BeL9x}S6NE_RgupNtLDRV=-YEcz#G+V*^|{dCi4b)3VZARJn{tiaM?e$ z`16>m5hZb^06Q?-)C++>io0M>wko~K@)2Dx%lKRDQ@J_V8m5zze+i$g3|?(QU%QCW zH8~?MZoh+CWks+{_OOv0wj#DiJXdW(Jq*!V*oN}l>YQQ!2w5p#pg$mFeTp-xLFufz z_Pez9P;r5L-WKb2&c8F^8uu^39@Z52`Cr87QsB}yxXLxSO}=Im!yP)OgYCg{Le(NJ z!SiJ^m;ULIQEzT|4Nl4)lr0S|Q9sm~A@%z@IF0)8)KlSciQXCYLg>7?_M`ln8nD`C zfrpf#=e+ysa>5K-0bRFc{-@DJW#lQ&Of1)#7B?7wKhHc%()T_HyS+-S5xed_LIzI%*ZwJhh|Ud6_U^OBpJ1m2&(#|zRo4!_o(qs}?_^R_48mU*$g zsl>It=!l+Lr`|5vwyuu{FR@iy1p9%%n6J1$Jj#i~#^WP3`;~U&7wSp2wUI*!Z23=g zN?(C12qydnKS^c|xxtxR=)DPNOyCUuOkXN7NU~K8AIuB)5i7BM+jwwiACE4x#^#(+ z75Dy;JkB5!@&|Jgu1KBAV=^4XHsw4{9P&}EqYdY~@i9MtcxZSC-#Nzp$@^R>utfW1 zojEgA4W4lVEbWYK*M$EN_iQIHGdDct0Qtz>$hj=`NzAeL5O=gkxD&nK%^7pn=j$+i zux)si&CWfWVpo^N8OgsawzQReNawjY%M_|6M31P2FNd$euk+y7^*JvtMsoHRqi9*Rd{Qx@0rgG=9lB0 zB>1fQGHgB03oOSL=)qQfI6Q=|be^sTvEWD6qCJbMoG~Hx$tTnwAHv(T->BNAD?;4h z?1uV#960OGJn7+a#1y|LQVCpY!yGxwqnyZ>lU*L#%T z7k&h0a9+uFhR=xam*Z#7!G*K1P1bFRIUzZz?@eMaAUr~i3T)H9KsbV0LIM7^N6y%@ z@28?p^3NF@aIy~^48E(J)xt02gYsS53ao>R*e2AUJ90jhSgA97s$+xR*IwA=E5lE^C~)QgHAcW>en0LcW|`uSLDkM=IGZS2lv zLnjvOtvIQr;)`vJOo@Twu{sx^^NQhTl|k729b*%^@rDRD7t)w$d)}emkZsouVNF? zk^BpL7oXJoF%&a(t~nG>(fJqRQ>c8j7P&p~FWi7U=mpT;d`MoY{VVmoMWrL^P}&m> z!A78sb4{$JFS>5VoJv#2n+8s|=6NMnm-A<5+HswI2jw-k6IjIlMO3xw4)}Iudd`cY z!>(Xs1@ewe#Lr&nxAfHE(J9zcALL0cA1X)h14a?c!U_CKtyJ+boV0DZ9f0rg+{ilF z5jF~@sJ3aFv+n>lMe1WC}`m0Y<@8XHbkLWK`|B7mH>SMLJ$p!kdH4pxPLxklXQW+E$17>i`3Vt8jCo&Y}S$shBNVr{9q9n8oY!2 z=##Js?D;++-%)=C`8eF$HbTeTH;_-&V-D(P%gVe0)}9SslL`4zi?uo78j=%uV6zeb z$)SZ4{yc`{+<-U>hUpB9zfT-Ct(d?*t&N9+)MQ=_$vv)62WpN?TJRn90QJM>o3UQy zYH$wQTKfn1fVi1r406-Hus&mMP-i&B+_cAajateTd>XFdd(z^pAajR5# z7+9;lUB7?$19dI=J9E{$L9};l>r-3U35P{bzK7+z$z6RPLT&O&_5q$p`@0F6TA!ofK#`6%)6stEZ+=7h}o7LmwIZq4f z5v(cv82z*%r@hE~$tr^ZZCNvM%=>a1Y&6S1@6NKF$jknOa|)L_qi5aoS>zA@(s`Tk zZtO?v%(djC$IPzaU>4p5B)RoOUfGYOs17EaS1&uIin_ih<=~Aof$X^hdDkV*f@uz| z$>p&9KzP*?&Uby*Y>)rF;J)x1e^!e<3gvf!MPFTr!Q`aDLU>CL){~jNlAgq=0mMA& zoB{cW_Sw`c7P-vt2XJ4`@O$4MfPCp+QD1@jJ8T~IP~;2jOW4AEW;yccCEue?^$dQ{ zT{XaCDc-KS;aBQ+#rJGa@RqaKe!rM{rtYJUjpEJw> zziWdpm8S1P^6Q6vo=;C!E!_^r4~XaL>#%+BmDB8}tigu6Q(O3zb?p|G;(ydz{CT$_ z)TH3bevhml`3W{@axR!i!K z6R}HbgzB#`lc^=OU=9)NZ5B0ycIYS-S-ylnOKz>W{wTG91mX#?(!am2Dt-s&F@xB% zQ>^KMExZ?bM0H5p9X-zG3}zYR*`3dYQ^knYfz2`_>sd22CvoyMcn5#Wo>RkRD z7MP~K@2EHfb|RUF!esT8tY^ zUiG!gp4Id9ZCYB$K8yC>d>v15Y7q4eFh~1P%Ab_iMYWb;;K@(;wKxxbOzQXdG5rB- z?-CsBW1e*{G6A0Jv;cGsgG%48^|V**6@AobSh0z->Q%n6(i@ z{e6ZbQ~FSSZbhwC@hKdDjXeZ!BS-S<8p?g)>)JcB58w~Pzi>Qrz7L(VhaFbab?WRp z;33U`OM3s?+WS zZ=V8VrK6+N+a5sIOU#(?H|&tTO@GdpT-c7lX5VCuJ6MNmsgsDmuSemRUlROUBg-@u*)+vVE1D+G}{X6~O zg#Mjo<+!IX{BGWZCvF91`gf68u#kM+zppGAZ0I~Yd*1F!}I>?(pX&yl1QnSly0#-y&)P zV1R8)jo`y$DXusA?Lyt3dbGQjc+{F2);#jcR@iwfu;e*vu&Knm2)e`n+z@1_`8-a| zXE->(zOMhw80@npesnK+*;v*~&zhI>WcaUQA3or~8joZ5@J_JlPgkgan*_Ab%n>}AB*|>NTrNOeOl!i;0)&Z_Q9lOd$?-xb2(OkxwPwFDedC&ZIqNpUc$2caW3z z{%jrmx(a^Ol$@Xna;SlA*Tz1}a-BTIzYhd?8S;5Ihwy^kEcEb}K9(RKPPg@$dE_=MCUL?jhFL5qys4og~76E^xpr$b|iS+ljqjIKO}I zM_cwPiPtU%Sc<&GQylncd!sw}toJqQjyhlN1n7C2)Wmp7xpjyJe^=2jGXB z3Wo6h3gwP&E;fcd18mN{0cI+PUIr%21P97=*dE6p|IVCJd%jRU>Shv4wUY&hNjZ_h+~-*rWSn zP;>3Iy7l-C^>e-N*?kUXA4VqdFSnlOe*edoRN3%u;>>|!)AarZ_Z9K^=;J$8qjF!v z?S3t_GOG3Rd%g1}ti*c1z*jz_FDa2dm(StRfOPaJ;@v*>GPJJ<|8hss^Vh`iSCBb% zZtY7e=DQQ%ycSD}(J_m29MXNvEZ2{|ab{4MWSdVHC-pAkPJ_+2vQWEd0 zghRoVw1@9>R#a!gzr6p;=F$cR~+|A zW4+v8XMy#uY{$C&nc`;P629%nl_e8!I}sVa!|#xh3qvmR{x{FPAyeu*McR;{}#L`2R_4C?+;YH!C$|E z?=W^7i6dq8J`?s$K7vay))@u24eY%$X5ZS@|lN0;*0aAks;8HHib>3;F z{L+b6Yn%jD>v{utAbTdhwo{eb(j4+`)~)y92-E!aWPIdV{H`!EzDlk}j_#@x+clmD z9_spe@=kJ1$M4iHs~Vdh|H!+#g|WJh{rT|%<=Vs!`R@$uN8=CiLD|fm@yF;_{lx=V z`*P}L8jqw7uj{HCbYV|w7PvPMeWww>Sic`X28QT*UwnyNB?JrXgZKt^u6GFgad+P7 zt?Q%NzoD+HUJKhD!0tv|u8({)CQkb6s#i3J|6$*@0r=7d|JInkKfN>AkE!YTv8C=_ zl$gSEjOqp&Ux(M$U|n~{<>8@wh!=Z^i};uyW7kI4za=02kiH=Hz5V+Oh_OxT6@M{`xn-Ta6pQKQ(?4{)0T!C*dB318BS$zkUszxifwR-cHV?oW(ssywEtE zSfJ~V!F%Ze*BMOPlb!(0Gdny}W8%5Lu3VM1_&VX;e3x1&>1b`M@i=0w?x{Kh9E?=F zpWYL0Ci8P;@J(at0seYPa?F>J&%Ml_^WAz^i686zmQTYobUhK-Ydi?OQ#;qYJ9L)H zj~9`Db^zaWUE^_J%LTs6zPRt*(0C~_)%EMd26}2`fBrqdYq7;=;A8Cb``!_*>l~JU z-^YE_AILmwkN;`5%;x=vkFd5E zvG2L$AmgYVYP<+Ouj`|b!F2kFs^J5#Fwc*`3XR{OhO+|xpz)K$DUIRl{yjQd;Y-8= zKi>Uv8K?F10)sTB4&u*!A|qj-AD`q|T=#X~0~sSSi1DhUm~V*jevXWZ>;Agdp!l!c zQ)fgDVngIJe*7bI()CW%b@o$({Q{ozIGp?!`U&9*e$2Zc{Pj_M_Bc2zKCCk?C*kxO zD@Qm?o~g0U?rSW26K_Aw`Uk+HU1s@)AgVE znf(HT-P@e_9lXhp^YH0qta#4C3d>yqoH5B&yd|g!IO2{fNb*+NfYia7z=wrIXhA z>)PWMcPxv{t5K6;kIV?uG-j{eU$1z3J+1j{QSz4J*n!6E#reKvjp=RiW3bF>pMxF% zy&Kezldy{=N$c*63Egqxy0!%Sz0?-{IEfri*Xj4sJ0+d)5qq=57`-RikICEo^$*Fn zspmWPOU&0`0kU>K5+^lg{{H$=`d)b7hks{XHeyC);yreveLq{BeIoAf&*7G5o~?;% zb$OQR1sZFQn0-kW7)T^w?oIJsbh>I2dBqJIdk{{brOxK#rW&=(cAj?{cL1Q z|BLqegF)y{W1Yd&^#;W9W~$Fp@4ShQwCBoLJ=c1tvA@n3Q#AegcOz@uf;gh<>aXCPD*iq23&=$qlDoZ6uBUMmdRO>Y`v8{S8>8B36*x`z z+vA$7w=*`T>*e838morD|F6W?)88Z>X*>Ymd4{>)9tXq#y*t^Tt)|A|$Nyq4Sl1g- z;}9QGzA}$~4EU;l$Ck#u;HkPkjoi4?<|)2UNxhm~$>qffHSR$^5{FGH_vr|)+{8Na zQ~HyTE_fJY-(MuZ>)U2kRQppJ zbH>N_b=(>E!v-qLwpJAiv+*pAb)H4nYv9|QfAPH~x!6nSMep$z;)TY28+G*8v!RP_ z^g{eZ?c^N3Or6@1)B5oUYU8^8A7nIyeTs4Lwhi3({u^i11LMc!0RH-i$S|$dySR1i zPfP+a3B)82lR!)YF$u&Z5R*Vm0x=21BoLE8Oad_p#3T@tKuiKL3B)82lR!)YF$u&Z z5R*Vm0x=21BoLE8Oad_p#3T@tKuiKL3B)82lR!)YF$u&Z5R*Vm0x=21BoLE8Oad_p z#3T@tKuiKL3B)82lR!)YF$u&Z5R*Vm0x=21BoLE8Oad_p#3T@tKuiKL3B)82lR!)Y zF$u&Z5R*Vm0x=21BoLE8Oad_p#3T@tKuiKL3B)82lR!)YF$u&Z5R*Vm0x=21BoLE8 zOad_p#3T@tKuiKL3B)82lR!)YF$u&Z5R<_FPYGoGoO&!T+48#SH&%PX3$8 zkK*Dqj*E2M^9BF-aa>&HpZz#^Fm&+Qd5`sdI8>F36&;ov*J*T4U_pa1)_{`31jE&9J-`#=3`H^s#Tvu4fK zY;TM7pO2aKv-D^7oiCZb{Eu<@S%2?byZ!GSsq*8{|N7O?)C%9_Y*zN4D~0~+7g?I| zKhWmS9knvdzSCd%|MwpWT=&28H@?-Qy&rE`G_9)4N`EcC)ZhQtKkkLNIGK_D+_{&v zcSiqczRv9*?<_day`x5rynooxRezZy1j*pPpZzJEw^+Wo^u4%M%2 G|M_31W`WrN literal 0 HcmV?d00001 diff --git a/testsuite/MDAnalysisTests/data/tprs/all_bonded/dummy_2024_4.tpr b/testsuite/MDAnalysisTests/data/tprs/all_bonded/dummy_2024_4.tpr new file mode 100644 index 0000000000000000000000000000000000000000..03b95c151b829a4096550f0d62ea60333e282ca5 GIT binary patch literal 5188 zcmeHLO>Epm6!vbiN&m8igoKtqMf`{g$yJ&b8h(;>B62{YO+{8n2yt-iS?{Q8dt-Z> zCZ|Y9a6y1T98&cJCj>VRh*KgYZpoosB2M(kje^7t3EpSVb7oEWiL`|r80q=_&YL%H zp5J)aFpQIiVN_q8e`WEdh0EiU6O$LtULc8K^b@>A@DRZfg270%gdGbajVeJ2gc_iA zfLQ5AZd6uwD)O@fmwwjuJ)lRGkN*1nVqMWCCd&15N>r}T=G0_PozJN(5r$E& zKb^~XE9LsdTsD6(%Mi)y5XlP-)x&n!3cc0wM8t`5FtE59C&E`; zWg%&bNHP6UW_B;}$oInpPbeMuGC>~!&ay%>ZM?83yCp8(-{#&oSAv&%22Y8vFStacMvK z-IQ4>7UXP;=&f~n(69)wZc+YsvCy=14}{bCI9nZ{hmJoDb z_cCx8@$_(A;?* z$B$+Y9mftD&7XP11Q{*^jt_Ce7cK`3JtoL>9&;hn=VGt$kBdcL8#a*Rvf*6u1qC@S zAkG9wKPM6o9Inn1k8+T`4Gedud)9uR|0rvYJ6821A6OUf@G5_6zoy*vN0_0ibd)~OFW zhnYUmrVgobDg8D=aGUFCh;I7K9w;Yc`sM8%LQ3BE1JNW;G1O{w^R+8Wm%jb!(_isT z3%`KcJKH<>PEZrl(yWnuKc3~l)4zQE9vdm#fAPt0K7a1b;jkblR$C#Z+0!2_dn=i` z^(C5ry5=T*-T%obSzGkWWMkt7^pm<#?W+xpQ~`PIwT&AvRL1-!LxrrWp`Vuf!ehI~ zX2&y){?hf&W&x!>kf0x5|L%L`pjU|F15RO1kKh2SY_4ZX)DP0K_8g#9PAHZat=jE& zOA6O)gpniS1yZAtVN~g#0w;;2V>YB{xw@)!9UQt9MtOdskx}Wh68gfkbxAqtHi)I? zcNvscd+cwmxBgDMB8X+OYI+^%QnF&{cUpEL!vO2>l`vpCiM%G3I2RQ!3Onssv4ia> zOoR*+J4`-g%Z}jHr+;KR^e2E)4ap!7kxBM$$4QiM)J{U*jN6g0T^V@hijZD2;VG3) zK#r7YH9bqy34=JXgQRgexOzsZ9`)=_97{VelgJKYGVJmx-K5!=$+{M1O#P)fdGLfO z1Cx@G$aYJ_X4o)SQYY5BT9Iz_NHjzw0!Nsf#)fQ(!1nicoSU5WBS+2bqwrgb=5ieg zC}O)M*Vva7@e(V?Y}an%1hQf_V}~pQFF%jR_t%?=^u=Dbw8&4yP1#7yYxEj~5vQ8> zHJWEIEd

  • 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 DT|y*T literal 0 HcmV?d00001 diff --git a/testsuite/MDAnalysisTests/datafiles.py b/testsuite/MDAnalysisTests/datafiles.py index 9a63d33716c..3d3d7d93129 100644 --- a/testsuite/MDAnalysisTests/datafiles.py +++ b/testsuite/MDAnalysisTests/datafiles.py @@ -86,13 +86,15 @@ "TPR460", "TPR461", "TPR502", "TPR504", "TPR505", "TPR510", "TPR2016", "TPR2018", "TPR2019B3", "TPR2020B2", "TPR2020", "TPR2020Double", "TPR2021", "TPR2021Double", "TPR2022RC1", "TPR2023", "TPR2024", + "TPR2024_4", "TPR510_bonded", "TPR2016_bonded", "TPR2018_bonded", "TPR2019B3_bonded", "TPR2020B2_bonded", "TPR2020_bonded", "TPR2020_double_bonded", "TPR2021_bonded", "TPR2021_double_bonded", "TPR2022RC1_bonded", "TPR334_bonded", "TPR2023_bonded", "TPR2024_bonded", + "TPR2024_4_bonded", "TPR_EXTRA_2021", "TPR_EXTRA_2020", "TPR_EXTRA_2018", "TPR_EXTRA_2016", "TPR_EXTRA_407", "TPR_EXTRA_2022RC1", - "TPR_EXTRA_2023", "TPR_EXTRA_2024", + "TPR_EXTRA_2023", "TPR_EXTRA_2024", "TPR_EXTRA_2024_4", "PDB_sub_sol", "PDB_sub_dry", # TRRReader sub selection "TRR_sub_sol", "XTC_sub_sol", @@ -407,6 +409,7 @@ TPR2022RC1 = (_data_ref / 'tprs/2lyz_gmx_2022-rc1.tpr').as_posix() TPR2023 = (_data_ref / 'tprs/2lyz_gmx_2023.tpr').as_posix() TPR2024 = (_data_ref / 'tprs/2lyz_gmx_2024.tpr').as_posix() +TPR2024_4 = (_data_ref / 'tprs/2lyz_gmx_2024_4.tpr').as_posix() # double precision TPR455Double = (_data_ref / 'tprs/drew_gmx_4.5.5.double.tpr').as_posix() TPR460 = (_data_ref / 'tprs/ab42_gmx_4.6.tpr').as_posix() @@ -427,7 +430,9 @@ TPR2022RC1_bonded = (_data_ref / 'tprs/all_bonded/dummy_2022-rc1.tpr').as_posix() TPR2023_bonded = (_data_ref / 'tprs/all_bonded/dummy_2023.tpr').as_posix() TPR2024_bonded = (_data_ref / 'tprs/all_bonded/dummy_2024.tpr').as_posix() +TPR2024_4_bonded = (_data_ref / 'tprs/all_bonded/dummy_2024_4.tpr').as_posix() # all interactions +TPR_EXTRA_2024_4 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2024_4.tpr').as_posix() TPR_EXTRA_2024 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2024.tpr').as_posix() TPR_EXTRA_2023 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2023.tpr').as_posix() TPR_EXTRA_2022RC1 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2022-rc1.tpr').as_posix() diff --git a/testsuite/MDAnalysisTests/topology/test_tprparser.py b/testsuite/MDAnalysisTests/topology/test_tprparser.py index 34461c3d66d..208769bd61d 100644 --- a/testsuite/MDAnalysisTests/topology/test_tprparser.py +++ b/testsuite/MDAnalysisTests/topology/test_tprparser.py @@ -32,14 +32,15 @@ TPR450, TPR451, TPR452, TPR453, TPR454, TPR455, TPR455Double, TPR460, TPR461, TPR502, TPR504, TPR505, TPR510, TPR510_bonded, TPR2016, TPR2018, TPR2019B3, TPR2020B2, TPR2020, TPR2020Double, - TPR2021, TPR2021Double, TPR2022RC1, TPR2023, TPR2024, + TPR2021, TPR2021Double, TPR2022RC1, TPR2023, TPR2024, TPR2024_4, TPR2016_bonded, TPR2018_bonded, TPR2019B3_bonded, TPR2020B2_bonded, TPR2020_bonded, TPR2020_double_bonded, TPR2021_bonded, TPR2021_double_bonded, TPR334_bonded, TPR2022RC1_bonded, TPR2023_bonded, TPR2024_bonded, + TPR2024_4_bonded, TPR_EXTRA_2021, TPR_EXTRA_2020, TPR_EXTRA_2018, TPR_EXTRA_2016, TPR_EXTRA_407, TPR_EXTRA_2022RC1, - TPR_EXTRA_2023, TPR_EXTRA_2024, + TPR_EXTRA_2023, TPR_EXTRA_2024, TPR_EXTRA_2024_4, XTC, ) from MDAnalysisTests.topology.base import ParserBase @@ -58,6 +59,8 @@ TPR2022RC1_bonded, TPR2023_bonded, TPR2024_bonded, + TPR2024_4_bonded, + TPR_EXTRA_2024_4, TPR_EXTRA_2024, TPR_EXTRA_2023, TPR_EXTRA_2022RC1, @@ -135,7 +138,7 @@ class TestTPRGromacsVersions(TPRAttrs): TPR455, TPR502, TPR504, TPR505, TPR510, TPR2016, TPR2018, TPR2019B3, TPR2020, TPR2020Double, TPR2021, TPR2021Double, TPR2022RC1, TPR2023, - TPR2024]) + TPR2024, TPR2024_4]) def filename(self, request): return request.param @@ -262,7 +265,7 @@ def test_all_impropers(topology, impr): @pytest.fixture(params=( TPR400, TPR402, TPR403, TPR404, TPR405, TPR406, TPR407, TPR450, TPR451, TPR452, TPR453, TPR454, TPR502, TPR504, TPR505, TPR510, TPR2016, TPR2018, - TPR2023, TPR2024, + TPR2023, TPR2024, TPR2024_4, )) def bonds_water(request): parser = MDAnalysis.topology.TPRParser.TPRParser(request.param).parse() From b710e57a64654bed5250eb771f0f27a2dddfeebf Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Sun, 29 Dec 2024 23:00:26 +0100 Subject: [PATCH 48/58] [fmt] Format analysis module and tests (#4848) --- package/MDAnalysis/analysis/__init__.py | 54 +- package/MDAnalysis/analysis/backends.py | 27 +- package/MDAnalysis/analysis/base.py | 221 ++-- package/MDAnalysis/analysis/bat.py | 228 ++-- package/MDAnalysis/analysis/contacts.py | 98 +- package/MDAnalysis/analysis/data/filenames.py | 9 +- package/MDAnalysis/analysis/density.py | 236 ++-- package/MDAnalysis/analysis/dielectric.py | 36 +- package/MDAnalysis/analysis/dihedrals.py | 206 ++- package/MDAnalysis/analysis/distances.py | 44 +- package/MDAnalysis/analysis/dssp/dssp.py | 11 +- .../MDAnalysis/analysis/dssp/pydssp_numpy.py | 13 +- .../MDAnalysis/analysis/encore/__init__.py | 28 +- .../MDAnalysis/analysis/encore/bootstrap.py | 37 +- .../encore/clustering/ClusterCollection.py | 89 +- .../encore/clustering/ClusteringMethod.py | 193 +-- .../analysis/encore/clustering/__init__.py | 5 +- .../analysis/encore/clustering/cluster.py | 57 +- .../analysis/encore/confdistmatrix.py | 291 +++-- .../MDAnalysis/analysis/encore/covariance.py | 101 +- .../DimensionalityReductionMethod.py | 67 +- .../dimensionality_reduction/__init__.py | 5 +- .../reduce_dimensionality.py | 59 +- .../MDAnalysis/analysis/encore/similarity.py | 578 +++++---- package/MDAnalysis/analysis/encore/utils.py | 59 +- package/MDAnalysis/analysis/gnm.py | 110 +- .../MDAnalysis/analysis/hbonds/__init__.py | 3 +- .../analysis/hbonds/hbond_autocorrel.py | 38 +- package/MDAnalysis/analysis/helix_analysis.py | 149 ++- package/MDAnalysis/analysis/hole2/__init__.py | 10 +- .../analysis/hydrogenbonds/__init__.py | 7 +- .../hydrogenbonds/hbond_autocorrel.py | 250 ++-- .../hydrogenbonds/wbridge_analysis.py | 918 +++++++++----- package/MDAnalysis/analysis/leaflet.py | 103 +- package/MDAnalysis/analysis/legacy/x3dna.py | 503 ++++++-- package/MDAnalysis/analysis/lineardensity.py | 117 +- package/MDAnalysis/analysis/msd.py | 85 +- package/MDAnalysis/analysis/nucleicacids.py | 162 ++- package/MDAnalysis/analysis/nuclinfo.py | 478 ++++--- package/MDAnalysis/analysis/pca.py | 221 ++-- package/MDAnalysis/analysis/polymer.py | 95 +- package/MDAnalysis/analysis/psa.py | 10 +- package/MDAnalysis/analysis/rdf.py | 209 +-- package/MDAnalysis/analysis/results.py | 25 +- package/MDAnalysis/analysis/rms.py | 322 +++-- package/MDAnalysis/analysis/waterdynamics.py | 10 +- package/pyproject.toml | 1 + .../MDAnalysisTests/analysis/conftest.py | 59 +- .../MDAnalysisTests/analysis/test_align.py | 468 ++++--- .../analysis/test_atomicdistances.py | 33 +- .../MDAnalysisTests/analysis/test_backends.py | 38 +- .../MDAnalysisTests/analysis/test_base.py | 419 +++--- .../MDAnalysisTests/analysis/test_bat.py | 76 +- .../MDAnalysisTests/analysis/test_contacts.py | 353 +++--- .../MDAnalysisTests/analysis/test_data.py | 12 +- .../MDAnalysisTests/analysis/test_density.py | 267 ++-- .../analysis/test_dielectric.py | 16 +- .../analysis/test_diffusionmap.py | 94 +- .../analysis/test_dihedrals.py | 214 +++- .../analysis/test_distances.py | 157 +-- .../MDAnalysisTests/analysis/test_dssp.py | 25 +- .../MDAnalysisTests/analysis/test_encore.py | 1127 ++++++++++------- .../MDAnalysisTests/analysis/test_gnm.py | 147 ++- .../analysis/test_helix_analysis.py | 498 +++++--- .../analysis/test_hydrogenbondautocorrel.py | 332 +++-- .../test_hydrogenbondautocorrel_deprecated.py | 361 +++--- .../analysis/test_hydrogenbonds_analysis.py | 524 ++++---- .../MDAnalysisTests/analysis/test_leaflet.py | 80 +- .../analysis/test_lineardensity.py | 220 +++- .../MDAnalysisTests/analysis/test_msd.py | 143 ++- .../analysis/test_nucleicacids.py | 14 +- .../MDAnalysisTests/analysis/test_nuclinfo.py | 201 ++- .../MDAnalysisTests/analysis/test_pca.py | 161 +-- .../analysis/test_persistencelength.py | 20 +- .../MDAnalysisTests/analysis/test_rdf.py | 38 +- .../MDAnalysisTests/analysis/test_rdf_s.py | 58 +- .../MDAnalysisTests/analysis/test_results.py | 15 +- .../MDAnalysisTests/analysis/test_rms.py | 425 ++++--- .../MDAnalysisTests/analysis/test_wbridge.py | 901 ++++++++----- testsuite/pyproject.toml | 1 + 80 files changed, 8817 insertions(+), 5258 deletions(-) diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index 056c7899826..804f9dbf24c 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -22,31 +22,31 @@ # __all__ = [ - 'align', - 'backends', - 'base', - 'contacts', - 'density', - 'distances', - 'diffusionmap', - 'dihedrals', - 'distances', - 'dielectric', - 'gnm', - 'hbonds', - 'helix_analysis', - 'hole2', - 'hydrogenbonds', - 'leaflet', - 'lineardensity', - 'msd', - 'nuclinfo', - 'nucleicacids', - 'polymer', - 'pca', - 'psa', - 'rdf', - 'results', - 'rms', - 'waterdynamics', + "align", + "backends", + "base", + "contacts", + "density", + "distances", + "diffusionmap", + "dihedrals", + "distances", + "dielectric", + "gnm", + "hbonds", + "helix_analysis", + "hole2", + "hydrogenbonds", + "leaflet", + "lineardensity", + "msd", + "nuclinfo", + "nucleicacids", + "polymer", + "pca", + "psa", + "rdf", + "results", + "rms", + "waterdynamics", ] diff --git a/package/MDAnalysis/analysis/backends.py b/package/MDAnalysis/analysis/backends.py index 38917cb2ae7..0cfad4e1c31 100644 --- a/package/MDAnalysis/analysis/backends.py +++ b/package/MDAnalysis/analysis/backends.py @@ -35,6 +35,7 @@ ------- """ + import warnings from typing import Callable from MDAnalysis.lib.util import is_installed @@ -102,8 +103,9 @@ def _get_checks(self): checked during ``_validate()`` run """ return { - isinstance(self.n_workers, int) and self.n_workers > 0: - f"n_workers should be positive integer, got {self.n_workers=}", + isinstance(self.n_workers, int) + and self.n_workers + > 0: f"n_workers should be positive integer, got {self.n_workers=}", } def _get_warnings(self): @@ -183,8 +185,8 @@ def _get_warnings(self): checked during ``_validate()`` run """ return { - self.n_workers == 1: - "n_workers is ignored when executing with backend='serial'" + self.n_workers + == 1: "n_workers is ignored when executing with backend='serial'" } def apply(self, func: Callable, computations: list) -> list: @@ -307,10 +309,12 @@ def apply(self, func: Callable, computations: list) -> list: import dask computations = [delayed(func)(task) for task in computations] - results = dask.compute(computations, - scheduler="processes", - chunksize=1, - num_workers=self.n_workers)[0] + results = dask.compute( + computations, + scheduler="processes", + chunksize=1, + num_workers=self.n_workers, + )[0] return results def _get_checks(self): @@ -326,8 +330,9 @@ def _get_checks(self): """ base_checks = super()._get_checks() checks = { - is_installed("dask"): - ("module 'dask' is missing. Please install 'dask': " - "https://docs.dask.org/en/stable/install.html") + is_installed("dask"): ( + "module 'dask' is missing. Please install 'dask': " + "https://docs.dask.org/en/stable/install.html" + ) } return base_checks | checks diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 4e7f58dc0bd..675c6d6967b 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -159,7 +159,12 @@ def _get_aggregator(self): from ..core.groups import AtomGroup from ..lib.log import ProgressBar -from .backends import BackendDask, BackendMultiprocessing, BackendSerial, BackendBase +from .backends import ( + BackendDask, + BackendMultiprocessing, + BackendSerial, + BackendBase, +) from .results import Results, ResultsGroup logger = logging.getLogger(__name__) @@ -288,10 +293,10 @@ def get_supported_backends(cls): """ return ("serial",) - # class authors: override _analysis_algorithm_is_parallelizable - # in derived classes and only set to True if you have confirmed - # that your algorithm works reliably when parallelized with - # the split-apply-combine approach (see docs) + # class authors: override _analysis_algorithm_is_parallelizable + # in derived classes and only set to True if you have confirmed + # that your algorithm works reliably when parallelized with + # the split-apply-combine approach (see docs) _analysis_algorithm_is_parallelizable = False @property @@ -301,13 +306,13 @@ def parallelizable(self): :meth:`_single_frame` to multiple workers and then combine them with a proper :meth:`_conclude` call. If set to ``False``, no backends except for ``serial`` are supported. - + .. note:: If you want to check parallelizability of the whole class, without explicitly creating an instance of the class, see :attr:`_analysis_algorithm_is_parallelizable`. Note that you setting it to other value will break things if the algorithm behind the analysis is not trivially parallelizable. - + Returns ------- @@ -325,9 +330,9 @@ def __init__(self, trajectory, verbose=False, **kwargs): self._verbose = verbose self.results = Results() - def _define_run_frames(self, trajectory, - start=None, stop=None, step=None, frames=None - ) -> Union[slice, np.ndarray]: + def _define_run_frames( + self, trajectory, start=None, stop=None, step=None, frames=None + ) -> Union[slice, np.ndarray]: """Defines limits for the whole run, as passed by self.run() arguments Parameters @@ -362,10 +367,14 @@ def _define_run_frames(self, trajectory, self._trajectory = trajectory if frames is not None: if not all(opt is None for opt in [start, stop, step]): - raise ValueError("start/stop/step cannot be combined with frames") + raise ValueError( + "start/stop/step cannot be combined with frames" + ) slicer = frames else: - start, stop, step = trajectory.check_slice_indices(start, stop, step) + start, stop, step = trajectory.check_slice_indices( + start, stop, step + ) slicer = slice(start, stop, step) self.start, self.stop, self.step = start, stop, step return slicer @@ -388,7 +397,9 @@ def _prepare_sliced_trajectory(self, slicer: Union[slice, np.ndarray]): self.frames = np.zeros(self.n_frames, dtype=int) self.times = np.zeros(self.n_frames) - def _setup_frames(self, trajectory, start=None, stop=None, step=None, frames=None): + def _setup_frames( + self, trajectory, start=None, stop=None, step=None, frames=None + ): """Pass a Reader object and define the desired iteration pattern through the trajectory @@ -417,11 +428,11 @@ def _setup_frames(self, trajectory, start=None, stop=None, step=None, frames=Non .. versionchanged:: 1.0.0 Added .frames and .times arrays as attributes - + .. versionchanged:: 2.2.0 Added ability to iterate through trajectory by passing a list of frame indices in the `frames` keyword argument - + .. versionchanged:: 2.8.0 Split function into two: :meth:`_define_run_frames` and :meth:`_prepare_sliced_trajectory`: first one defines the limits @@ -450,8 +461,8 @@ def _single_frame(self): def _prepare(self): """ - Set things up before the analysis loop begins. - + Set things up before the analysis loop begins. + Notes ----- ``self.results`` is initialized already in :meth:`self.__init__` with an @@ -477,9 +488,13 @@ def _conclude(self): """ pass # pylint: disable=unnecessary-pass - def _compute(self, indexed_frames: np.ndarray, - verbose: bool = None, - *, progressbar_kwargs=None) -> "AnalysisBase": + def _compute( + self, + indexed_frames: np.ndarray, + verbose: bool = None, + *, + progressbar_kwargs=None, + ) -> "AnalysisBase": """Perform the calculation on a balanced slice of frames that have been setup prior to that using _setup_computation_groups() @@ -504,7 +519,9 @@ def _compute(self, indexed_frames: np.ndarray, 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 + verbose = ( + getattr(self, "_verbose", False) if verbose is None else verbose + ) frames = indexed_frames[:, 1] @@ -514,10 +531,11 @@ def _compute(self, indexed_frames: np.ndarray, if len(frames) == 0: # if `frames` were empty in `run` or `stop=0` return self - for idx, ts in enumerate(ProgressBar( - self._sliced_trajectory, - verbose=verbose, - **progressbar_kwargs)): + for idx, ts in enumerate( + ProgressBar( + self._sliced_trajectory, verbose=verbose, **progressbar_kwargs + ) + ): self._frame_index = idx # accessed later by subclasses self._ts = ts self.frames[idx] = ts.frame @@ -527,9 +545,12 @@ def _compute(self, indexed_frames: np.ndarray, return self def _setup_computation_groups( - self, n_parts: int, - start: int = None, stop: int = None, step: int = None, - frames: Union[slice, np.ndarray] = None + self, + n_parts: int, + start: int = None, + stop: int = None, + step: int = None, + frames: Union[slice, np.ndarray] = None, ) -> list[np.ndarray]: """ Splits the trajectory frames, defined by ``start/stop/step`` or @@ -566,7 +587,9 @@ def _setup_computation_groups( .. versionadded:: 2.8.0 """ if frames is None: - start, stop, step = self._trajectory.check_slice_indices(start, stop, step) + start, stop, step = self._trajectory.check_slice_indices( + start, stop, step + ) used_frames = np.arange(start, stop, step) elif not all(opt is None for opt in [start, stop, step]): raise ValueError("start/stop/step cannot be combined with frames") @@ -578,23 +601,27 @@ def _setup_computation_groups( used_frames = arange[used_frames] # similar to list(enumerate(frames)) - enumerated_frames = np.vstack([np.arange(len(used_frames)), used_frames]).T + 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") + 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) def _configure_backend( - self, - backend: Union[str, BackendBase], - n_workers: int, - unsupported_backend: bool = False - ) -> BackendBase: + self, + backend: Union[str, BackendBase], + n_workers: int, + unsupported_backend: bool = False, + ) -> BackendBase: """Matches a passed backend string value with class attributes :attr:`parallelizable` and :meth:`get_supported_backends()` to check if downstream calculations can be performed. @@ -642,13 +669,12 @@ def _configure_backend( builtin_backends = { "serial": BackendSerial, "multiprocessing": BackendMultiprocessing, - "dask": BackendDask + "dask": BackendDask, } backend_class = builtin_backends.get(backend, backend) supported_backend_classes = [ - builtin_backends.get(b) - for b in self.get_supported_backends() + builtin_backends.get(b) for b in self.get_supported_backends() ] # check for serial-only classes @@ -656,20 +682,28 @@ def _configure_backend( raise ValueError(f"Can not parallelize class {self.__class__}") # make sure user enabled 'unsupported_backend=True' for custom classes - if (not unsupported_backend and self.parallelizable - and backend_class not in supported_backend_classes): - raise ValueError(( - f"Must specify 'unsupported_backend=True'" - f"if you want to use a custom {backend_class=} for {self.__class__}" - )) + if ( + not unsupported_backend + and self.parallelizable + and backend_class not in supported_backend_classes + ): + raise ValueError( + ( + f"Must specify 'unsupported_backend=True'" + f"if you want to use a custom {backend_class=} for {self.__class__}" + ) + ) # check for the presence of parallelizable transformations if backend_class is not BackendSerial and any( - not t.parallelizable - for t in self._trajectory.transformations): - raise ValueError(( - "Trajectory should not have " - "associated unparallelizable transformations")) + not t.parallelizable for t in self._trajectory.transformations + ): + raise ValueError( + ( + "Trajectory should not have " + "associated unparallelizable transformations" + ) + ) # conclude mapping from string to backend class if it's a builtin backend if isinstance(backend, str): @@ -679,21 +713,27 @@ def _configure_backend( if ( isinstance(backend, BackendBase) and n_workers is not None - and hasattr(backend, 'n_workers') + and hasattr(backend, "n_workers") and backend.n_workers != n_workers ): - raise ValueError(( - f"n_workers specified twice: in {backend.n_workers=}" - f"and in run({n_workers=}). Remove it from run()" - )) + raise ValueError( + ( + f"n_workers specified twice: in {backend.n_workers=}" + f"and in run({n_workers=}). Remove it from run()" + ) + ) # or pass along an instance of the class itself # after ensuring it has apply method - if not isinstance(backend, BackendBase) or not hasattr(backend, "apply"): - raise ValueError(( - f"{backend=} is invalid: should have 'apply' method " - "and be instance of MDAnalysis.analysis.backends.BackendBase" - )) + if not isinstance(backend, BackendBase) or not hasattr( + backend, "apply" + ): + raise ValueError( + ( + f"{backend=} is invalid: should have 'apply' method " + "and be instance of MDAnalysis.analysis.backends.BackendBase" + ) + ) return backend def run( @@ -775,18 +815,23 @@ def run( # default to serial execution backend = "serial" if backend is None else backend - progressbar_kwargs = {} if progressbar_kwargs is None else progressbar_kwargs - if ((progressbar_kwargs or verbose) and - not (backend == "serial" or - isinstance(backend, BackendSerial))): - raise ValueError("Can not display progressbar with non-serial backend") + progressbar_kwargs = ( + {} if progressbar_kwargs is None else progressbar_kwargs + ) + if (progressbar_kwargs or verbose) and not ( + backend == "serial" or isinstance(backend, BackendSerial) + ): + raise ValueError( + "Can not display progressbar with non-serial backend" + ) # if number of workers not specified, try getting the number from # the backend instance if possible, or set to 1 if n_workers is None: n_workers = ( backend.n_workers - if isinstance(backend, BackendBase) and hasattr(backend, "n_workers") + if isinstance(backend, BackendBase) + and hasattr(backend, "n_workers") else 1 ) @@ -798,22 +843,31 @@ def run( executor = self._configure_backend( backend=backend, n_workers=n_workers, - unsupported_backend=unsupported_backend) + unsupported_backend=unsupported_backend, + ) if ( hasattr(executor, "n_workers") and n_parts < executor.n_workers ): # using executor's value here for non-default executors - warnings.warn(( - f"Analysis not making use of all workers: " - f"{executor.n_workers=} is greater than {n_parts=}")) + warnings.warn( + ( + f"Analysis not making use of all workers: " + f"{executor.n_workers=} is greater than {n_parts=}" + ) + ) # start preparing the run worker_func = partial( self._compute, progressbar_kwargs=progressbar_kwargs, - verbose=verbose) + verbose=verbose, + ) self._setup_frames( trajectory=self._trajectory, - start=start, stop=stop, step=step, frames=frames) + start=start, + stop=stop, + step=step, + frames=frames, + ) computation_groups = self._setup_computation_groups( start=start, stop=stop, step=step, frames=frames, n_parts=n_parts ) @@ -822,7 +876,8 @@ def run( # we need `AnalysisBase` classes # since they hold `frames`, `times` and `results` attributes remote_objects: list["AnalysisBase"] = executor.apply( - worker_func, computation_groups) + worker_func, computation_groups + ) self.frames = np.hstack([obj.frames for obj in remote_objects]) self.times = np.hstack([obj.times for obj in remote_objects]) @@ -911,8 +966,9 @@ def get_supported_backends(cls): return ("serial", "multiprocessing", "dask") def __init__(self, function, trajectory=None, *args, **kwargs): - if (trajectory is not None) and (not isinstance( - trajectory, coordinates.base.ProtoReader)): + if (trajectory is not None) and ( + not isinstance(trajectory, coordinates.base.ProtoReader) + ): args = (trajectory,) + args trajectory = None @@ -940,7 +996,9 @@ def _get_aggregator(self): return ResultsGroup({"timeseries": ResultsGroup.flatten_sequence}) def _single_frame(self): - self.results.timeseries.append(self.function(*self.args, **self.kwargs)) + self.results.timeseries.append( + self.function(*self.args, **self.kwargs) + ) def _conclude(self): self.results.frames = self.frames @@ -1001,7 +1059,9 @@ def RotationMatrix(mobile, ref): class WrapperClass(AnalysisFromFunction): def __init__(self, trajectory=None, *args, **kwargs): - super(WrapperClass, self).__init__(function, trajectory, *args, **kwargs) + super(WrapperClass, self).__init__( + function, trajectory, *args, **kwargs + ) @classmethod def get_supported_backends(cls): @@ -1045,8 +1105,9 @@ def _filter_baseanalysis_kwargs(function, kwargs): n_base_defaults = len(base_argspec.defaults) base_kwargs = { name: val - for name, val in zip(base_argspec.args[-n_base_defaults:], - base_argspec.defaults) + for name, val in zip( + base_argspec.args[-n_base_defaults:], base_argspec.defaults + ) } try: diff --git a/package/MDAnalysis/analysis/bat.py b/package/MDAnalysis/analysis/bat.py index 9c1995f7ccc..9b08c7dbff4 100644 --- a/package/MDAnalysis/analysis/bat.py +++ b/package/MDAnalysis/analysis/bat.py @@ -215,20 +215,30 @@ def _find_torsions(root, atoms): torsionAdded = False for a1 in selected_atoms: # Find a0, which is a new atom connected to the selected atom - a0_list = _sort_atoms_by_mass(a for a in a1.bonded_atoms \ - if (a in atoms) and (a not in selected_atoms)) + a0_list = _sort_atoms_by_mass( + a + for a in a1.bonded_atoms + if (a in atoms) and (a not in selected_atoms) + ) for a0 in a0_list: # Find a2, which is connected to a1, is not a terminal atom, # and has been selected - a2_list = _sort_atoms_by_mass(a for a in a1.bonded_atoms \ - if (a!=a0) and len(a.bonded_atoms)>1 and \ - (a in atoms) and (a in selected_atoms)) + a2_list = _sort_atoms_by_mass( + a + for a in a1.bonded_atoms + if (a != a0) + and len(a.bonded_atoms) > 1 + and (a in atoms) + and (a in selected_atoms) + ) for a2 in a2_list: # Find a3, which is # connected to a2, has been selected, and is not a1 - a3_list = _sort_atoms_by_mass(a for a in a2.bonded_atoms \ - if (a!=a1) and \ - (a in atoms) and (a in selected_atoms)) + a3_list = _sort_atoms_by_mass( + a + for a in a2.bonded_atoms + if (a != a1) and (a in atoms) and (a in selected_atoms) + ) for a3 in a3_list: # Add the torsion to the list of torsions torsions.append(mda.AtomGroup([a0, a1, a2, a3])) @@ -239,11 +249,11 @@ def _find_torsions(root, atoms): break # out of the a3 loop break # out of the a2 loop if torsionAdded is False: - print('Selected atoms:') + print("Selected atoms:") print([a.index + 1 for a in selected_atoms]) - print('Torsions found:') + print("Torsions found:") print([list(t.indices + 1) for t in torsions]) - raise ValueError('Additional torsions not found.') + raise ValueError("Additional torsions not found.") return torsions @@ -254,13 +264,14 @@ class BAT(AnalysisBase): 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 + 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 ( @@ -270,11 +281,10 @@ def get_supported_backends(cls): ) @due.dcite( - Doi("10.1002/jcc.26036"), - description="Bond-Angle-Torsions Coordinate Transformation", - path="MDAnalysis.analysis.bat.BAT", + 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 ---------- @@ -307,20 +317,21 @@ def __init__(self, ag, initial_atom=None, filename=None, **kwargs): self._ag = ag # Check that the ag contains bonds - if not hasattr(self._ag, 'bonds'): - raise AttributeError('AtomGroup has no attribute bonds') + if not hasattr(self._ag, "bonds"): + raise AttributeError("AtomGroup has no attribute bonds") if len(self._ag.fragments) > 1: - raise ValueError('AtomGroup has more than one molecule') + raise ValueError("AtomGroup has more than one molecule") # Determine the root # The initial atom must be a terminal atom - terminal_atoms = _sort_atoms_by_mass(\ - [a for a in self._ag.atoms if len(a.bonds)==1], reverse=True) - if (initial_atom is None): + terminal_atoms = _sort_atoms_by_mass( + [a for a in self._ag.atoms if len(a.bonds) == 1], reverse=True + ) + if initial_atom is None: # Select the heaviest root atoms from the heaviest terminal atom initial_atom = terminal_atoms[0] - elif (not initial_atom in terminal_atoms): - raise ValueError('Initial atom is not a terminal atom') + elif not initial_atom in terminal_atoms: + raise ValueError("Initial atom is not a terminal atom") # The next atom in the root is bonded to the initial atom # Since the initial atom is a terminal atom, there is only # one bonded atom @@ -330,16 +341,25 @@ def __init__(self, ag, initial_atom=None, filename=None, **kwargs): # If there are more than three atoms, # then the last atom cannot be a terminal atom. if self._ag.n_atoms != 3: - third_atom = _sort_atoms_by_mass(\ - [a for a in second_atom.bonded_atoms \ - if (a in self._ag) and (a!=initial_atom) \ - and (a not in terminal_atoms)], \ - reverse=True)[0] + third_atom = _sort_atoms_by_mass( + [ + a + for a in second_atom.bonded_atoms + if (a in self._ag) + and (a != initial_atom) + and (a not in terminal_atoms) + ], + reverse=True, + )[0] else: - third_atom = _sort_atoms_by_mass(\ - [a for a in second_atom.bonded_atoms \ - if (a in self._ag) and (a!=initial_atom)], \ - reverse=True)[0] + third_atom = _sort_atoms_by_mass( + [ + a + for a in second_atom.bonded_atoms + if (a in self._ag) and (a != initial_atom) + ], + reverse=True, + )[0] self._root = mda.AtomGroup([initial_atom, second_atom, third_atom]) # Construct a list of torsion angles @@ -347,18 +367,23 @@ def __init__(self, ag, initial_atom=None, filename=None, **kwargs): # Get indices of the root and torsion atoms # in a Cartesian positions array that matches the AtomGroup - self._root_XYZ_inds = [(self._ag.indices==a.index).nonzero()[0][0] \ - for a in self._root] - self._torsion_XYZ_inds = [[(self._ag.indices==a.index).nonzero()[0][0] \ - for a in t] for t in self._torsions] + self._root_XYZ_inds = [ + (self._ag.indices == a.index).nonzero()[0][0] for a in self._root + ] + self._torsion_XYZ_inds = [ + [(self._ag.indices == a.index).nonzero()[0][0] for a in t] + for t in self._torsions + ] # The primary torsion is the first torsion on the list # with the same central atoms prior_atoms = [sorted([a1, a2]) for (a0, a1, a2, a3) in self._torsions] - self._primary_torsion_indices = [prior_atoms.index(prior_atoms[n]) \ - for n in range(len(prior_atoms))] - self._unique_primary_torsion_indices = \ - list(set(self._primary_torsion_indices)) + self._primary_torsion_indices = [ + prior_atoms.index(prior_atoms[n]) for n in range(len(prior_atoms)) + ] + self._unique_primary_torsion_indices = list( + set(self._primary_torsion_indices) + ) self._ag1 = mda.AtomGroup([ag[0] for ag in self._torsions]) self._ag2 = mda.AtomGroup([ag[1] for ag in self._torsions]) @@ -370,7 +395,8 @@ def __init__(self, ag, initial_atom=None, filename=None, **kwargs): def _prepare(self): self.results.bat = np.zeros( - (self.n_frames, 3*self._ag.n_atoms), dtype=np.float64) + (self.n_frames, 3 * self._ag.n_atoms), dtype=np.float64 + ) def _single_frame(self): # Calculate coordinates based on the root atoms @@ -384,13 +410,24 @@ def _single_frame(self): v01 = p1 - p0 v21 = p1 - p2 # Internal coordinates - r01 = np.sqrt(np.einsum('i,i->',v01,v01)) + r01 = np.sqrt(np.einsum("i,i->", v01, v01)) # Distance between first two root atoms - r12 = np.sqrt(np.einsum('i,i->',v21,v21)) + r12 = np.sqrt(np.einsum("i,i->", v21, v21)) # Distance between second two root atoms # Angle between root atoms - a012 = np.arccos(max(-1.,min(1.,np.einsum('i,i->',v01,v21)/\ - np.sqrt(np.einsum('i,i->',v01,v01)*np.einsum('i,i->',v21,v21))))) + a012 = np.arccos( + max( + -1.0, + min( + 1.0, + np.einsum("i,i->", v01, v21) + / np.sqrt( + np.einsum("i,i->", v01, v01) + * np.einsum("i,i->", v21, v21) + ), + ), + ) + ) # External coordinates e = v01 / r01 phi = np.arctan2(e[1], e[0]) # Polar angle @@ -400,35 +437,41 @@ def _single_frame(self): sp = np.sin(phi) ct = np.cos(theta) st = np.sin(theta) - Rz = np.array([[cp * ct, ct * sp, -st], [-sp, cp, 0], - [cp * st, sp * st, ct]]) + Rz = np.array( + [[cp * ct, ct * sp, -st], [-sp, cp, 0], [cp * st, sp * st, ct]] + ) pos2 = Rz.dot(p2 - p1) # Angle about the rotation axis omega = np.arctan2(pos2[1], pos2[0]) root_based = np.concatenate((p0, [phi, theta, omega, r01, r12, a012])) # Calculate internal coordinates from the torsion list - bonds = calc_bonds(self._ag1.positions, - self._ag2.positions, - box=self._ag1.dimensions) - angles = calc_angles(self._ag1.positions, - self._ag2.positions, - self._ag3.positions, - box=self._ag1.dimensions) - torsions = calc_dihedrals(self._ag1.positions, - self._ag2.positions, - self._ag3.positions, - self._ag4.positions, - box=self._ag1.dimensions) + bonds = calc_bonds( + self._ag1.positions, self._ag2.positions, box=self._ag1.dimensions + ) + angles = calc_angles( + self._ag1.positions, + self._ag2.positions, + self._ag3.positions, + box=self._ag1.dimensions, + ) + torsions = calc_dihedrals( + self._ag1.positions, + self._ag2.positions, + self._ag3.positions, + self._ag4.positions, + box=self._ag1.dimensions, + ) # When appropriate, calculate improper torsions shift = torsions[self._primary_torsion_indices] - shift[self._unique_primary_torsion_indices] = 0. + shift[self._unique_primary_torsion_indices] = 0.0 torsions -= shift # Wrap torsions to between -np.pi and np.pi torsions = ((torsions + np.pi) % (2 * np.pi)) - np.pi self.results.bat[self._frame_index, :] = np.concatenate( - (root_based, bonds, angles, torsions)) + (root_based, bonds, angles, torsions) + ) def load(self, filename, start=None, stop=None, step=None): """Loads the bat trajectory from a file in numpy binary format @@ -455,16 +498,20 @@ def load(self, filename, start=None, stop=None, step=None): self.results.bat = np.load(filename) # Check array dimensions - if self.results.bat.shape != (self.n_frames, 3*self._ag.n_atoms): - errmsg = ('Dimensions of array in loaded file, ' - f'({self.results.bat.shape[0]},' - f'{self.results.bat.shape[1]}), differ from required ' - f'dimensions of ({self.n_frames, 3*self._ag.n_atoms})') + if self.results.bat.shape != (self.n_frames, 3 * self._ag.n_atoms): + errmsg = ( + "Dimensions of array in loaded file, " + f"({self.results.bat.shape[0]}," + f"{self.results.bat.shape[1]}), differ from required " + f"dimensions of ({self.n_frames, 3*self._ag.n_atoms})" + ) raise ValueError(errmsg) # Check position of initial atom if (self.results.bat[0, :3] != self._root[0].position).any(): - raise ValueError('Position of initial atom in file ' + \ - 'inconsistent with current trajectory in starting frame.') + raise ValueError( + "Position of initial atom in file " + + "inconsistent with current trajectory in starting frame." + ) return self def save(self, filename): @@ -501,21 +548,21 @@ def Cartesian(self, bat_frame): origin = bat_frame[:3] (phi, theta, omega) = bat_frame[3:6] (r01, r12, a012) = bat_frame[6:9] - n_torsions = (self._ag.n_atoms - 3) - bonds = bat_frame[9:n_torsions + 9] - angles = bat_frame[n_torsions + 9:2 * n_torsions + 9] - torsions = copy.deepcopy(bat_frame[2 * n_torsions + 9:]) + n_torsions = self._ag.n_atoms - 3 + bonds = bat_frame[9 : n_torsions + 9] + angles = bat_frame[n_torsions + 9 : 2 * n_torsions + 9] + torsions = copy.deepcopy(bat_frame[2 * n_torsions + 9 :]) # When appropriate, convert improper to proper torsions shift = torsions[self._primary_torsion_indices] - shift[self._unique_primary_torsion_indices] = 0. + shift[self._unique_primary_torsion_indices] = 0.0 torsions += shift # Wrap torsions to between -np.pi and np.pi torsions = ((torsions + np.pi) % (2 * np.pi)) - np.pi # Set initial root atom positions based on internal coordinates - p0 = np.array([0., 0., 0.]) - p1 = np.array([0., 0., r01]) - p2 = np.array([r12 * np.sin(a012), 0., r01 - r12 * np.cos(a012)]) + p0 = np.array([0.0, 0.0, 0.0]) + p1 = np.array([0.0, 0.0, r01]) + p2 = np.array([r12 * np.sin(a012), 0.0, r01 - r12 * np.cos(a012)]) # Rotate the third atom by the appropriate value co = np.cos(omega) @@ -529,8 +576,9 @@ def Cartesian(self, bat_frame): ct = np.cos(theta) st = np.sin(theta) # $R_Z(\phi) R_Y(\theta)$ - Re = np.array([[cp * ct, -sp, cp * st], [ct * sp, cp, sp * st], - [-st, 0, ct]]) + Re = np.array( + [[cp * ct, -sp, cp * st], [ct * sp, cp, sp * st], [-st, 0, ct]] + ) p1 = Re.dot(p1) p2 = Re.dot(p2) # Translate the first three atoms by the origin @@ -544,8 +592,9 @@ def Cartesian(self, bat_frame): XYZ[self._root_XYZ_inds[2]] = p2 # Place the remaining atoms - for ((a0,a1,a2,a3), r01, angle, torsion) \ - in zip(self._torsion_XYZ_inds, bonds, angles, torsions): + for (a0, a1, a2, a3), r01, angle, torsion in zip( + self._torsion_XYZ_inds, bonds, angles, torsions + ): p1 = XYZ[a1] p3 = XYZ[a3] @@ -557,19 +606,20 @@ def Cartesian(self, bat_frame): cs_tor = np.cos(torsion) v21 = p1 - p2 - v21 /= np.sqrt(np.einsum('i,i->',v21,v21)) + v21 /= np.sqrt(np.einsum("i,i->", v21, v21)) v32 = p2 - p3 - v32 /= np.sqrt(np.einsum('i,i->',v32,v32)) + v32 /= np.sqrt(np.einsum("i,i->", v32, v32)) vp = np.cross(v32, v21) - cs = np.einsum('i,i->',v21,v32) + cs = np.einsum("i,i->", v21, v32) sn = max(np.sqrt(1.0 - cs * cs), 0.0000000001) vp = vp / sn vu = np.cross(vp, v21) - XYZ[a0] = p1 + \ - r01*(vu*sn_ang*cs_tor + vp*sn_ang*sn_tor - v21*cs_ang) + XYZ[a0] = p1 + r01 * ( + vu * sn_ang * cs_tor + vp * sn_ang * sn_tor - v21 * cs_ang + ) return XYZ @property @@ -578,4 +628,4 @@ def atoms(self): return self._ag def _get_aggregator(self): - return ResultsGroup(lookup={'bat': ResultsGroup.ndarray_vstack}) + return ResultsGroup(lookup={"bat": ResultsGroup.ndarray_vstack}) diff --git a/package/MDAnalysis/analysis/contacts.py b/package/MDAnalysis/analysis/contacts.py index f29fd4961e8..5d63471ae7d 100644 --- a/package/MDAnalysis/analysis/contacts.py +++ b/package/MDAnalysis/analysis/contacts.py @@ -260,7 +260,7 @@ def soft_cut_q(r, r0, beta=5.0, lambda_constant=1.8): """ r = np.asarray(r) r0 = np.asarray(r0) - result = 1/(1 + np.exp(beta*(r - lambda_constant * r0))) + result = 1 / (1 + np.exp(beta * (r - lambda_constant * r0))) return result.sum() / len(r0) @@ -392,8 +392,17 @@ def get_supported_backends(cls): "dask", ) - def __init__(self, u, select, refgroup, method="hard_cut", radius=4.5, - pbc=True, kwargs=None, **basekwargs): + def __init__( + self, + u, + select, + refgroup, + method="hard_cut", + radius=4.5, + pbc=True, + kwargs=None, + **basekwargs, + ): """ Parameters ---------- @@ -437,12 +446,14 @@ def __init__(self, u, select, refgroup, method="hard_cut", radius=4.5, self.fraction_kwargs = kwargs if kwargs is not None else {} - if method == 'hard_cut': + if method == "hard_cut": self.fraction_contacts = hard_cut_q - elif method == 'soft_cut': + elif method == "soft_cut": self.fraction_contacts = soft_cut_q - elif method == 'radius_cut': - self.fraction_contacts = functools.partial(radius_cut_q, radius=radius) + elif method == "radius_cut": + self.fraction_contacts = functools.partial( + radius_cut_q, radius=radius + ) else: if not callable(method): raise ValueError("method has to be callable") @@ -453,7 +464,7 @@ def __init__(self, u, select, refgroup, method="hard_cut", radius=4.5, self.grA, self.grB = (self._get_atomgroup(u, sel) for sel in select) self.pbc = pbc - + # contacts formed in reference self.r0 = [] self.initial_contacts = [] @@ -463,23 +474,37 @@ def __init__(self, u, select, refgroup, method="hard_cut", radius=4.5, if isinstance(refgroup[0], AtomGroup): refA, refB = refgroup - self.r0.append(distance_array(refA.positions, refB.positions, - box=self._get_box(refA.universe))) + self.r0.append( + distance_array( + refA.positions, + refB.positions, + box=self._get_box(refA.universe), + ) + ) self.initial_contacts.append(contact_matrix(self.r0[-1], radius)) else: for refA, refB in refgroup: - self.r0.append(distance_array(refA.positions, refB.positions, - box=self._get_box(refA.universe))) - self.initial_contacts.append(contact_matrix(self.r0[-1], radius)) + self.r0.append( + distance_array( + refA.positions, + refB.positions, + box=self._get_box(refA.universe), + ) + ) + self.initial_contacts.append( + contact_matrix(self.r0[-1], radius) + ) 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 " - "static AtomGroup. Updating AtomGroups " - "are not supported.") + select_error_message = ( + "selection must be either string or a " + "static AtomGroup. Updating AtomGroups " + "are not supported." + ) if isinstance(sel, str): return u.select_atoms(sel) elif isinstance(sel, AtomGroup): @@ -513,17 +538,19 @@ def _get_box_func(ts, pbc): return ts.dimensions if pbc else None def _prepare(self): - self.results.timeseries = np.empty((self.n_frames, len(self.r0)+1)) + self.results.timeseries = np.empty((self.n_frames, len(self.r0) + 1)) def _single_frame(self): self.results.timeseries[self._frame_index][0] = self._ts.frame - + # compute distance array for a frame - d = distance_array(self.grA.positions, self.grB.positions, - box=self._get_box(self._ts)) - - for i, (initial_contacts, r0) in enumerate(zip(self.initial_contacts, - self.r0), 1): + d = distance_array( + self.grA.positions, self.grB.positions, box=self._get_box(self._ts) + ) + + for i, (initial_contacts, r0) in enumerate( + zip(self.initial_contacts, self.r0), 1 + ): # select only the contacts that were formed in the reference state r = d[initial_contacts] r0 = r0[initial_contacts] @@ -532,14 +559,17 @@ def _single_frame(self): @property def timeseries(self): - wmsg = ("The `timeseries` attribute was deprecated in MDAnalysis " - "2.0.0 and will be removed in MDAnalysis 3.0.0. Please use " - "`results.timeseries` instead") + wmsg = ( + "The `timeseries` attribute was deprecated in MDAnalysis " + "2.0.0 and will be removed in MDAnalysis 3.0.0. Please use " + "`results.timeseries` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.timeseries def _get_aggregator(self): - return ResultsGroup(lookup={'timeseries': ResultsGroup.ndarray_vstack}) + return ResultsGroup(lookup={"timeseries": ResultsGroup.ndarray_vstack}) + def _new_selections(u_orig, selections, frame): """create stand alone AGs from selections at frame""" @@ -548,7 +578,7 @@ def _new_selections(u_orig, selections, frame): return [u.select_atoms(s) for s in selections] -def q1q2(u, select='all', radius=4.5): +def q1q2(u, select="all", radius=4.5): """Perform a q1-q2 analysis. Compares native contacts between the starting structure and final structure @@ -568,7 +598,7 @@ def q1q2(u, select='all', radius=4.5): contacts : :class:`Contacts` Contact Analysis that is set up for a q1-q2 analysis - + .. versionchanged:: 1.0.0 Changed `selection` keyword to `select` Support for setting ``start``, ``stop``, and ``step`` has been removed. @@ -577,6 +607,10 @@ def q1q2(u, select='all', radius=4.5): selection = (select, select) first_frame_refs = _new_selections(u, selection, 0) last_frame_refs = _new_selections(u, selection, -1) - return Contacts(u, selection, - (first_frame_refs, last_frame_refs), - radius=radius, method='radius_cut') + return Contacts( + u, + selection, + (first_frame_refs, last_frame_refs), + radius=radius, + method="radius_cut", + ) diff --git a/package/MDAnalysis/analysis/data/filenames.py b/package/MDAnalysis/analysis/data/filenames.py index a747450b86d..68ab3d96551 100644 --- a/package/MDAnalysis/analysis/data/filenames.py +++ b/package/MDAnalysis/analysis/data/filenames.py @@ -103,16 +103,17 @@ __all__ = [ - "Rama_ref", "Janin_ref", + "Rama_ref", + "Janin_ref", # reference plots for Ramachandran and Janin classes ] from importlib import resources -_base_ref = resources.files('MDAnalysis.analysis.data') -Rama_ref = (_base_ref / 'rama_ref_data.npy').as_posix() -Janin_ref = (_base_ref / 'janin_ref_data.npy').as_posix() +_base_ref = resources.files("MDAnalysis.analysis.data") +Rama_ref = (_base_ref / "rama_ref_data.npy").as_posix() +Janin_ref = (_base_ref / "janin_ref_data.npy").as_posix() # This should be the last line: clean up namespace del resources diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index eb2522531f2..61c4e679899 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -161,8 +161,12 @@ import MDAnalysis from MDAnalysis.core import groups -from MDAnalysis.lib.util import (fixedwidth_bins, iterable, asiterable, - deprecate,) +from MDAnalysis.lib.util import ( + fixedwidth_bins, + iterable, + asiterable, + deprecate, +) from MDAnalysis.lib import NeighborSearch as NS from MDAnalysis import NoDataError, MissingDataWarning from .. import units @@ -400,16 +404,24 @@ class DensityAnalysis(AnalysisBase): 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, atomgroup, delta=1.0, - metadata=None, padding=2.0, - gridcenter=None, - xdim=None, ydim=None, zdim=None): + return ("serial", "multiprocessing", "dask") + + def __init__( + self, + atomgroup, + delta=1.0, + metadata=None, + padding=2.0, + gridcenter=None, + xdim=None, + ydim=None, + zdim=None, + ): u = atomgroup.universe super(DensityAnalysis, self).__init__(u.trajectory) self._atomgroup = atomgroup @@ -423,16 +435,19 @@ def __init__(self, atomgroup, delta=1.0, # The grid with its dimensions has to be set up in __init__ # so that parallel analysis works correctly: each process # needs to have a results._grid of the same size and the - # same self._bins and self._arange (so this cannot happen - # in _prepare(), which is executed in parallel on different - # parts of the trajectory). + # same self._bins and self._arange (so this cannot happen + # in _prepare(), which is executed in parallel on different + # parts of the trajectory). coord = self._atomgroup.positions - if (self._gridcenter is not None or - any([self._xdim, self._ydim, self._zdim])): + if self._gridcenter is not None or any( + [self._xdim, self._ydim, self._zdim] + ): # Issue 2372: padding is ignored, defaults to 2.0 therefore warn if self._padding > 0: - msg = (f"Box padding (currently set at {self._padding}) " - f"is not used in user defined grids.") + msg = ( + f"Box padding (currently set at {self._padding}) " + f"is not used in user defined grids." + ) warnings.warn(msg) logger.warning(msg) # Generate a copy of smin/smax from coords to later check if the @@ -441,18 +456,25 @@ def __init__(self, atomgroup, delta=1.0, smin = np.min(coord, axis=0) smax = np.max(coord, axis=0) except ValueError as err: - msg = ("No atoms in AtomGroup at input time frame. " - "This may be intended; please ensure that " - "your grid selection covers the atomic " - "positions you wish to capture.") + msg = ( + "No atoms in AtomGroup at input time frame. " + "This may be intended; please ensure that " + "your grid selection covers the atomic " + "positions you wish to capture." + ) warnings.warn(msg) logger.warning(msg) - smin = self._gridcenter #assigns limits to be later - - smax = self._gridcenter #overwritten by _set_user_grid + smin = self._gridcenter # assigns limits to be later - + smax = self._gridcenter # overwritten by _set_user_grid # Overwrite smin/smax with user defined values - smin, smax = self._set_user_grid(self._gridcenter, self._xdim, - self._ydim, self._zdim, smin, - smax) + smin, smax = self._set_user_grid( + self._gridcenter, + self._xdim, + self._ydim, + self._zdim, + smin, + smax, + ) else: # Make the box bigger to avoid as much as possible 'outlier'. This # is important if the sites are defined at a high density: in this @@ -465,18 +487,21 @@ def __init__(self, atomgroup, delta=1.0, smin = np.min(coord, axis=0) - self._padding smax = np.max(coord, axis=0) + self._padding except ValueError as err: - errmsg = ("No atoms in AtomGroup at input time frame. " - "Grid for density could not be automatically" - " generated. If this is expected, a user" - " defined grid will need to be " - "provided instead.") + errmsg = ( + "No atoms in AtomGroup at input time frame. " + "Grid for density could not be automatically" + " generated. If this is expected, a user" + " defined grid will need to be " + "provided instead." + ) raise ValueError(errmsg) from err BINS = fixedwidth_bins(self._delta, smin, smax) - arange = np.transpose(np.vstack((BINS['min'], BINS['max']))) - bins = BINS['Nbins'] + arange = np.transpose(np.vstack((BINS["min"], BINS["max"]))) + bins = BINS["Nbins"] # create empty grid with the right dimensions (and get the edges) - grid, edges = np.histogramdd(np.zeros((1, 3)), bins=bins, - range=arange, density=False) + grid, edges = np.histogramdd( + np.zeros((1, 3)), bins=bins, range=arange, density=False + ) grid *= 0.0 self.results._grid = grid self._edges = edges @@ -484,30 +509,36 @@ def __init__(self, atomgroup, delta=1.0, self._bins = bins def _single_frame(self): - h, _ = np.histogramdd(self._atomgroup.positions, - bins=self._bins, range=self._arange, - density=False) + h, _ = np.histogramdd( + self._atomgroup.positions, + bins=self._bins, + range=self._arange, + density=False, + ) self.results._grid += h def _conclude(self): # average: self.results._grid /= float(self.n_frames) - density = Density(grid=self.results._grid, edges=self._edges, - units={'length': "Angstrom"}, - parameters={'isDensity': False}) + density = Density( + grid=self.results._grid, + edges=self._edges, + units={"length": "Angstrom"}, + parameters={"isDensity": False}, + ) density.make_density() self.results.density = density def _get_aggregator(self): - return ResultsGroup(lookup={ - '_grid': ResultsGroup.ndarray_sum} - ) + return ResultsGroup(lookup={"_grid": ResultsGroup.ndarray_sum}) @property def density(self): - wmsg = ("The `density` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.density` instead") + wmsg = ( + "The `density` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.density` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.density @@ -544,10 +575,12 @@ def _set_user_grid(gridcenter, xdim, ydim, zdim, smin, smax): """ # Check user inputs if any(x is None for x in [gridcenter, xdim, ydim, zdim]): - errmsg = ("Gridcenter or grid dimensions are not provided") + errmsg = "Gridcenter or grid dimensions are not provided" raise ValueError(errmsg) try: - gridcenter = np.asarray(gridcenter, dtype=np.float32).reshape(3,) + gridcenter = np.asarray(gridcenter, dtype=np.float32).reshape( + 3, + ) except ValueError as err: raise ValueError("Gridcenter must be a 3D coordinate") from err try: @@ -557,16 +590,17 @@ def _set_user_grid(gridcenter, xdim, ydim, zdim, smin, smax): if any(np.isnan(gridcenter)) or any(np.isnan(xyzdim)): raise ValueError("Gridcenter or grid dimensions have NaN element") - # Set min/max by shifting by half the edge length of each dimension - umin = gridcenter - xyzdim/2 - umax = gridcenter + xyzdim/2 + umin = gridcenter - xyzdim / 2 + umax = gridcenter + xyzdim / 2 # Here we test if coords of selection fall outside of the defined grid # if this happens, we warn users they may want to resize their grids if any(smin < umin) or any(smax > umax): - msg = ("Atom selection does not fit grid --- " - "you may want to define a larger box") + msg = ( + "Atom selection does not fit grid --- " + "you may want to define a larger box" + ) warnings.warn(msg) logger.warning(msg) return umin, umax @@ -725,22 +759,25 @@ class Density(Grid): """ def __init__(self, *args, **kwargs): - length_unit = 'Angstrom' - - parameters = kwargs.pop('parameters', {}) - if (len(args) > 0 and isinstance(args[0], str) or - isinstance(kwargs.get('grid', None), str)): + length_unit = "Angstrom" + + parameters = kwargs.pop("parameters", {}) + if ( + len(args) > 0 + and isinstance(args[0], str) + or isinstance(kwargs.get("grid", None), str) + ): # try to be smart: when reading from a file then it is likely that # this is a density - parameters.setdefault('isDensity', True) + parameters.setdefault("isDensity", True) else: - parameters.setdefault('isDensity', False) - units = kwargs.pop('units', {}) - units.setdefault('length', length_unit) - if parameters['isDensity']: - units.setdefault('density', length_unit) + parameters.setdefault("isDensity", False) + units = kwargs.pop("units", {}) + units.setdefault("length", length_unit) + if parameters["isDensity"]: + units.setdefault("density", length_unit) else: - units.setdefault('density', None) + units.setdefault("density", None) super(Density, self).__init__(*args, **kwargs) @@ -767,25 +804,31 @@ def _check_set_unit(self, u): # all this unit crap should be a class... try: for unit_type, value in u.items(): - if value is None: # check here, too iffy to use dictionary[None]=None + if ( + value is None + ): # check here, too iffy to use dictionary[None]=None self.units[unit_type] = None continue try: units.conversion_factor[unit_type][value] self.units[unit_type] = value except KeyError: - errmsg = (f"Unit {value} of type {unit_type} is not " - f"recognized.") + errmsg = ( + f"Unit {value} of type {unit_type} is not " + f"recognized." + ) raise ValueError(errmsg) from None except AttributeError: - errmsg = '"unit" must be a dictionary with keys "length" and "density.' + errmsg = ( + '"unit" must be a dictionary with keys "length" and "density.' + ) logger.fatal(errmsg) raise ValueError(errmsg) from None # need at least length and density (can be None) - if 'length' not in self.units: + if "length" not in self.units: raise ValueError('"unit" must contain a unit for "length".') - if 'density' not in self.units: - self.units['density'] = None + if "density" not in self.units: + self.units["density"] = None def make_density(self): """Convert the grid (a histogram, counts in a cell) to a density (counts/volume). @@ -800,7 +843,7 @@ def make_density(self): # Make it a density by dividing by the volume of each grid cell # (from numpy.histogramdd, which is for general n-D grids) - if self.parameters['isDensity']: + if self.parameters["isDensity"]: msg = "Running make_density() makes no sense: Grid is already a density. Nothing done." logger.warning(msg) warnings.warn(msg) @@ -812,11 +855,11 @@ def make_density(self): shape = np.ones(D, int) shape[i] = len(dedges[i]) self.grid /= dedges[i].reshape(shape) - self.parameters['isDensity'] = True + self.parameters["isDensity"] = True # see units.densityUnit_factor for units - self.units['density'] = self.units['length'] + "^{-3}" + self.units["density"] = self.units["length"] + "^{-3}" - def convert_length(self, unit='Angstrom'): + def convert_length(self, unit="Angstrom"): """Convert Grid object to the new `unit`. Parameters @@ -833,14 +876,16 @@ def convert_length(self, unit='Angstrom'): histogram and a length unit and use :meth:`make_density`. """ - if unit == self.units['length']: + if unit == self.units["length"]: return - cvnfact = units.get_conversion_factor('length', self.units['length'], unit) + cvnfact = units.get_conversion_factor( + "length", self.units["length"], unit + ) self.edges = [x * cvnfact for x in self.edges] - self.units['length'] = unit + self.units["length"] = unit self._update() # needed to recalculate midpoints and origin - def convert_density(self, unit='Angstrom^{-3}'): + def convert_density(self, unit="Angstrom^{-3}"): """Convert the density to the physical units given by `unit`. Parameters @@ -879,24 +924,33 @@ def convert_density(self, unit='Angstrom^{-3}'): and floating point artifacts for multiple conversions. """ - if not self.parameters['isDensity']: - errmsg = 'The grid is not a density so convert_density() makes no sense.' + if not self.parameters["isDensity"]: + errmsg = "The grid is not a density so convert_density() makes no sense." logger.fatal(errmsg) raise RuntimeError(errmsg) - if unit == self.units['density']: + if unit == self.units["density"]: return try: - self.grid *= units.get_conversion_factor('density', - self.units['density'], unit) + self.grid *= units.get_conversion_factor( + "density", self.units["density"], unit + ) except KeyError: - errmsg = (f"The name of the unit ({unit} supplied) must be one " - f"of:\n{units.conversion_factor['density'].keys()}") + errmsg = ( + f"The name of the unit ({unit} supplied) must be one " + f"of:\n{units.conversion_factor['density'].keys()}" + ) raise ValueError(errmsg) from None - self.units['density'] = unit + self.units["density"] = unit def __repr__(self): - if self.parameters['isDensity']: - grid_type = 'density' + if self.parameters["isDensity"]: + grid_type = "density" else: - grid_type = 'histogram' - return '' + grid_type = "histogram" + return ( + "" + ) diff --git a/package/MDAnalysis/analysis/dielectric.py b/package/MDAnalysis/analysis/dielectric.py index 4f14eb88074..28a22b5116b 100644 --- a/package/MDAnalysis/analysis/dielectric.py +++ b/package/MDAnalysis/analysis/dielectric.py @@ -37,10 +37,12 @@ from MDAnalysis.due import due, Doi from MDAnalysis.exceptions import NoDataError -due.cite(Doi("10.1080/00268978300102721"), - description="Dielectric analysis", - path="MDAnalysis.analysis.dielectric", - cite_module=True) +due.cite( + Doi("10.1080/00268978300102721"), + description="Dielectric analysis", + path="MDAnalysis.analysis.dielectric", + cite_module=True, +) del Doi @@ -121,9 +123,11 @@ class DielectricConstant(AnalysisBase): .. versionadded:: 2.1.0 """ + def __init__(self, atomgroup, temperature=300, make_whole=True, **kwargs): - super(DielectricConstant, self).__init__(atomgroup.universe.trajectory, - **kwargs) + super(DielectricConstant, self).__init__( + atomgroup.universe.trajectory, **kwargs + ) self.atomgroup = atomgroup self.temperature = temperature self.make_whole = make_whole @@ -132,11 +136,14 @@ def _prepare(self): if not hasattr(self.atomgroup, "charges"): raise NoDataError("No charges defined given atomgroup.") - if not np.allclose(self.atomgroup.total_charge(compound='fragments'), - 0.0, atol=1E-5): - raise NotImplementedError("Analysis for non-neutral systems or" - " systems with free charges are not" - " available.") + if not np.allclose( + self.atomgroup.total_charge(compound="fragments"), 0.0, atol=1e-5 + ): + raise NotImplementedError( + "Analysis for non-neutral systems or" + " systems with free charges are not" + " available." + ) self.volume = 0 self.results.M = np.zeros(3) @@ -163,8 +170,11 @@ def _conclude(self): self.results.fluct = self.results.M2 - self.results.M * self.results.M self.results.eps = self.results.fluct / ( - convert(constants["Boltzmann_constant"], "kJ/mol", "eV") * - self.temperature * self.volume * constants["electric_constant"]) + convert(constants["Boltzmann_constant"], "kJ/mol", "eV") + * self.temperature + * self.volume + * constants["electric_constant"] + ) self.results.eps_mean = self.results.eps.mean() diff --git a/package/MDAnalysis/analysis/dihedrals.py b/package/MDAnalysis/analysis/dihedrals.py index c6a5585f7a0..0c5ecb33cdf 100644 --- a/package/MDAnalysis/analysis/dihedrals.py +++ b/package/MDAnalysis/analysis/dihedrals.py @@ -271,12 +271,16 @@ class Dihedral(AnalysisBase): 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',) - + return ( + "serial", + "multiprocessing", + "dask", + ) def __init__(self, atomgroups, **kwargs): """Parameters @@ -292,7 +296,8 @@ def __init__(self, atomgroups, **kwargs): """ super(Dihedral, self).__init__( - atomgroups[0].universe.trajectory, **kwargs) + atomgroups[0].universe.trajectory, **kwargs + ) self.atomgroups = atomgroups if any([len(ag) != 4 for ag in atomgroups]): @@ -307,12 +312,16 @@ def _prepare(self): self.results.angles = [] def _get_aggregator(self): - return ResultsGroup(lookup={'angles': ResultsGroup.ndarray_vstack}) + 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, - box=self.ag1.dimensions) + angle = calc_dihedrals( + self.ag1.positions, + self.ag2.positions, + self.ag3.positions, + self.ag4.positions, + box=self.ag1.dimensions, + ) self.results.angles.append(angle) def _conclude(self): @@ -320,9 +329,11 @@ def _conclude(self): @property def angles(self): - wmsg = ("The `angle` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.angles` instead") + wmsg = ( + "The `angle` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.angles` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.angles @@ -394,16 +405,29 @@ class Ramachandran(AnalysisBase): 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): + return ( + "serial", + "multiprocessing", + "dask", + ) + + def __init__( + self, + atomgroup, + c_name="C", + n_name="N", + ca_name="CA", + check_protein=True, + **kwargs, + ): super(Ramachandran, self).__init__( - atomgroup.universe.trajectory, **kwargs) + atomgroup.universe.trajectory, **kwargs + ) self.atomgroup = atomgroup residues = self.atomgroup.residues @@ -411,12 +435,16 @@ def __init__(self, atomgroup, c_name='C', n_name='N', ca_name='CA', protein = self.atomgroup.universe.select_atoms("protein").residues if not residues.issubset(protein): - raise ValueError("Found atoms outside of protein. Only atoms " - "inside of a 'protein' selection can be used to " - "calculate dihedrals.") + raise ValueError( + "Found atoms outside of protein. Only atoms " + "inside of a 'protein' selection can be used to " + "calculate dihedrals." + ) elif not residues.isdisjoint(protein[[0, -1]]): - warnings.warn("Cannot determine phi and psi angles for the first " - "or last residues") + warnings.warn( + "Cannot determine phi and psi angles for the first " + "or last residues" + ) residues = residues.difference(protein[[0, -1]]) prev = residues._get_prev_residues_by_resid() @@ -425,17 +453,20 @@ def __init__(self, atomgroup, c_name='C', n_name='N', ca_name='CA', keep = keep & np.array([r is not None for r in nxt]) if not np.all(keep): - warnings.warn("Some residues in selection do not have " - "phi or psi selections") + warnings.warn( + "Some residues in selection do not have " + "phi or psi selections" + ) prev = sum(prev[keep]) nxt = sum(nxt[keep]) residues = residues[keep] # find n, c, ca - keep_prev = [sum(r.atoms.names==c_name)==1 for r in prev] + keep_prev = [sum(r.atoms.names == c_name) == 1 for r in prev] rnames = [n_name, c_name, ca_name] - keep_res = [all(sum(r.atoms.names == n) == 1 for n in rnames) - for r in residues] + keep_res = [ + all(sum(r.atoms.names == n) == 1 for n in rnames) for r in residues + ] keep_next = [sum(r.atoms.names == n_name) == 1 for r in nxt] # alright we'll keep these @@ -451,20 +482,27 @@ def __init__(self, atomgroup, c_name='C', n_name='N', ca_name='CA', self.ag4 = res.atoms[rnames == c_name] self.ag5 = nxt.atoms[nxt.atoms.names == n_name] - def _prepare(self): self.results.angles = [] def _get_aggregator(self): - return ResultsGroup(lookup={'angles': ResultsGroup.ndarray_vstack}) + 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, - box=self.ag1.dimensions) - psi_angles = calc_dihedrals(self.ag2.positions, self.ag3.positions, - self.ag4.positions, self.ag5.positions, - box=self.ag1.dimensions) + phi_angles = calc_dihedrals( + self.ag1.positions, + self.ag2.positions, + self.ag3.positions, + self.ag4.positions, + box=self.ag1.dimensions, + ) + psi_angles = calc_dihedrals( + self.ag2.positions, + self.ag3.positions, + self.ag4.positions, + self.ag5.positions, + box=self.ag1.dimensions, + ) phi_psi = [(phi, psi) for phi, psi in zip(phi_angles, psi_angles)] self.results.angles.append(phi_psi) @@ -499,31 +537,40 @@ def plot(self, ax=None, ref=False, **kwargs): if ax is None: ax = plt.gca() ax.axis([-180, 180, -180, 180]) - ax.axhline(0, color='k', lw=1) - ax.axvline(0, color='k', lw=1) - ax.set(xticks=range(-180, 181, 60), yticks=range(-180, 181, 60), - xlabel=r"$\phi$", ylabel=r"$\psi$") + ax.axhline(0, color="k", lw=1) + ax.axvline(0, color="k", lw=1) + ax.set( + xticks=range(-180, 181, 60), + yticks=range(-180, 181, 60), + xlabel=r"$\phi$", + ylabel=r"$\psi$", + ) degree_formatter = plt.matplotlib.ticker.StrMethodFormatter( - r"{x:g}$\degree$") + r"{x:g}$\degree$" + ) ax.xaxis.set_major_formatter(degree_formatter) ax.yaxis.set_major_formatter(degree_formatter) if ref: - X, Y = np.meshgrid(np.arange(-180, 180, 4), - np.arange(-180, 180, 4)) + X, Y = np.meshgrid( + np.arange(-180, 180, 4), np.arange(-180, 180, 4) + ) levels = [1, 17, 15000] - colors = ['#A1D4FF', '#35A1FF'] + colors = ["#A1D4FF", "#35A1FF"] ax.contourf(X, Y, np.load(Rama_ref), levels=levels, colors=colors) a = self.results.angles.reshape( - np.prod(self.results.angles.shape[:2]), 2) + np.prod(self.results.angles.shape[:2]), 2 + ) ax.scatter(a[:, 0], a[:, 1], **kwargs) return ax @property def angles(self): - wmsg = ("The `angle` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.angles` instead") + wmsg = ( + "The `angle` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.angles` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.angles @@ -549,10 +596,13 @@ class Janin(Ramachandran): """ - def __init__(self, atomgroup, - select_remove="resname ALA CYS* GLY PRO SER THR VAL", - select_protein="protein", - **kwargs): + def __init__( + self, + atomgroup, + select_remove="resname ALA CYS* GLY PRO SER THR VAL", + select_protein="protein", + **kwargs, + ): r"""Parameters ---------- atomgroup : AtomGroup or ResidueGroup @@ -588,20 +638,25 @@ def __init__(self, atomgroup, :class:`MDAnalysis.analysis.base.Results` instance. """ super(Ramachandran, self).__init__( - atomgroup.universe.trajectory, **kwargs) + atomgroup.universe.trajectory, **kwargs + ) self.atomgroup = atomgroup residues = atomgroup.residues protein = atomgroup.select_atoms(select_protein).residues remove = residues.atoms.select_atoms(select_remove).residues if not residues.issubset(protein): - raise ValueError("Found atoms outside of protein. Only atoms " - "inside of a protein " - f"(select_protein='{select_protein}') can be " - "used to calculate dihedrals.") + raise ValueError( + "Found atoms outside of protein. Only atoms " + "inside of a protein " + f"(select_protein='{select_protein}') can be " + "used to calculate dihedrals." + ) elif len(remove) != 0: - warnings.warn(f"All residues selected with '{select_remove}' " - "have been removed from the selection.") + warnings.warn( + f"All residues selected with '{select_remove}' " + "have been removed from the selection." + ) residues = residues.difference(remove) self.ag1 = residues.atoms.select_atoms("name N") @@ -613,14 +668,19 @@ def __init__(self, atomgroup, # if there is an altloc attribute, too many atoms will be selected which # must be removed before using the class, or the file is missing atoms # for some residues which must also be removed - if any(len(self.ag1) != len(ag) for ag in [self.ag2, self.ag3, - self.ag4, self.ag5]): - raise ValueError("Too many or too few atoms selected. Check for " - "missing or duplicate atoms in topology.") + if any( + len(self.ag1) != len(ag) + for ag in [self.ag2, self.ag3, self.ag4, self.ag5] + ): + raise ValueError( + "Too many or too few atoms selected. Check for " + "missing or duplicate atoms in topology." + ) def _conclude(self): - self.results.angles = (np.rad2deg(np.array( - self.results.angles)) + 360) % 360 + self.results.angles = ( + np.rad2deg(np.array(self.results.angles)) + 360 + ) % 360 def plot(self, ax=None, ref=False, **kwargs): """Plots data into standard Janin plot. @@ -650,21 +710,27 @@ def plot(self, ax=None, ref=False, **kwargs): if ax is None: ax = plt.gca() ax.axis([0, 360, 0, 360]) - ax.axhline(180, color='k', lw=1) - ax.axvline(180, color='k', lw=1) - ax.set(xticks=range(0, 361, 60), yticks=range(0, 361, 60), - xlabel=r"$\chi_1$", ylabel=r"$\chi_2$") + ax.axhline(180, color="k", lw=1) + ax.axvline(180, color="k", lw=1) + ax.set( + xticks=range(0, 361, 60), + yticks=range(0, 361, 60), + xlabel=r"$\chi_1$", + ylabel=r"$\chi_2$", + ) degree_formatter = plt.matplotlib.ticker.StrMethodFormatter( - r"{x:g}$\degree$") + r"{x:g}$\degree$" + ) ax.xaxis.set_major_formatter(degree_formatter) ax.yaxis.set_major_formatter(degree_formatter) if ref: X, Y = np.meshgrid(np.arange(0, 360, 6), np.arange(0, 360, 6)) levels = [1, 6, 600] - colors = ['#A1D4FF', '#35A1FF'] + colors = ["#A1D4FF", "#35A1FF"] ax.contourf(X, Y, np.load(Janin_ref), levels=levels, colors=colors) - a = self.results.angles.reshape(np.prod( - self.results.angles.shape[:2]), 2) + a = self.results.angles.reshape( + np.prod(self.results.angles.shape[:2]), 2 + ) ax.scatter(a[:, 0], a[:, 1], **kwargs) return ax diff --git a/package/MDAnalysis/analysis/distances.py b/package/MDAnalysis/analysis/distances.py index 9e81de95688..44a09c4fcbd 100644 --- a/package/MDAnalysis/analysis/distances.py +++ b/package/MDAnalysis/analysis/distances.py @@ -38,28 +38,38 @@ :mod:`MDAnalysis.lib.distances` """ -__all__ = ['distance_array', 'self_distance_array', - 'contact_matrix', 'dist', 'between'] +__all__ = [ + "distance_array", + "self_distance_array", + "contact_matrix", + "dist", + "between", +] import numpy as np import scipy.sparse from MDAnalysis.lib.distances import ( - capped_distance, - self_distance_array, distance_array, # legacy reasons + capped_distance, + self_distance_array, + distance_array, # legacy reasons +) +from MDAnalysis.lib.c_distances import ( + contact_matrix_no_pbc, + contact_matrix_pbc, ) -from MDAnalysis.lib.c_distances import contact_matrix_no_pbc, contact_matrix_pbc from MDAnalysis.lib.NeighborSearch import AtomNeighborSearch from MDAnalysis.lib.distances import calc_bonds import warnings import logging + logger = logging.getLogger("MDAnalysis.analysis.distances") def contact_matrix(coord, cutoff=15.0, returntype="numpy", box=None): - '''Calculates a matrix of contacts. + """Calculates a matrix of contacts. There is a fast, high-memory-usage version for small systems (*returntype* = 'numpy'), and a slower, low-memory-usage version for @@ -97,20 +107,24 @@ def contact_matrix(coord, cutoff=15.0, returntype="numpy", box=None): .. versionchanged:: 0.11.0 Keyword *suppress_progmet* and *progress_meter_freq* were removed. - ''' + """ if returntype == "numpy": adj = np.full((len(coord), len(coord)), False, dtype=bool) - pairs = capped_distance(coord, coord, max_cutoff=cutoff, box=box, return_distances=False) - + pairs = capped_distance( + coord, coord, max_cutoff=cutoff, box=box, return_distances=False + ) + idx, idy = np.transpose(pairs) - adj[idx, idy]=True - + adj[idx, idy] = True + return adj elif returntype == "sparse": # Initialize square List of Lists matrix of dimensions equal to number # of coordinates passed - sparse_contacts = scipy.sparse.lil_matrix((len(coord), len(coord)), dtype='bool') + sparse_contacts = scipy.sparse.lil_matrix( + (len(coord), len(coord)), dtype="bool" + ) if box is not None: # with PBC contact_matrix_pbc(coord, sparse_contacts, box, cutoff) @@ -154,14 +168,16 @@ def dist(A, B, offset=0, box=None): """ if A.atoms.n_atoms != B.atoms.n_atoms: - raise ValueError("AtomGroups A and B do not have the same number of atoms") + raise ValueError( + "AtomGroups A and B do not have the same number of atoms" + ) try: off_A, off_B = offset except (TypeError, ValueError): off_A = off_B = int(offset) residues_A = np.array(A.resids) + off_A residues_B = np.array(B.resids) + off_B - + d = calc_bonds(A.positions, B.positions, box) return np.array([residues_A, residues_B, d]) diff --git a/package/MDAnalysis/analysis/dssp/dssp.py b/package/MDAnalysis/analysis/dssp/dssp.py index 21dd9423e49..d88f1bdbe45 100644 --- a/package/MDAnalysis/analysis/dssp/dssp.py +++ b/package/MDAnalysis/analysis/dssp/dssp.py @@ -292,7 +292,6 @@ def get_supported_backends(cls): "dask", ) - def __init__( self, atoms: Union[Universe, AtomGroup], @@ -317,14 +316,15 @@ def __init__( for t in heavyatom_names } self._hydrogens: list["AtomGroup"] = [ - res.atoms.select_atoms(f"name {hydrogen_name}") for res in ag.residues + res.atoms.select_atoms(f"name {hydrogen_name}") + for res in ag.residues ] # can't do it the other way because I need missing values to exist # so that I could fill them in later if not self._guess_hydrogens: # zip() assumes that _heavy_atoms and _hydrogens is ordered in the # same way. This is true as long as the original AtomGroup ag is - # sorted. With the hard-coded protein selection for ag this is always + # sorted. With the hard-coded protein selection for ag this is always # true but if the code on L277 ever changes, make sure to sort first! for calpha, hydrogen in zip( self._heavy_atoms["CA"][1:], self._hydrogens[1:] @@ -373,7 +373,9 @@ def _get_coords(self) -> np.ndarray: coords = np.array(positions) if not self._guess_hydrogens: - guessed_h_coords = _get_hydrogen_atom_position(coords.swapaxes(0, 1)) + guessed_h_coords = _get_hydrogen_atom_position( + coords.swapaxes(0, 1) + ) h_coords = np.array( [ @@ -402,6 +404,7 @@ def _get_aggregator(self): lookup={"dssp_ndarray": ResultsGroup.flatten_sequence}, ) + def translate(onehot: np.ndarray) -> np.ndarray: """Translate a one-hot encoding summary into char-based secondary structure assignment. diff --git a/package/MDAnalysis/analysis/dssp/pydssp_numpy.py b/package/MDAnalysis/analysis/dssp/pydssp_numpy.py index 1ae8ac369ea..f54ea5443af 100644 --- a/package/MDAnalysis/analysis/dssp/pydssp_numpy.py +++ b/package/MDAnalysis/analysis/dssp/pydssp_numpy.py @@ -65,7 +65,10 @@ def _upsample(a: np.ndarray, window: int) -> np.ndarray: def _unfold(a: np.ndarray, window: int, axis: int): "Helper function for 2D array upsampling" - idx = np.arange(window)[:, None] + np.arange(a.shape[axis] - window + 1)[None, :] + idx = ( + np.arange(window)[:, None] + + np.arange(a.shape[axis] - window + 1)[None, :] + ) unfolded = np.take(a, idx, axis=axis) return np.moveaxis(unfolded, axis - 1, -1) @@ -154,7 +157,9 @@ def get_hbond_map( h_1 = coord[1:, 4] coord = coord[:, :4] else: # pragma: no cover - raise ValueError("Number of atoms should be 4 (N,CA,C,O) or 5 (N,CA,C,O,H)") + raise ValueError( + "Number of atoms should be 4 (N,CA,C,O) or 5 (N,CA,C,O,H)" + ) # after this: # h.shape == (n_atoms, 3) # coord.shape == (n_atoms, 4, 3) @@ -176,7 +181,9 @@ def get_hbond_map( # electrostatic interaction energy # e[i, j] = e(CO_i) - e(NH_j) e = np.pad( - CONST_Q1Q2 * (1.0 / d_on + 1.0 / d_ch - 1.0 / d_oh - 1.0 / d_cn) * CONST_F, + CONST_Q1Q2 + * (1.0 / d_on + 1.0 / d_ch - 1.0 / d_oh - 1.0 / d_cn) + * CONST_F, [[1, 0], [0, 1]], ) diff --git a/package/MDAnalysis/analysis/encore/__init__.py b/package/MDAnalysis/analysis/encore/__init__.py index 49095ecfd5c..880b8b5d46c 100644 --- a/package/MDAnalysis/analysis/encore/__init__.py +++ b/package/MDAnalysis/analysis/encore/__init__.py @@ -20,31 +20,35 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -from .similarity import hes, ces, dres, \ - ces_convergence, dres_convergence +from .similarity import hes, ces, dres, ces_convergence, dres_convergence from .clustering.ClusterCollection import ClusterCollection, Cluster from .clustering.ClusteringMethod import * from .clustering.cluster import cluster from .dimensionality_reduction.DimensionalityReductionMethod import * from .dimensionality_reduction.reduce_dimensionality import ( - reduce_dimensionality) + reduce_dimensionality, +) from .confdistmatrix import get_distance_matrix from .utils import merge_universes -__all__ = ['covariance', 'similarity', 'confdistmatrix', 'clustering'] +__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.") +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"), - description="ENCORE Ensemble Comparison", - path="MDAnalysis.analysis.encore", - cite_module=True) +due.cite( + Doi("10.1371/journal.pcbi.1004415"), + description="ENCORE Ensemble Comparison", + path="MDAnalysis.analysis.encore", + cite_module=True, +) diff --git a/package/MDAnalysis/analysis/encore/bootstrap.py b/package/MDAnalysis/analysis/encore/bootstrap.py index 80761a8fdd2..8784409c7f6 100644 --- a/package/MDAnalysis/analysis/encore/bootstrap.py +++ b/package/MDAnalysis/analysis/encore/bootstrap.py @@ -70,9 +70,13 @@ def bootstrapped_matrix(matrix, ensemble_assignment): indexes = [] for ens in ensemble_identifiers: old_indexes = np.where(ensemble_assignment == ens)[0] - indexes.append(np.random.randint(low=np.min(old_indexes), - high=np.max(old_indexes) + 1, - size=old_indexes.shape[0])) + indexes.append( + np.random.randint( + low=np.min(old_indexes), + high=np.max(old_indexes) + 1, + size=old_indexes.shape[0], + ) + ) indexes = np.hstack(indexes) for j in range(this_m.size): @@ -83,10 +87,9 @@ def bootstrapped_matrix(matrix, ensemble_assignment): return this_m -def get_distance_matrix_bootstrap_samples(distance_matrix, - ensemble_assignment, - samples=100, - ncores=1): +def get_distance_matrix_bootstrap_samples( + distance_matrix, ensemble_assignment, samples=100, ncores=1 +): """ Calculates distance matrices corresponding to bootstrapped ensembles, by resampling with replacement. @@ -113,8 +116,9 @@ def get_distance_matrix_bootstrap_samples(distance_matrix, confdistmatrix : list of encore.utils.TriangularMatrix """ - bs_args = \ - [([distance_matrix, ensemble_assignment]) for i in range(samples)] + bs_args = [ + ([distance_matrix, ensemble_assignment]) for i in range(samples) + ] pc = ParallelCalculation(ncores, bootstrapped_matrix, bs_args) @@ -125,8 +129,7 @@ def get_distance_matrix_bootstrap_samples(distance_matrix, return bootstrap_matrices -def get_ensemble_bootstrap_samples(ensemble, - samples=100): +def get_ensemble_bootstrap_samples(ensemble, samples=100): """ Generates a bootstrapped ensemble by resampling with replacement. @@ -152,9 +155,13 @@ def get_ensemble_bootstrap_samples(ensemble, indices = np.random.randint( low=0, high=ensemble.trajectory.timeseries().shape[1], - size=ensemble.trajectory.timeseries().shape[1]) + size=ensemble.trajectory.timeseries().shape[1], + ) ensembles.append( - mda.Universe(ensemble.filename, - ensemble.trajectory.timeseries(order='fac')[indices,:,:], - format=mda.coordinates.memory.MemoryReader)) + mda.Universe( + ensemble.filename, + ensemble.trajectory.timeseries(order="fac")[indices, :, :], + format=mda.coordinates.memory.MemoryReader, + ) + ) return ensembles diff --git a/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py b/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py index e4b7070dcac..8c545c54215 100644 --- a/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py +++ b/package/MDAnalysis/analysis/encore/clustering/ClusterCollection.py @@ -63,7 +63,7 @@ class Cluster(object): elements : numpy.array array containing the cluster elements. - """ + """ def __init__(self, elem_list=None, centroid=None, idn=None, metadata=None): """Class constructor. If elem_list is None, an empty cluster is created @@ -85,7 +85,7 @@ def __init__(self, elem_list=None, centroid=None, idn=None, metadata=None): metadata, one value for each cluster element. The iterable must have the same length as the elements array. - """ + """ self.id = idn @@ -99,16 +99,20 @@ def __init__(self, elem_list=None, centroid=None, idn=None, metadata=None): self.metadata = {} self.elements = elem_list if centroid not in self.elements: - raise LookupError("Centroid of cluster not found in the element list") + raise LookupError( + "Centroid of cluster not found in the element list" + ) self.centroid = centroid self.size = self.elements.shape[0] if metadata: for name, data in metadata.items(): if len(data) != self.size: - raise TypeError('Size of metadata having label "{0}" ' - 'is not equal to the number of cluster ' - 'elements'.format(name)) + raise TypeError( + 'Size of metadata having label "{0}" ' + "is not equal to the number of cluster " + "elements".format(name) + ) self.add_metadata(name, data) def __iter__(self): @@ -125,8 +129,10 @@ def __len__(self): def add_metadata(self, name, data): if len(data) != self.size: - raise TypeError("Size of metadata is not equal to the number of " - "cluster elements") + raise TypeError( + "Size of metadata is not equal to the number of " + "cluster elements" + ) self.metadata[name] = np.array(data) def __repr__(self): @@ -137,9 +143,9 @@ def __repr__(self): return "" else: return "".format( - self.size, - self.centroid, - self.id) + self.size, self.centroid, self.id + ) + class ClusterCollection(object): """Clusters collection class; this class represents the results of a full @@ -152,38 +158,38 @@ class ClusterCollection(object): clusters : list list of of Cluster objects which are part of the Cluster collection -""" + """ def __init__(self, elements=None, metadata=None): """Class constructor. If elements is None, an empty cluster collection - will be created. Otherwise, the constructor takes as input an - iterable of ints, for instance: + will be created. Otherwise, the constructor takes as input an + iterable of ints, for instance: - [ a, a, a, a, b, b, b, c, c, ... , z, z ] + [ a, a, a, a, b, b, b, c, c, ... , z, z ] - the variables a,b,c,...,z are cluster centroids, here as cluster - element numbers (i.e. 3 means the 4th element of the ordered input - for clustering). The array maps a correspondence between - cluster elements (which are implicitly associated with the - position in the array) with centroids, i. e. defines clusters. - For instance: + the variables a,b,c,...,z are cluster centroids, here as cluster + element numbers (i.e. 3 means the 4th element of the ordered input + for clustering). The array maps a correspondence between + cluster elements (which are implicitly associated with the + position in the array) with centroids, i. e. defines clusters. + For instance: - [ 1, 1, 1, 4, 4, 5 ] + [ 1, 1, 1, 4, 4, 5 ] - means that elements 0, 1, 2 form a cluster which has 1 as centroid, - elements 3 and 4 form a cluster which has 4 as centroid, and - element 5 has its own cluster. + means that elements 0, 1, 2 form a cluster which has 1 as centroid, + elements 3 and 4 form a cluster which has 4 as centroid, and + element 5 has its own cluster. - Parameters - ---------- + Parameters + ---------- - elements : iterable of ints or None - clustering results. See the previous description for details + elements : iterable of ints or None + clustering results. See the previous description for details - metadata : {str:list, str:list,...} or None - metadata for the data elements. The list must be of the same - size as the elements array, with one value per element. + metadata : {str:list, str:list,...} or None + metadata for the data elements. The list must be of the same + size as the elements array, with one value per element. """ idn = 0 @@ -198,9 +204,10 @@ def __init__(self, elements=None, metadata=None): centroids = np.unique(elements_array) for i in centroids: if elements[i] != i: - raise ValueError("element {0}, which is a centroid, doesn't " - "belong to its own cluster".format( - elements[i])) + raise ValueError( + "element {0}, which is a centroid, doesn't " + "belong to its own cluster".format(elements[i]) + ) for c in centroids: this_metadata = {} this_array = np.where(elements_array == c) @@ -208,8 +215,13 @@ def __init__(self, elements=None, metadata=None): for k, v in metadata.items(): this_metadata[k] = np.asarray(v)[this_array] self.clusters.append( - Cluster(elem_list=this_array[0], idn=idn, centroid=c, - metadata=this_metadata)) + Cluster( + elem_list=this_array[0], + idn=idn, + centroid=c, + metadata=this_metadata, + ) + ) idn += 1 @@ -259,4 +271,5 @@ def __repr__(self): return "" else: return "".format( - len(self.clusters)) + len(self.clusters) + ) diff --git a/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py b/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py index 8071d5eac4a..9af8d48fa86 100644 --- a/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py +++ b/package/MDAnalysis/analysis/encore/clustering/ClusteringMethod.py @@ -50,8 +50,10 @@ import sklearn.cluster except ImportError: sklearn = None - msg = "sklearn.cluster could not be imported: some functionality will " \ - "not be available in encore.fit_clusters()" + msg = ( + "sklearn.cluster could not be imported: some functionality will " + "not be available in encore.fit_clusters()" + ) warnings.warn(msg, category=ImportWarning) logging.warning(msg) del msg @@ -69,13 +71,13 @@ def encode_centroid_info(clusters, cluster_centers_indices): return values[indices] -class ClusteringMethod (object): +class ClusteringMethod(object): """ Base class for any Clustering Method """ # Whether the method accepts a distance matrix - accepts_distance_matrix=True + accepts_distance_matrix = True def __call__(self, x): """ @@ -90,21 +92,29 @@ def __call__(self, x): Raises ------ NotImplementedError - Method or behavior needs to be defined by a subclass - + Method or behavior needs to be defined by a subclass + """ - raise NotImplementedError("Class {0} doesn't implement __call__()" - .format(self.__class__.__name__)) + raise NotImplementedError( + "Class {0} doesn't implement __call__()".format( + self.__class__.__name__ + ) + ) class AffinityPropagationNative(ClusteringMethod): """ Interface to the natively implemented Affinity propagation procedure. """ - def __init__(self, - damping=0.9, preference=-1.0, - max_iter=500, convergence_iter=50, - add_noise=True): + + def __init__( + self, + damping=0.9, + preference=-1.0, + max_iter=500, + convergence_iter=50, + add_noise=True, + ): """ Parameters ---------- @@ -150,21 +160,24 @@ def __call__(self, distance_matrix): Returns ------- - numpy.array : array, shape(n_elements) + numpy.array : array, shape(n_elements) centroid frames of the clusters for all of the elements .. versionchanged:: 1.0.0 This method no longer returns ``details`` """ clusters = affinityprop.AffinityPropagation( - s=distance_matrix * -1., # invert sign + s=distance_matrix * -1.0, # invert sign preference=self.preference, lam=self.damping, - max_iterations = self.max_iter, - convergence = self.convergence_iter, - noise=int(self.add_noise)) - + max_iterations=self.max_iter, + convergence=self.convergence_iter, + noise=int(self.add_noise), + ) + return clusters + + if sklearn: class AffinityPropagation(ClusteringMethod): @@ -173,10 +186,14 @@ class AffinityPropagation(ClusteringMethod): in sklearn. """ - def __init__(self, - damping=0.9, preference=-1.0, - max_iter=500, convergence_iter=50, - **kwargs): + def __init__( + self, + damping=0.9, + preference=-1.0, + max_iter=500, + convergence_iter=50, + **kwargs, + ): """ Parameters ---------- @@ -204,14 +221,14 @@ def __init__(self, Other keyword arguments are passed to :class:`sklearn.cluster.AffinityPropagation`. """ - self.ap = \ - sklearn.cluster.AffinityPropagation( - damping=damping, - preference=preference, - max_iter=max_iter, - convergence_iter=convergence_iter, - affinity="precomputed", - **kwargs) + self.ap = sklearn.cluster.AffinityPropagation( + damping=damping, + preference=preference, + max_iter=max_iter, + convergence_iter=convergence_iter, + affinity="precomputed", + **kwargs, + ) def __call__(self, distance_matrix): """ @@ -223,35 +240,40 @@ def __call__(self, distance_matrix): Returns ------- - numpy.array : array, shape(n_elements) + numpy.array : array, shape(n_elements) centroid frames of the clusters for all of the elements .. versionchanged:: 1.0.0 This method no longer returns ``details`` """ - logging.info("Starting Affinity Propagation: {0}".format - (self.ap.get_params())) + logging.info( + "Starting Affinity Propagation: {0}".format( + self.ap.get_params() + ) + ) # Convert from distance matrix to similarity matrix similarity_matrix = distance_matrix.as_array() * -1 clusters = self.ap.fit_predict(similarity_matrix) - clusters = encode_centroid_info(clusters, - self.ap.cluster_centers_indices_) - - return clusters - + clusters = encode_centroid_info( + clusters, self.ap.cluster_centers_indices_ + ) + return clusters class DBSCAN(ClusteringMethod): """ Interface to the DBSCAN clustering procedure implemented in sklearn. """ - def __init__(self, - eps=0.5, - min_samples=5, - algorithm="auto", - leaf_size=30, - **kwargs): + + def __init__( + self, + eps=0.5, + min_samples=5, + algorithm="auto", + leaf_size=30, + **kwargs, + ): """ Parameters ---------- @@ -284,12 +306,14 @@ def __init__(self, """ - self.dbscan = sklearn.cluster.DBSCAN(eps=eps, - min_samples = min_samples, - algorithm=algorithm, - leaf_size = leaf_size, - metric="precomputed", - **kwargs) + self.dbscan = sklearn.cluster.DBSCAN( + eps=eps, + min_samples=min_samples, + algorithm=algorithm, + leaf_size=leaf_size, + metric="precomputed", + **kwargs, + ) def __call__(self, distance_matrix): """ @@ -302,23 +326,23 @@ def __call__(self, distance_matrix): Returns ------- - numpy.array : array, shape(n_elements) + numpy.array : array, shape(n_elements) centroid frames of the clusters for all of the elements .. versionchanged:: 1.0.0 This method no longer returns ``details`` """ - logging.info("Starting DBSCAN: {0}".format( - self.dbscan.get_params())) + logging.info( + "Starting DBSCAN: {0}".format(self.dbscan.get_params()) + ) clusters = self.dbscan.fit_predict(distance_matrix.as_array()) if np.min(clusters == -1): clusters += 1 # No centroid information is provided by DBSCAN, so we just # pick random members cluster_representatives = np.unique(clusters, return_index=True)[1] - clusters = encode_centroid_info(clusters, - cluster_representatives) - + clusters = encode_centroid_info(clusters, cluster_representatives) + return clusters class KMeans(ClusteringMethod): @@ -329,17 +353,20 @@ class KMeans(ClusteringMethod): """ Interface to the KMeans clustering procedure implemented in sklearn. """ - def __init__(self, - n_clusters, - max_iter=300, - n_init=10, - init='k-means++', - algorithm="auto", - tol=1e-4, - verbose=False, - random_state=None, - copy_x=True, - **kwargs): + + def __init__( + self, + n_clusters, + max_iter=300, + n_init=10, + init="k-means++", + algorithm="auto", + tol=1e-4, + verbose=False, + random_state=None, + copy_x=True, + **kwargs, + ): """ Parameters ---------- @@ -388,15 +415,17 @@ def __init__(self, the data mean. """ - self.kmeans = sklearn.cluster.KMeans(n_clusters=n_clusters, - max_iter=max_iter, - n_init=n_init, - init=init, - tol=tol, - verbose=verbose, - random_state=random_state, - copy_x=copy_x, - **kwargs) + self.kmeans = sklearn.cluster.KMeans( + n_clusters=n_clusters, + max_iter=max_iter, + n_init=n_init, + init=init, + tol=tol, + verbose=verbose, + random_state=random_state, + copy_x=copy_x, + **kwargs, + ) def __call__(self, coordinates): """ @@ -409,18 +438,18 @@ def __call__(self, coordinates): Returns ------- - numpy.array : array, shape(n_elements) + numpy.array : array, shape(n_elements) centroid frames of the clusters for all of the elements .. versionchanged:: 1.0.0 This method no longer returns ``details`` """ - logging.info("Starting Kmeans: {0}".format( - (self.kmeans.get_params()))) + logging.info( + "Starting Kmeans: {0}".format((self.kmeans.get_params())) + ) clusters = self.kmeans.fit_predict(coordinates) distances = self.kmeans.transform(coordinates) cluster_center_indices = np.argmin(distances, axis=0) - clusters = encode_centroid_info(clusters, - cluster_center_indices) - + clusters = encode_centroid_info(clusters, cluster_center_indices) + return clusters diff --git a/package/MDAnalysis/analysis/encore/clustering/__init__.py b/package/MDAnalysis/analysis/encore/clustering/__init__.py index 33f828ce5f4..69527468748 100644 --- a/package/MDAnalysis/analysis/encore/clustering/__init__.py +++ b/package/MDAnalysis/analysis/encore/clustering/__init__.py @@ -26,8 +26,9 @@ from .ClusteringMethod import AffinityPropagationNative from .ClusterCollection import ClusterCollection, Cluster -__all__ = ['ClusterCollection', 'Cluster', 'AffinityPropagationNative'] +__all__ = ["ClusterCollection", "Cluster", "AffinityPropagationNative"] if ClusteringMethod.sklearn: from .ClusteringMethod import AffinityPropagation, DBSCAN - __all__ += ['AffinityPropagation', 'DBSCAN'] + + __all__ += ["AffinityPropagation", "DBSCAN"] diff --git a/package/MDAnalysis/analysis/encore/clustering/cluster.py b/package/MDAnalysis/analysis/encore/clustering/cluster.py index 3bffa490236..2173a8d207d 100644 --- a/package/MDAnalysis/analysis/encore/clustering/cluster.py +++ b/package/MDAnalysis/analysis/encore/clustering/cluster.py @@ -160,11 +160,11 @@ def cluster( method = ClusteringMethod.AffinityPropagationNative() # Internally, ensembles are always transformed to a list of lists if ensembles is not None: - if not hasattr(ensembles, '__iter__'): + if not hasattr(ensembles, "__iter__"): ensembles = [ensembles] ensembles_list = ensembles - if not hasattr(ensembles[0], '__iter__'): + if not hasattr(ensembles[0], "__iter__"): ensembles_list = [ensembles] # Calculate merged ensembles and transfer to memory @@ -176,35 +176,41 @@ def cluster( merged_ensembles.append(merge_universes(ensembles)) methods = method - if not hasattr(method, '__iter__'): + if not hasattr(method, "__iter__"): methods = [method] # Check whether any of the clustering methods can make use of a distance # matrix - any_method_accept_distance_matrix = \ - np.any([_method.accepts_distance_matrix for _method in methods]) + any_method_accept_distance_matrix = np.any( + [_method.accepts_distance_matrix for _method in methods] + ) # If distance matrices are provided, check that it matches the number # of ensembles if distance_matrix: - if not hasattr(distance_matrix, '__iter__'): + if not hasattr(distance_matrix, "__iter__"): distance_matrix = [distance_matrix] - if ensembles is not None and \ - len(distance_matrix) != len(merged_ensembles): - raise ValueError("Dimensions of provided list of distance matrices " - "does not match that of provided list of " - "ensembles: {0} vs {1}" - .format(len(distance_matrix), - len(merged_ensembles))) + if ensembles is not None and len(distance_matrix) != len( + merged_ensembles + ): + raise ValueError( + "Dimensions of provided list of distance matrices " + "does not match that of provided list of " + "ensembles: {0} vs {1}".format( + len(distance_matrix), len(merged_ensembles) + ) + ) else: # Calculate distance matrices for all merged ensembles - if not provided if any_method_accept_distance_matrix: distance_matrix = [] for merged_ensemble in merged_ensembles: - distance_matrix.append(get_distance_matrix(merged_ensemble, - select=select, - **kwargs)) + distance_matrix.append( + get_distance_matrix( + merged_ensemble, select=select, **kwargs + ) + ) args = [] for method in methods: @@ -212,11 +218,14 @@ def cluster( args += [(d,) for d in distance_matrix] else: for merged_ensemble in merged_ensembles: - coordinates = merged_ensemble.trajectory.timeseries(order="fac") + coordinates = merged_ensemble.trajectory.timeseries( + order="fac" + ) # Flatten coordinate matrix into n_frame x n_coordinates - coordinates = np.reshape(coordinates, - (coordinates.shape[0], -1)) + coordinates = np.reshape( + coordinates, (coordinates.shape[0], -1) + ) args.append((coordinates,)) @@ -231,14 +240,16 @@ def cluster( if ensembles is not None: ensemble_assignment = [] for i, ensemble in enumerate(ensembles): - ensemble_assignment += [i+1]*len(ensemble.trajectory) + ensemble_assignment += [i + 1] * len(ensemble.trajectory) ensemble_assignment = np.array(ensemble_assignment) - metadata = {'ensemble_membership': ensemble_assignment} + metadata = {"ensemble_membership": ensemble_assignment} # Create clusters collections from clustering results, # one for each cluster. None if clustering didn't work. - ccs = [ClusterCollection(clusters[1], - metadata=metadata) for clusters in results] + ccs = [ + ClusterCollection(clusters[1], metadata=metadata) + for clusters in results + ] if allow_collapsed_result and len(ccs) == 1: ccs = ccs[0] diff --git a/package/MDAnalysis/analysis/encore/confdistmatrix.py b/package/MDAnalysis/analysis/encore/confdistmatrix.py index 739d715865f..95b22bf0563 100644 --- a/package/MDAnalysis/analysis/encore/confdistmatrix.py +++ b/package/MDAnalysis/analysis/encore/confdistmatrix.py @@ -57,11 +57,18 @@ class to compute an RMSD matrix in such a way is also available. from .utils import TriangularMatrix, trm_indices -def conformational_distance_matrix(ensemble, - conf_dist_function, select="", - superimposition_select="", n_jobs=1, pairwise_align=True, weights='mass', - metadata=True, verbose=False, - max_nbytes=None): +def conformational_distance_matrix( + ensemble, + conf_dist_function, + select="", + superimposition_select="", + n_jobs=1, + pairwise_align=True, + weights="mass", + metadata=True, + verbose=False, + max_nbytes=None, +): """ Run the conformational distance matrix calculation. args and kwargs are passed to conf_dist_function. @@ -104,33 +111,44 @@ def conformational_distance_matrix(ensemble, """ # framesn: number of frames - framesn = len(ensemble.trajectory.timeseries( - ensemble.select_atoms(select), order='fac')) + framesn = len( + ensemble.trajectory.timeseries( + ensemble.select_atoms(select), order="fac" + ) + ) # Prepare metadata recarray if metadata: - metadata = np.array([(gethostname(), - getuser(), - str(datetime.now()), - ensemble.filename, - framesn, - pairwise_align, - select, - weights=='mass')], - dtype=[('host', object), - ('user', object), - ('date', object), - ('topology file', object), - ('number of frames', int), - ('pairwise superimposition', bool), - ('superimposition subset', object), - ('mass-weighted', bool)]) + metadata = np.array( + [ + ( + gethostname(), + getuser(), + str(datetime.now()), + ensemble.filename, + framesn, + pairwise_align, + select, + weights == "mass", + ) + ], + dtype=[ + ("host", object), + ("user", object), + ("date", object), + ("topology file", object), + ("number of frames", int), + ("pairwise superimposition", bool), + ("superimposition subset", object), + ("mass-weighted", bool), + ], + ) # Prepare alignment subset coordinates as necessary rmsd_coordinates = ensemble.trajectory.timeseries( - ensemble.select_atoms(select), - order='fac') + ensemble.select_atoms(select), order="fac" + ) if pairwise_align: if superimposition_select: @@ -139,31 +157,45 @@ def conformational_distance_matrix(ensemble, subset_select = select fitting_coordinates = ensemble.trajectory.timeseries( - ensemble.select_atoms(subset_select), - order='fac') + ensemble.select_atoms(subset_select), order="fac" + ) else: fitting_coordinates = None - if not isinstance(weights, (list, tuple, np.ndarray)) and weights == 'mass': + if ( + not isinstance(weights, (list, tuple, np.ndarray)) + and weights == "mass" + ): weights = ensemble.select_atoms(select).masses.astype(np.float64) if pairwise_align: - subset_weights = ensemble.select_atoms(subset_select).masses.astype(np.float64) + subset_weights = ensemble.select_atoms( + subset_select + ).masses.astype(np.float64) else: subset_weights = None elif weights is None: - weights = np.ones((ensemble.trajectory.timeseries( - ensemble.select_atoms(select))[0].shape[0])).astype(np.float64) + weights = np.ones( + ( + ensemble.trajectory.timeseries(ensemble.select_atoms(select))[ + 0 + ].shape[0] + ) + ).astype(np.float64) if pairwise_align: - subset_weights = np.ones((fit_coords[0].shape[0])).astype(np.float64) + subset_weights = np.ones((fit_coords[0].shape[0])).astype( + np.float64 + ) else: subset_weights = None else: if pairwise_align: if len(weights) != 2: - raise RuntimeError("used pairwise alignment with custom " - "weights. Please provide 2 tuple with " - "weights for 'select' and " - "'superimposition_select'") + raise RuntimeError( + "used pairwise alignment with custom " + "weights. Please provide 2 tuple with " + "weights for 'select' and " + "'superimposition_select'" + ) subset_weights = weights[1] weights = weights[0] else: @@ -176,24 +208,38 @@ def conformational_distance_matrix(ensemble, # Initialize workers. Simple worker doesn't perform fitting, # fitter worker does. indices = trm_indices((0, 0), (framesn - 1, framesn - 1)) - Parallel(n_jobs=n_jobs, verbose=verbose, require='sharedmem', - max_nbytes=max_nbytes)(delayed(conf_dist_function)( - np.int64(element), - rmsd_coordinates, - distmat, - weights, - fitting_coordinates, - subset_weights) for element in indices) - + Parallel( + n_jobs=n_jobs, + verbose=verbose, + require="sharedmem", + max_nbytes=max_nbytes, + )( + delayed(conf_dist_function)( + np.int64(element), + rmsd_coordinates, + distmat, + weights, + fitting_coordinates, + subset_weights, + ) + for element in indices + ) # When the workers have finished, return a TriangularMatrix object return TriangularMatrix(distmat, metadata=metadata) -def set_rmsd_matrix_elements(tasks, coords, rmsdmat, weights, fit_coords=None, - fit_weights=None, *args, **kwargs): - - ''' +def set_rmsd_matrix_elements( + tasks, + coords, + rmsdmat, + weights, + fit_coords=None, + fit_weights=None, + *args, + **kwargs, +): + """ RMSD Matrix calculator Parameters @@ -223,51 +269,60 @@ def set_rmsd_matrix_elements(tasks, coords, rmsdmat, weights, fit_coords=None, fit_weights : numpy.array. optional Array of atomic weights, having the same order as the fit_coords array - ''' + """ i, j = tasks if fit_coords is None and fit_weights is None: sumweights = np.sum(weights) - rmsdmat[(i + 1) * i // 2 + j] = PureRMSD(coords[i].astype(np.float64), - coords[j].astype(np.float64), - coords[j].shape[0], - weights, - sumweights) + rmsdmat[(i + 1) * i // 2 + j] = PureRMSD( + coords[i].astype(np.float64), + coords[j].astype(np.float64), + coords[j].shape[0], + weights, + sumweights, + ) elif fit_coords is not None and fit_weights is not None: sumweights = np.sum(weights) subset_weights = np.asarray(fit_weights) / np.mean(fit_weights) - com_i = np.average(fit_coords[i], axis=0, - weights=fit_weights) + com_i = np.average(fit_coords[i], axis=0, weights=fit_weights) translated_i = coords[i] - com_i subset1_coords = fit_coords[i] - com_i - com_j = np.average(fit_coords[j], axis=0, - weights=fit_weights) + com_j = np.average(fit_coords[j], axis=0, weights=fit_weights) translated_j = coords[j] - com_j subset2_coords = fit_coords[j] - com_j - rotamat = rotation_matrix(subset1_coords, subset2_coords, - subset_weights)[0] + rotamat = rotation_matrix( + subset1_coords, subset2_coords, subset_weights + )[0] rotated_i = np.transpose(np.dot(rotamat, np.transpose(translated_i))) rmsdmat[(i + 1) * i // 2 + j] = PureRMSD( - rotated_i.astype(np.float64), translated_j.astype(np.float64), - coords[j].shape[0], weights, sumweights) + rotated_i.astype(np.float64), + translated_j.astype(np.float64), + coords[j].shape[0], + weights, + sumweights, + ) else: - raise TypeError("Both fit_coords and fit_weights must be specified " - "if one of them is given") - - -def get_distance_matrix(ensemble, - select="name CA", - load_matrix=None, - save_matrix=None, - superimpose=True, - superimposition_subset="name CA", - weights='mass', - n_jobs=1, - max_nbytes=None, - verbose=False, - *conf_dist_args, - **conf_dist_kwargs): + raise TypeError( + "Both fit_coords and fit_weights must be specified " + "if one of them is given" + ) + + +def get_distance_matrix( + ensemble, + select="name CA", + load_matrix=None, + save_matrix=None, + superimpose=True, + superimposition_subset="name CA", + weights="mass", + n_jobs=1, + max_nbytes=None, + verbose=False, + *conf_dist_args, + **conf_dist_kwargs, +): """ Retrieves or calculates the conformational distance (RMSD) matrix. The distance matrix is calculated between all the frames of all @@ -321,60 +376,76 @@ def get_distance_matrix(ensemble, # Load the matrix if required if load_matrix: logging.info( - " Loading similarity matrix from: {0}".format(load_matrix)) - confdistmatrix = \ - TriangularMatrix( - size=ensemble.trajectory.timeseries( - ensemble.select_atoms(select), - order='fac').shape[0], - loadfile=load_matrix) + " Loading similarity matrix from: {0}".format(load_matrix) + ) + confdistmatrix = TriangularMatrix( + size=ensemble.trajectory.timeseries( + ensemble.select_atoms(select), order="fac" + ).shape[0], + loadfile=load_matrix, + ) logging.info(" Done!") for key in confdistmatrix.metadata.dtype.names: - logging.info(" {0} : {1}".format( - key, str(confdistmatrix.metadata[key][0]))) + logging.info( + " {0} : {1}".format( + key, str(confdistmatrix.metadata[key][0]) + ) + ) # Check matrix size for consistency - if not confdistmatrix.size == \ - ensemble.trajectory.timeseries( - ensemble.select_atoms(select), - order='fac').shape[0]: + if ( + not confdistmatrix.size + == ensemble.trajectory.timeseries( + ensemble.select_atoms(select), order="fac" + ).shape[0] + ): logging.error( "ERROR: The size of the loaded matrix and of the ensemble" - " do not match") + " do not match" + ) return None - # Calculate the matrix else: # Transfer universe to memory to ensure timeseries() support ensemble.transfer_to_memory() - if not isinstance(weights, (list, tuple, np.ndarray)) and weights == 'mass': - weight_type = 'Mass' + if ( + not isinstance(weights, (list, tuple, np.ndarray)) + and weights == "mass" + ): + weight_type = "Mass" elif weights is None: - weight_type = 'None' + weight_type = "None" else: - weight_type = 'Custom' + weight_type = "Custom" + logging.info( + " Perform pairwise alignment: {0}".format(str(superimpose)) + ) logging.info( - " Perform pairwise alignment: {0}".format(str(superimpose))) - logging.info(" weighted alignment and RMSD: {0}".format(weight_type)) + " weighted alignment and RMSD: {0}".format(weight_type) + ) if superimpose: logging.info( - " Atoms subset for alignment: {0}" - .format(superimposition_subset)) + " Atoms subset for alignment: {0}".format( + superimposition_subset + ) + ) logging.info(" Calculating similarity matrix . . .") # Use superimposition subset, if necessary. If the pairwise alignment # is not required, it will not be performed anyway. - confdistmatrix = conformational_distance_matrix(ensemble, - conf_dist_function=set_rmsd_matrix_elements, - select=select, - pairwise_align=superimpose, - weights=weights, - n_jobs=n_jobs, - max_nbytes=max_nbytes, - verbose=verbose) + confdistmatrix = conformational_distance_matrix( + ensemble, + conf_dist_function=set_rmsd_matrix_elements, + select=select, + pairwise_align=superimpose, + weights=weights, + n_jobs=n_jobs, + max_nbytes=max_nbytes, + verbose=verbose, + ) logging.info(" Done!") diff --git a/package/MDAnalysis/analysis/encore/covariance.py b/package/MDAnalysis/analysis/encore/covariance.py index 5c7b3b363a5..08e014e315c 100644 --- a/package/MDAnalysis/analysis/encore/covariance.py +++ b/package/MDAnalysis/analysis/encore/covariance.py @@ -39,6 +39,7 @@ """ import numpy as np + def ml_covariance_estimator(coordinates, reference_coordinates=None): """ Standard maximum likelihood estimator of the covariance matrix. @@ -70,17 +71,17 @@ def ml_covariance_estimator(coordinates, reference_coordinates=None): coordinates_offset = coordinates - np.average(coordinates, axis=0) # Calculate covariance manually - coordinates_cov = np.zeros((coordinates.shape[1], - coordinates.shape[1])) + coordinates_cov = np.zeros((coordinates.shape[1], coordinates.shape[1])) for frame in coordinates_offset: coordinates_cov += np.outer(frame, frame) coordinates_cov /= coordinates.shape[0] return coordinates_cov -def shrinkage_covariance_estimator( coordinates, - reference_coordinates=None, - shrinkage_parameter=None): + +def shrinkage_covariance_estimator( + coordinates, reference_coordinates=None, shrinkage_parameter=None +): """ Shrinkage estimator of the covariance matrix using the method described in @@ -125,8 +126,11 @@ def shrinkage_covariance_estimator( coordinates, xmkt = np.average(x, axis=1) # Call maximum likelihood estimator (note the additional column) - sample = ml_covariance_estimator(np.hstack([x, xmkt[:, np.newaxis]]), 0)\ - * (t-1)/float(t) + sample = ( + ml_covariance_estimator(np.hstack([x, xmkt[:, np.newaxis]]), 0) + * (t - 1) + / float(t) + ) # Split covariance matrix into components covmkt = sample[0:n, n] @@ -134,53 +138,59 @@ def shrinkage_covariance_estimator( coordinates, sample = sample[:n, :n] # Prior - prior = np.outer(covmkt, covmkt)/varmkt + prior = np.outer(covmkt, covmkt) / varmkt prior[np.ma.make_mask(np.eye(n))] = np.diag(sample) # If shrinkage parameter is not set, estimate it if shrinkage_parameter is None: # Frobenius norm - c = np.linalg.norm(sample - prior, ord='fro')**2 + c = np.linalg.norm(sample - prior, ord="fro") ** 2 y = x**2 - p = 1/float(t)*np.sum(np.dot(np.transpose(y), y))\ - - np.sum(np.sum(sample**2)) - rdiag = 1/float(t)*np.sum(np.sum(y**2))\ - - np.sum(np.diag(sample)**2) + p = 1 / float(t) * np.sum(np.dot(np.transpose(y), y)) - np.sum( + np.sum(sample**2) + ) + rdiag = 1 / float(t) * np.sum(np.sum(y**2)) - np.sum( + np.diag(sample) ** 2 + ) z = x * np.repeat(xmkt[:, np.newaxis], n, axis=1) - v1 = 1/float(t) * np.dot(np.transpose(y), z) \ - - np.repeat(covmkt[:, np.newaxis], n, axis=1)*sample - roff1 = (np.sum( - v1*np.transpose( - np.repeat( - covmkt[:, np.newaxis], n, axis=1) - ) - )/varmkt - - np.sum(np.diag(v1)*covmkt)/varmkt) - v3 = 1/float(t)*np.dot(np.transpose(z), z) - varmkt*sample - roff3 = (np.sum(v3*np.outer(covmkt, covmkt))/varmkt**2 - - np.sum(np.diag(v3)*covmkt**2)/varmkt**2) - roff = 2*roff1-roff3 - r = rdiag+roff + v1 = ( + 1 / float(t) * np.dot(np.transpose(y), z) + - np.repeat(covmkt[:, np.newaxis], n, axis=1) * sample + ) + roff1 = ( + np.sum( + v1 * np.transpose(np.repeat(covmkt[:, np.newaxis], n, axis=1)) + ) + / varmkt + - np.sum(np.diag(v1) * covmkt) / varmkt + ) + v3 = 1 / float(t) * np.dot(np.transpose(z), z) - varmkt * sample + roff3 = ( + np.sum(v3 * np.outer(covmkt, covmkt)) / varmkt**2 + - np.sum(np.diag(v3) * covmkt**2) / varmkt**2 + ) + roff = 2 * roff1 - roff3 + r = rdiag + roff # Shrinkage constant - k = (p-r)/c - shrinkage_parameter = max(0, min(1, k/float(t))) + k = (p - r) / c + shrinkage_parameter = max(0, min(1, k / float(t))) # calculate covariance matrix - sigma = shrinkage_parameter*prior+(1-shrinkage_parameter)*sample + sigma = shrinkage_parameter * prior + (1 - shrinkage_parameter) * sample return sigma - - -def covariance_matrix(ensemble, - select="name CA", - estimator=shrinkage_covariance_estimator, - weights='mass', - reference=None): +def covariance_matrix( + ensemble, + select="name CA", + estimator=shrinkage_covariance_estimator, + weights="mass", + reference=None, +): """ Calculates (optionally mass weighted) covariance matrix @@ -209,8 +219,8 @@ def covariance_matrix(ensemble, """ # Extract coordinates from ensemble coordinates = ensemble.trajectory.timeseries( - ensemble.select_atoms(select), - order='fac') + ensemble.select_atoms(select), order="fac" + ) # Flatten coordinate matrix into n_frame x n_coordinates coordinates = np.reshape(coordinates, (coordinates.shape[0], -1)) @@ -230,7 +240,10 @@ def covariance_matrix(ensemble, # Optionally correct with weights if weights is not None: # Calculate mass-weighted covariance matrix - if not isinstance(weights, (list, tuple, np.ndarray)) and weights == 'mass': + if ( + not isinstance(weights, (list, tuple, np.ndarray)) + and weights == "mass" + ): if select: weights = ensemble.select_atoms(select).masses else: @@ -241,13 +254,15 @@ def covariance_matrix(ensemble, else: req_len = ensemble.atoms.n_atoms if req_len != len(weights): - raise ValueError("number of weights is unequal to number of " - "atoms in ensemble") + raise ValueError( + "number of weights is unequal to number of " + "atoms in ensemble" + ) # broadcast to a (len(weights), 3) array weights = np.repeat(weights, 3) - weight_matrix = np.sqrt(np.identity(len(weights))*weights) + weight_matrix = np.sqrt(np.identity(len(weights)) * weights) sigma = np.dot(weight_matrix, np.dot(sigma, weight_matrix)) return sigma diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py index 50349960bdd..3ca43202f5e 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/DimensionalityReductionMethod.py @@ -50,18 +50,22 @@ except ImportError: sklearn = None import warnings - warnings.warn("sklearn.decomposition could not be imported: some " - "functionality will not be available in " - "encore.dimensionality_reduction()", category=ImportWarning) + warnings.warn( + "sklearn.decomposition could not be imported: some " + "functionality will not be available in " + "encore.dimensionality_reduction()", + category=ImportWarning, + ) -class DimensionalityReductionMethod (object): + +class DimensionalityReductionMethod(object): """ Base class for any Dimensionality Reduction Method """ # Whether the method accepts a distance matrix - accepts_distance_matrix=True + accepts_distance_matrix = True def __call__(self, x): """ @@ -80,21 +84,27 @@ def __call__(self, x): coordinates in reduced space """ - raise NotImplementedError("Class {0} doesn't implement __call__()" - .format(self.__class__.__name__)) + raise NotImplementedError( + "Class {0} doesn't implement __call__()".format( + self.__class__.__name__ + ) + ) class StochasticProximityEmbeddingNative(DimensionalityReductionMethod): """ Interface to the natively implemented Affinity propagation procedure. """ - def __init__(self, - dimension = 2, - distance_cutoff = 1.5, - min_lam = 0.1, - max_lam = 2.0, - ncycle = 100, - nstep = 10000,): + + def __init__( + self, + dimension=2, + distance_cutoff=1.5, + min_lam=0.1, + max_lam=2.0, + ncycle=100, + nstep=10000, + ): """ Parameters ---------- @@ -140,21 +150,21 @@ def __call__(self, distance_matrix): coordinates in reduced space """ - final_stress, coordinates = \ + final_stress, coordinates = ( stochasticproxembed.StochasticProximityEmbedding( - s=distance_matrix, - rco=self.distance_cutoff, - dim=self.dimension, - minlam = self.min_lam, - maxlam = self.max_lam, - ncycle = self.ncycle, - nstep = self.nstep, - stressfreq = self.stressfreq + s=distance_matrix, + rco=self.distance_cutoff, + dim=self.dimension, + minlam=self.min_lam, + maxlam=self.max_lam, + ncycle=self.ncycle, + nstep=self.nstep, + stressfreq=self.stressfreq, + ) ) return coordinates, {"final_stress": final_stress} - if sklearn: class PrincipalComponentAnalysis(DimensionalityReductionMethod): @@ -166,9 +176,7 @@ class PrincipalComponentAnalysis(DimensionalityReductionMethod): # Whether the method accepts a distance matrix accepts_distance_matrix = False - def __init__(self, - dimension = 2, - **kwargs): + def __init__(self, dimension=2, **kwargs): """ Parameters ---------- @@ -177,8 +185,9 @@ def __init__(self, Number of dimensions to which the conformational space will be reduced to (default is 3). """ - self.pca = sklearn.decomposition.PCA(n_components=dimension, - **kwargs) + self.pca = sklearn.decomposition.PCA( + n_components=dimension, **kwargs + ) def __call__(self, coordinates): """ diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py index fefd1b85acd..a2ca041d305 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/__init__.py @@ -24,8 +24,9 @@ from .DimensionalityReductionMethod import StochasticProximityEmbeddingNative -__all__ = ['StochasticProximityEmbeddingNative'] +__all__ = ["StochasticProximityEmbeddingNative"] if DimensionalityReductionMethod.sklearn: from .DimensionalityReductionMethod import PrincipalComponentAnalysis - __all__ += ['PrincipalComponentAnalysis'] + + __all__ += ["PrincipalComponentAnalysis"] diff --git a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py index 82e805c91bf..d1e05e1cd2f 100644 --- a/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py +++ b/package/MDAnalysis/analysis/encore/dimensionality_reduction/reduce_dimensionality.py @@ -41,7 +41,8 @@ from ..confdistmatrix import get_distance_matrix from ..utils import ParallelCalculation, merge_universes from ..dimensionality_reduction.DimensionalityReductionMethod import ( - StochasticProximityEmbeddingNative) + StochasticProximityEmbeddingNative, +) def reduce_dimensionality( @@ -157,11 +158,11 @@ def reduce_dimensionality( if method is None: method = StochasticProximityEmbeddingNative() if ensembles is not None: - if not hasattr(ensembles, '__iter__'): + if not hasattr(ensembles, "__iter__"): ensembles = [ensembles] ensembles_list = ensembles - if not hasattr(ensembles[0], '__iter__'): + if not hasattr(ensembles[0], "__iter__"): ensembles_list = [ensembles] # Calculate merged ensembles and transfer to memory @@ -173,37 +174,40 @@ def reduce_dimensionality( merged_ensembles.append(merge_universes(ensembles)) methods = method - if not hasattr(method, '__iter__'): + if not hasattr(method, "__iter__"): methods = [method] # Check whether any of the methods can make use of a distance matrix - any_method_accept_distance_matrix = \ - np.any([_method.accepts_distance_matrix for _method in - methods]) - - + any_method_accept_distance_matrix = np.any( + [_method.accepts_distance_matrix for _method in methods] + ) # If distance matrices are provided, check that it matches the number # of ensembles if distance_matrix: - if not hasattr(distance_matrix, '__iter__'): + if not hasattr(distance_matrix, "__iter__"): distance_matrix = [distance_matrix] - if ensembles is not None and \ - len(distance_matrix) != len(merged_ensembles): - raise ValueError("Dimensions of provided list of distance matrices " - "does not match that of provided list of " - "ensembles: {0} vs {1}" - .format(len(distance_matrix), - len(merged_ensembles))) + if ensembles is not None and len(distance_matrix) != len( + merged_ensembles + ): + raise ValueError( + "Dimensions of provided list of distance matrices " + "does not match that of provided list of " + "ensembles: {0} vs {1}".format( + len(distance_matrix), len(merged_ensembles) + ) + ) else: # Calculate distance matrices for all merged ensembles - if not provided if any_method_accept_distance_matrix: distance_matrix = [] for merged_ensemble in merged_ensembles: - distance_matrix.append(get_distance_matrix(merged_ensemble, - select=select, - **kwargs)) + distance_matrix.append( + get_distance_matrix( + merged_ensemble, select=select, **kwargs + ) + ) args = [] for method in methods: @@ -211,11 +215,14 @@ def reduce_dimensionality( args += [(d,) for d in distance_matrix] else: for merged_ensemble in merged_ensembles: - coordinates = merged_ensemble.trajectory.timeseries(order="fac") + coordinates = merged_ensemble.trajectory.timeseries( + order="fac" + ) # Flatten coordinate matrix into n_frame x n_coordinates - coordinates = np.reshape(coordinates, - (coordinates.shape[0], -1)) + coordinates = np.reshape( + coordinates, (coordinates.shape[0], -1) + ) args.append((coordinates,)) @@ -230,16 +237,16 @@ def reduce_dimensionality( if ensembles is not None: ensemble_assignment = [] for i, ensemble in enumerate(ensembles): - ensemble_assignment += [i+1]*len(ensemble.trajectory) + ensemble_assignment += [i + 1] * len(ensemble.trajectory) ensemble_assignment = np.array(ensemble_assignment) - details['ensemble_membership'] = ensemble_assignment + details["ensemble_membership"] = ensemble_assignment coordinates = [] for result in results: coordinates.append(result[1][0]) # details.append(result[1][1]) - if allow_collapsed_result and len(coordinates)==1: + if allow_collapsed_result and len(coordinates) == 1: coordinates = coordinates[0] # details = details[0] diff --git a/package/MDAnalysis/analysis/encore/similarity.py b/package/MDAnalysis/analysis/encore/similarity.py index c9a8ff1486c..17b9fb28860 100644 --- a/package/MDAnalysis/analysis/encore/similarity.py +++ b/package/MDAnalysis/analysis/encore/similarity.py @@ -180,24 +180,32 @@ from ...coordinates.memory import MemoryReader from .confdistmatrix import get_distance_matrix -from .bootstrap import (get_distance_matrix_bootstrap_samples, - get_ensemble_bootstrap_samples) +from .bootstrap import ( + get_distance_matrix_bootstrap_samples, + get_ensemble_bootstrap_samples, +) from .clustering.cluster import cluster from .clustering.ClusteringMethod import AffinityPropagationNative from .dimensionality_reduction.DimensionalityReductionMethod import ( - StochasticProximityEmbeddingNative) + StochasticProximityEmbeddingNative, +) from .dimensionality_reduction.reduce_dimensionality import ( - reduce_dimensionality) + reduce_dimensionality, +) from .covariance import ( - covariance_matrix, ml_covariance_estimator, shrinkage_covariance_estimator) + covariance_matrix, + ml_covariance_estimator, + shrinkage_covariance_estimator, +) from .utils import merge_universes from .utils import trm_indices_diag, trm_indices_nodiag # Low boundary value for log() argument - ensure no nans -EPSILON = 1E-15 +EPSILON = 1e-15 xlogy = np.vectorize( - lambda x, y: 0.0 if (x <= EPSILON and y <= EPSILON) else x * np.log(y)) + lambda x, y: 0.0 if (x <= EPSILON and y <= EPSILON) else x * np.log(y) +) def discrete_kullback_leibler_divergence(pA, pB): @@ -242,16 +250,15 @@ def discrete_jensen_shannon_divergence(pA, pB): djs : float Discrete Jensen-Shannon divergence -""" - return 0.5 * (discrete_kullback_leibler_divergence(pA, (pA + pB) * 0.5) + - discrete_kullback_leibler_divergence(pB, (pA + pB) * 0.5)) + """ + return 0.5 * ( + discrete_kullback_leibler_divergence(pA, (pA + pB) * 0.5) + + discrete_kullback_leibler_divergence(pB, (pA + pB) * 0.5) + ) # calculate harmonic similarity -def harmonic_ensemble_similarity(sigma1, - sigma2, - x1, - x2): +def harmonic_ensemble_similarity(sigma1, sigma2, x1, x2): """ Calculate the harmonic ensemble similarity measure as defined in :footcite:p:`Tiberti2015`. @@ -288,18 +295,22 @@ def harmonic_ensemble_similarity(sigma1, d_avg = x1 - x2 # Distance measure - trace = np.trace(np.dot(sigma1, sigma2_inv) + - np.dot(sigma2, sigma1_inv) - - 2 * np.identity(sigma1.shape[0])) - - d_hes = 0.25 * (np.dot(np.transpose(d_avg), - np.dot(sigma1_inv + sigma2_inv, - d_avg)) + trace) + trace = np.trace( + np.dot(sigma1, sigma2_inv) + + np.dot(sigma2, sigma1_inv) + - 2 * np.identity(sigma1.shape[0]) + ) + + d_hes = 0.25 * ( + np.dot(np.transpose(d_avg), np.dot(sigma1_inv + sigma2_inv, d_avg)) + + trace + ) return d_hes -def clustering_ensemble_similarity(cc, ens1, ens1_id, ens2, ens2_id, - select="name CA"): +def clustering_ensemble_similarity( + cc, ens1, ens1_id, ens2, ens2_id, select="name CA" +): """Clustering ensemble similarity: calculate the probability densities from the clusters and calculate discrete Jensen-Shannon divergence. @@ -332,16 +343,26 @@ def clustering_ensemble_similarity(cc, ens1, ens1_id, ens2, ens2_id, Jensen-Shannon divergence between the two ensembles, as calculated by the clustering ensemble similarity method """ - ens1_coordinates = ens1.trajectory.timeseries(ens1.select_atoms(select), - order='fac') - ens2_coordinates = ens2.trajectory.timeseries(ens2.select_atoms(select), - order='fac') - tmpA = np.array([np.where(c.metadata['ensemble_membership'] == ens1_id)[ - 0].shape[0] / float(ens1_coordinates.shape[0]) for - c in cc]) - tmpB = np.array([np.where(c.metadata['ensemble_membership'] == ens2_id)[ - 0].shape[0] / float(ens2_coordinates.shape[0]) for - c in cc]) + ens1_coordinates = ens1.trajectory.timeseries( + ens1.select_atoms(select), order="fac" + ) + ens2_coordinates = ens2.trajectory.timeseries( + ens2.select_atoms(select), order="fac" + ) + tmpA = np.array( + [ + np.where(c.metadata["ensemble_membership"] == ens1_id)[0].shape[0] + / float(ens1_coordinates.shape[0]) + for c in cc + ] + ) + tmpB = np.array( + [ + np.where(c.metadata["ensemble_membership"] == ens2_id)[0].shape[0] + / float(ens2_coordinates.shape[0]) + for c in cc + ] + ) # Exclude clusters which have 0 elements in both ensembles pA = tmpA[tmpA + tmpB > EPSILON] @@ -350,8 +371,9 @@ def clustering_ensemble_similarity(cc, ens1, ens1_id, ens2, ens2_id, return discrete_jensen_shannon_divergence(pA, pB) -def cumulative_clustering_ensemble_similarity(cc, ens1_id, ens2_id, - ens1_id_min=1, ens2_id_min=1): +def cumulative_clustering_ensemble_similarity( + cc, ens1_id, ens2_id, ens1_id_min=1, ens2_id_min=1 +): """ Calculate clustering ensemble similarity between joined ensembles. This means that, after clustering has been performed, some ensembles are @@ -383,16 +405,28 @@ def cumulative_clustering_ensemble_similarity(cc, ens1_id, ens2_id, Jensen-Shannon divergence between the two ensembles, as calculated by the clustering ensemble similarity method -""" + """ - ensA = [np.where(np.logical_and( - c.metadata['ensemble_membership'] <= ens1_id, - c.metadata['ensemble_membership']) - >= ens1_id_min)[0].shape[0] for c in cc] - ensB = [np.where(np.logical_and( - c.metadata['ensemble_membership'] <= ens2_id, - c.metadata['ensemble_membership']) - >= ens2_id_min)[0].shape[0] for c in cc] + ensA = [ + np.where( + np.logical_and( + c.metadata["ensemble_membership"] <= ens1_id, + c.metadata["ensemble_membership"], + ) + >= ens1_id_min + )[0].shape[0] + for c in cc + ] + ensB = [ + np.where( + np.logical_and( + c.metadata["ensemble_membership"] <= ens2_id, + c.metadata["ensemble_membership"], + ) + >= ens2_id_min + )[0].shape[0] + for c in cc + ] sizeA = float(np.sum(ensA)) sizeB = float(np.sum(ensB)) @@ -406,8 +440,7 @@ def cumulative_clustering_ensemble_similarity(cc, ens1_id, ens2_id, return discrete_jensen_shannon_divergence(pA, pB) -def gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, - nsamples): +def gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, nsamples): """ Generate Kernel Density Estimates (KDE) from embedded spaces and elaborate the coordinates for later use. @@ -452,7 +485,8 @@ def gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, for i in range(1, nensembles + 1): this_embedded = embedded_space.transpose()[ - np.where(np.array(ensemble_assignment) == i)].transpose() + np.where(np.array(ensemble_assignment) == i) + ].transpose() embedded_ensembles.append(this_embedded) kdes.append(scipy.stats.gaussian_kde(this_embedded)) @@ -467,9 +501,16 @@ def gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, return (kdes, resamples, embedded_ensembles) -def dimred_ensemble_similarity(kde1, resamples1, kde2, resamples2, - ln_P1_exp_P1=None, ln_P2_exp_P2=None, - ln_P1P2_exp_P1=None, ln_P1P2_exp_P2=None): +def dimred_ensemble_similarity( + kde1, + resamples1, + kde2, + resamples2, + ln_P1_exp_P1=None, + ln_P2_exp_P2=None, + ln_P1P2_exp_P1=None, + ln_P1P2_exp_P2=None, +): r"""Calculate the Jensen-Shannon divergence according the Dimensionality reduction method. @@ -541,21 +582,38 @@ def dimred_ensemble_similarity(kde1, resamples1, kde2, resamples2, """ - if not ln_P1_exp_P1 and not ln_P2_exp_P2 and not ln_P1P2_exp_P1 and not \ - ln_P1P2_exp_P2: + if ( + not ln_P1_exp_P1 + and not ln_P2_exp_P2 + and not ln_P1P2_exp_P1 + and not ln_P1P2_exp_P2 + ): ln_P1_exp_P1 = np.average(np.log(kde1.evaluate(resamples1))) ln_P2_exp_P2 = np.average(np.log(kde2.evaluate(resamples2))) - ln_P1P2_exp_P1 = np.average(np.log( - 0.5 * (kde1.evaluate(resamples1) + kde2.evaluate(resamples1)))) - ln_P1P2_exp_P2 = np.average(np.log( - 0.5 * (kde1.evaluate(resamples2) + kde2.evaluate(resamples2)))) + ln_P1P2_exp_P1 = np.average( + np.log( + 0.5 * (kde1.evaluate(resamples1) + kde2.evaluate(resamples1)) + ) + ) + ln_P1P2_exp_P2 = np.average( + np.log( + 0.5 * (kde1.evaluate(resamples2) + kde2.evaluate(resamples2)) + ) + ) return 0.5 * ( - ln_P1_exp_P1 - ln_P1P2_exp_P1 + ln_P2_exp_P2 - ln_P1P2_exp_P2) + ln_P1_exp_P1 - ln_P1P2_exp_P1 + ln_P2_exp_P2 - ln_P1P2_exp_P2 + ) -def cumulative_gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, - nsamples, ens_id_min=1, ens_id_max=None): +def cumulative_gen_kde_pdfs( + embedded_space, + ensemble_assignment, + nensembles, + nsamples, + ens_id_min=1, + ens_id_max=None, +): """ Generate Kernel Density Estimates (KDE) from embedded spaces and elaborate the coordinates for later use. However, consider more than @@ -615,9 +673,13 @@ def cumulative_gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, if not ens_id_max: ens_id_max = nensembles + 1 for i in range(ens_id_min, ens_id_max): - this_embedded = embedded_space.transpose()[np.where( - np.logical_and(ensemble_assignment >= ens_id_min, - ensemble_assignment <= i))].transpose() + this_embedded = embedded_space.transpose()[ + np.where( + np.logical_and( + ensemble_assignment >= ens_id_min, ensemble_assignment <= i + ) + ) + ].transpose() embedded_ensembles.append(this_embedded) kdes.append(scipy.stats.gaussian_kde(this_embedded)) @@ -628,8 +690,9 @@ def cumulative_gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, return (kdes, resamples, embedded_ensembles) -def write_output(matrix, base_fname=None, header="", suffix="", - extension="dat"): +def write_output( + matrix, base_fname=None, header="", suffix="", extension="dat" +): """ Write output matrix with a nice format, to stdout and optionally a file. @@ -662,9 +725,9 @@ def write_output(matrix, base_fname=None, header="", suffix="", matrix.square_print(header=header, fname=fname) -def prepare_ensembles_for_convergence_increasing_window(ensemble, - window_size, - select="name CA"): +def prepare_ensembles_for_convergence_increasing_window( + ensemble, window_size, select="name CA" +): """ Generate ensembles to be fed to ces_convergence or dres_convergence from a single ensemble. Basically, the different slices the algorithm @@ -693,8 +756,9 @@ def prepare_ensembles_for_convergence_increasing_window(ensemble, """ - ens_size = ensemble.trajectory.timeseries(ensemble.select_atoms(select), - order='fac').shape[0] + ens_size = ensemble.trajectory.timeseries( + ensemble.select_atoms(select), order="fac" + ).shape[0] rest_slices = ens_size // window_size residuals = ens_size % window_size @@ -706,24 +770,30 @@ def prepare_ensembles_for_convergence_increasing_window(ensemble, slices_n.append(slices_n[-1] + window_size) slices_n.append(slices_n[-1] + residuals + window_size) - for s,sl in enumerate(slices_n[:-1]): - tmp_ensembles.append(mda.Universe( - ensemble.filename, - ensemble.trajectory.timeseries(order='fac') - [slices_n[s]:slices_n[s + 1], :, :], - format=MemoryReader)) + for s, sl in enumerate(slices_n[:-1]): + tmp_ensembles.append( + mda.Universe( + ensemble.filename, + ensemble.trajectory.timeseries(order="fac")[ + slices_n[s] : slices_n[s + 1], :, : + ], + format=MemoryReader, + ) + ) return tmp_ensembles -def hes(ensembles, - select="name CA", - cov_estimator="shrinkage", - weights='mass', - align=False, - estimate_error=False, - bootstrapping_samples=100, - calc_diagonal=False): +def hes( + ensembles, + select="name CA", + cov_estimator="shrinkage", + weights="mass", + align=False, + estimate_error=False, + bootstrapping_samples=100, + calc_diagonal=False, +): r"""Calculates the Harmonic Ensemble Similarity (HES) between ensembles. The HES is calculated with the symmetrized version of Kullback-Leibler @@ -835,8 +905,11 @@ def hes(ensembles, """ - if not isinstance(weights, (list, tuple, np.ndarray)) and weights == 'mass': - weights = ['mass' for _ in range(len(ensembles))] + if ( + not isinstance(weights, (list, tuple, np.ndarray)) + and weights == "mass" + ): + weights = ["mass" for _ in range(len(ensembles))] elif weights is not None: if len(weights) != len(ensembles): raise ValueError("need weights for every ensemble") @@ -848,10 +921,9 @@ def hes(ensembles, # on the universe. if align: for e, w in zip(ensembles, weights): - mda.analysis.align.AlignTraj(e, ensembles[0], - select=select, - weights=w, - in_memory=True).run() + mda.analysis.align.AlignTraj( + e, ensembles[0], select=select, weights=w, in_memory=True + ).run() else: for ensemble in ensembles: ensemble.transfer_to_memory() @@ -871,7 +943,8 @@ def hes(ensembles, else: logging.error( "Covariance estimator {0} is not supported. " - "Choose between 'shrinkage' and 'ml'.".format(cov_estimator)) + "Choose between 'shrinkage' and 'ml'.".format(cov_estimator) + ) return None out_matrix_eln = len(ensembles) @@ -885,8 +958,9 @@ def hes(ensembles, for i, ensemble in enumerate(ensembles): ensembles_list.append( get_ensemble_bootstrap_samples( - ensemble, - samples=bootstrapping_samples)) + ensemble, samples=bootstrapping_samples + ) + ) for t in range(bootstrapping_samples): logging.info("The coordinates will be bootstrapped.") @@ -894,21 +968,30 @@ def hes(ensembles, sigmas = [] values = np.zeros((out_matrix_eln, out_matrix_eln)) for i, e_orig in enumerate(ensembles): - xs.append(np.average( - ensembles_list[i][t].trajectory.timeseries( - e_orig.select_atoms(select), - order=('fac')), - axis=0).flatten()) - sigmas.append(covariance_matrix(ensembles_list[i][t], - weights=weights[i], - estimator=covariance_estimator, - select=select)) + xs.append( + np.average( + ensembles_list[i][t].trajectory.timeseries( + e_orig.select_atoms(select), order=("fac") + ), + axis=0, + ).flatten() + ) + sigmas.append( + covariance_matrix( + ensembles_list[i][t], + weights=weights[i], + estimator=covariance_estimator, + select=select, + ) + ) for pair in pairs_indices: - value = harmonic_ensemble_similarity(x1=xs[pair[0]], - x2=xs[pair[1]], - sigma1=sigmas[pair[0]], - sigma2=sigmas[pair[1]]) + value = harmonic_ensemble_similarity( + x1=xs[pair[0]], + x2=xs[pair[1]], + sigma1=sigmas[pair[0]], + sigma2=sigmas[pair[1]], + ) values[pair[0], pair[1]] = value values[pair[1], pair[0]] = value data.append(values) @@ -923,31 +1006,32 @@ def hes(ensembles, for e, w in zip(ensembles, weights): # Extract coordinates from each ensemble - coordinates_system = e.trajectory.timeseries(e.select_atoms(select), - order='fac') + coordinates_system = e.trajectory.timeseries( + e.select_atoms(select), order="fac" + ) # Average coordinates in each system xs.append(np.average(coordinates_system, axis=0).flatten()) # Covariance matrices in each system - sigmas.append(covariance_matrix(e, - weights=w, - estimator=covariance_estimator, - select=select)) + sigmas.append( + covariance_matrix( + e, weights=w, estimator=covariance_estimator, select=select + ) + ) for i, j in pairs_indices: - value = harmonic_ensemble_similarity(x1=xs[i], - x2=xs[j], - sigma1=sigmas[i], - sigma2=sigmas[j]) + value = harmonic_ensemble_similarity( + x1=xs[i], x2=xs[j], sigma1=sigmas[i], sigma2=sigmas[j] + ) values[i, j] = value values[j, i] = value # Save details as required details = {} for i in range(out_matrix_eln): - details['ensemble{0:d}_mean'.format(i + 1)] = xs[i] - details['ensemble{0:d}_covariance_matrix'.format(i + 1)] = sigmas[i] + details["ensemble{0:d}_mean".format(i + 1)] = xs[i] + details["ensemble{0:d}_covariance_matrix".format(i + 1)] = sigmas[i] return values, details @@ -1099,39 +1183,42 @@ def ces( pairs_indices = list(trm_indices_nodiag(len(ensembles))) clustering_methods = clustering_method - if not hasattr(clustering_method, '__iter__'): + if not hasattr(clustering_method, "__iter__"): clustering_methods = [clustering_method] - any_method_accept_distance_matrix = \ - np.any([method.accepts_distance_matrix for method in clustering_methods]) - all_methods_accept_distance_matrix = \ - np.all([method.accepts_distance_matrix for method in clustering_methods]) + any_method_accept_distance_matrix = np.any( + [method.accepts_distance_matrix for method in clustering_methods] + ) + all_methods_accept_distance_matrix = np.all( + [method.accepts_distance_matrix for method in clustering_methods] + ) # Register which ensembles the samples belong to ensemble_assignment = [] for i, ensemble in enumerate(ensembles): - ensemble_assignment += [i+1]*len(ensemble.trajectory) + ensemble_assignment += [i + 1] * len(ensemble.trajectory) # Calculate distance matrix if not provided if any_method_accept_distance_matrix and not distance_matrix: - distance_matrix = get_distance_matrix(merge_universes(ensembles), - select=select, - ncores=ncores) + distance_matrix = get_distance_matrix( + merge_universes(ensembles), select=select, ncores=ncores + ) if estimate_error: if any_method_accept_distance_matrix: - distance_matrix = \ - get_distance_matrix_bootstrap_samples( - distance_matrix, - ensemble_assignment, - samples=bootstrapping_samples, - ncores=ncores) + distance_matrix = get_distance_matrix_bootstrap_samples( + distance_matrix, + ensemble_assignment, + samples=bootstrapping_samples, + ncores=ncores, + ) if not all_methods_accept_distance_matrix: ensembles_list = [] for i, ensemble in enumerate(ensembles): ensembles_list.append( get_ensemble_bootstrap_samples( - ensemble, - samples=bootstrapping_samples)) + ensemble, samples=bootstrapping_samples + ) + ) ensembles = [] for j in range(bootstrapping_samples): ensembles.append([]) @@ -1141,16 +1228,17 @@ def ces( # if all methods accept distances matrices, duplicate # ensemble so that it matches size of distance matrices # (no need to resample them since they will not be used) - ensembles = [ensembles]*bootstrapping_samples - + ensembles = [ensembles] * bootstrapping_samples # Call clustering procedure - ccs = cluster(ensembles, - method= clustering_methods, - select=select, - distance_matrix = distance_matrix, - ncores = ncores, - allow_collapsed_result=False) + ccs = cluster( + ensembles, + method=clustering_methods, + select=select, + distance_matrix=distance_matrix, + ncores=ncores, + allow_collapsed_result=False, + ) # Do error analysis if estimate_error: @@ -1166,20 +1254,20 @@ def ces( failed_runs += 1 k += 1 continue - values[i].append(np.zeros((len(ensembles[j]), - len(ensembles[j])))) + values[i].append( + np.zeros((len(ensembles[j]), len(ensembles[j]))) + ) for pair in pairs_indices: # Calculate dJS - this_djs = \ - clustering_ensemble_similarity(ccs[k], - ensembles[j][ - pair[0]], - pair[0] + 1, - ensembles[j][ - pair[1]], - pair[1] + 1, - select=select) + this_djs = clustering_ensemble_similarity( + ccs[k], + ensembles[j][pair[0]], + pair[0] + 1, + ensembles[j][pair[1]], + pair[1] + 1, + select=select, + ) values[i][-1][pair[0], pair[1]] = this_djs values[i][-1][pair[1], pair[0]] = this_djs k += 1 @@ -1187,7 +1275,7 @@ def ces( avgs.append(np.average(outs, axis=0)) stds.append(np.std(outs, axis=0)) - if hasattr(clustering_method, '__iter__'): + if hasattr(clustering_method, "__iter__"): pass else: avgs = avgs[0] @@ -1205,19 +1293,20 @@ def ces( for pair in pairs_indices: # Calculate dJS - this_val = \ - clustering_ensemble_similarity(ccs[i], - ensembles[pair[0]], - pair[0] + 1, - ensembles[pair[1]], - pair[1] + 1, - select=select) + this_val = clustering_ensemble_similarity( + ccs[i], + ensembles[pair[0]], + pair[0] + 1, + ensembles[pair[1]], + pair[1] + 1, + select=select, + ) values[-1][pair[0], pair[1]] = this_val values[-1][pair[1], pair[0]] = this_val - details['clustering'] = ccs + details["clustering"] = ccs - if allow_collapsed_result and not hasattr(clustering_method, '__iter__'): + if allow_collapsed_result and not hasattr(clustering_method, "__iter__"): values = values[0] return values, details @@ -1374,43 +1463,54 @@ def dres( pairs_indices = list(trm_indices_nodiag(len(ensembles))) dimensionality_reduction_methods = dimensionality_reduction_method - if not hasattr(dimensionality_reduction_method, '__iter__'): + if not hasattr(dimensionality_reduction_method, "__iter__"): dimensionality_reduction_methods = [dimensionality_reduction_method] - any_method_accept_distance_matrix = \ - np.any([method.accepts_distance_matrix for method in dimensionality_reduction_methods]) - all_methods_accept_distance_matrix = \ - np.all([method.accepts_distance_matrix for method in dimensionality_reduction_methods]) + any_method_accept_distance_matrix = np.any( + [ + method.accepts_distance_matrix + for method in dimensionality_reduction_methods + ] + ) + all_methods_accept_distance_matrix = np.all( + [ + method.accepts_distance_matrix + for method in dimensionality_reduction_methods + ] + ) # Register which ensembles the samples belong to ensemble_assignment = [] for i, ensemble in enumerate(ensembles): - ensemble_assignment += [i+1]*len(ensemble.trajectory) + ensemble_assignment += [i + 1] * len(ensemble.trajectory) # Calculate distance matrix if not provided if any_method_accept_distance_matrix and not distance_matrix: - distance_matrix = get_distance_matrix(merge_universes(ensembles), - select=select, - ncores=ncores) + distance_matrix = get_distance_matrix( + merge_universes(ensembles), select=select, ncores=ncores + ) if estimate_error: if any_method_accept_distance_matrix: - distance_matrix = \ - get_distance_matrix_bootstrap_samples( - distance_matrix, - ensemble_assignment, - samples=bootstrapping_samples, - ncores=ncores) + distance_matrix = get_distance_matrix_bootstrap_samples( + distance_matrix, + ensemble_assignment, + samples=bootstrapping_samples, + ncores=ncores, + ) if not all_methods_accept_distance_matrix: ensembles_list = [] for i, ensemble in enumerate(ensembles): ensembles_list.append( get_ensemble_bootstrap_samples( - ensemble, - samples=bootstrapping_samples)) + ensemble, samples=bootstrapping_samples + ) + ) ensembles = [] for j in range(bootstrapping_samples): - ensembles.append(ensembles_list[i, j] for i - in range(ensembles_list.shape[0])) + ensembles.append( + ensembles_list[i, j] + for i in range(ensembles_list.shape[0]) + ) else: # if all methods accept distances matrices, duplicate # ensemble so that it matches size of distance matrices @@ -1422,9 +1522,10 @@ def dres( ensembles, method=dimensionality_reduction_methods, select=select, - distance_matrix = distance_matrix, - ncores = ncores, - allow_collapsed_result = False) + distance_matrix=distance_matrix, + ncores=ncores, + allow_collapsed_result=False, + ) details = {} details["reduced_coordinates"] = coordinates @@ -1435,24 +1536,28 @@ def dres( values = {} avgs = [] stds = [] - for i,method in enumerate(dimensionality_reduction_methods): + for i, method in enumerate(dimensionality_reduction_methods): values[i] = [] for j in range(bootstrapping_samples): - values[i].append(np.zeros((len(ensembles[j]), - len(ensembles[j])))) + values[i].append( + np.zeros((len(ensembles[j]), len(ensembles[j]))) + ) kdes, resamples, embedded_ensembles = gen_kde_pdfs( coordinates[k], ensemble_assignment, len(ensembles[j]), - nsamples=nsamples) + nsamples=nsamples, + ) for pair in pairs_indices: - this_value = dimred_ensemble_similarity(kdes[pair[0]], - resamples[pair[0]], - kdes[pair[1]], - resamples[pair[1]]) + this_value = dimred_ensemble_similarity( + kdes[pair[0]], + resamples[pair[0]], + kdes[pair[1]], + resamples[pair[1]], + ) values[i][-1][pair[0], pair[1]] = this_value values[i][-1][pair[1], pair[0]] = this_value @@ -1461,7 +1566,7 @@ def dres( avgs.append(np.average(outs, axis=0)) stds.append(np.std(outs, axis=0)) - if hasattr(dimensionality_reduction_method, '__iter__'): + if hasattr(dimensionality_reduction_method, "__iter__"): pass else: avgs = avgs[0] @@ -1471,24 +1576,29 @@ def dres( values = [] - for i,method in enumerate(dimensionality_reduction_methods): + for i, method in enumerate(dimensionality_reduction_methods): values.append(np.zeros((len(ensembles), len(ensembles)))) - kdes, resamples, embedded_ensembles = gen_kde_pdfs(coordinates[i], - ensemble_assignment, - len(ensembles), - nsamples=nsamples) + kdes, resamples, embedded_ensembles = gen_kde_pdfs( + coordinates[i], + ensemble_assignment, + len(ensembles), + nsamples=nsamples, + ) for pair in pairs_indices: - this_value = dimred_ensemble_similarity(kdes[pair[0]], - resamples[pair[0]], - kdes[pair[1]], - resamples[pair[1]]) + this_value = dimred_ensemble_similarity( + kdes[pair[0]], + resamples[pair[0]], + kdes[pair[1]], + resamples[pair[1]], + ) values[-1][pair[0], pair[1]] = this_value values[-1][pair[1], pair[0]] = this_value - if allow_collapsed_result and not hasattr(dimensionality_reduction_method, - '__iter__'): + if allow_collapsed_result and not hasattr( + dimensionality_reduction_method, "__iter__" + ): values = values[0] return values, details @@ -1576,13 +1686,16 @@ def ces_convergence( ) ensembles = prepare_ensembles_for_convergence_increasing_window( - original_ensemble, window_size, select=select) + original_ensemble, window_size, select=select + ) - ccs = cluster(ensembles, - select=select, - method=clustering_method, - allow_collapsed_result=False, - ncores=ncores) + ccs = cluster( + ensembles, + select=select, + method=clustering_method, + allow_collapsed_result=False, + ncores=ncores, + ) out = [] for cc in ccs: @@ -1591,9 +1704,8 @@ def ces_convergence( out.append(np.zeros(len(ensembles))) for j, ensemble in enumerate(ensembles): out[-1][j] = cumulative_clustering_ensemble_similarity( - cc, - len(ensembles), - j + 1) + cc, len(ensembles), j + 1 + ) out = np.array(out).T return out @@ -1680,19 +1792,20 @@ def dres_convergence( ) ensembles = prepare_ensembles_for_convergence_increasing_window( - original_ensemble, window_size, select=select) + original_ensemble, window_size, select=select + ) - coordinates, dimred_details = \ - reduce_dimensionality( - ensembles, - select=select, - method=dimensionality_reduction_method, - allow_collapsed_result=False, - ncores=ncores) + coordinates, dimred_details = reduce_dimensionality( + ensembles, + select=select, + method=dimensionality_reduction_method, + allow_collapsed_result=False, + ncores=ncores, + ) ensemble_assignment = [] for i, ensemble in enumerate(ensembles): - ensemble_assignment += [i+1]*len(ensemble.trajectory) + ensemble_assignment += [i + 1] * len(ensemble.trajectory) ensemble_assignment = np.array(ensemble_assignment) out = [] @@ -1700,18 +1813,17 @@ def dres_convergence( out.append(np.zeros(len(ensembles))) - kdes, resamples, embedded_ensembles = \ - cumulative_gen_kde_pdfs( - coordinates[i], - ensemble_assignment=ensemble_assignment, - nensembles=len(ensembles), - nsamples=nsamples) + kdes, resamples, embedded_ensembles = cumulative_gen_kde_pdfs( + coordinates[i], + ensemble_assignment=ensemble_assignment, + nensembles=len(ensembles), + nsamples=nsamples, + ) for j, ensemble in enumerate(ensembles): - out[-1][j] = dimred_ensemble_similarity(kdes[-1], - resamples[-1], - kdes[j], - resamples[j]) + out[-1][j] = dimred_ensemble_similarity( + kdes[-1], resamples[-1], kdes[j], resamples[j] + ) out = np.array(out).T return out diff --git a/package/MDAnalysis/analysis/encore/utils.py b/package/MDAnalysis/analysis/encore/utils.py index 13a028f45c4..ae6407ddd19 100644 --- a/package/MDAnalysis/analysis/encore/utils.py +++ b/package/MDAnalysis/analysis/encore/utils.py @@ -123,14 +123,16 @@ def loadz(self, fname): """ loaded = np.load(fname, allow_pickle=True) - if loaded['metadata'].shape != (): - if loaded['metadata']['number of frames'] != self.size: + if loaded["metadata"].shape != (): + if loaded["metadata"]["number of frames"] != self.size: raise TypeError - self.metadata = loaded['metadata'] + self.metadata = loaded["metadata"] else: - if self.size*(self.size-1)/2+self.size != len(loaded['elements']): + if self.size * (self.size - 1) / 2 + self.size != len( + loaded["elements"] + ): raise TypeError - self._elements = loaded['elements'] + self._elements = loaded["elements"] def __add__(self, scalar): """Add scalar to matrix elements. @@ -142,7 +144,7 @@ def __add__(self, scalar): Scalar to be added. """ newMatrix = self.__class__(self.size) - newMatrix._elements = self._elements + scalar; + newMatrix._elements = self._elements + scalar return newMatrix def __iadd__(self, scalar): @@ -157,7 +159,6 @@ def __iadd__(self, scalar): self._elements += scalar return self - def __mul__(self, scalar): """Multiply with scalar. @@ -168,7 +169,7 @@ def __mul__(self, scalar): Scalar to multiply with. """ newMatrix = self.__class__(self.size) - newMatrix._elements = self._elements * scalar; + newMatrix._elements = self._elements * scalar return newMatrix def __imul__(self, scalar): @@ -237,10 +238,12 @@ class description. self.n_jobs = cpu_count() self.functions = function - if not hasattr(self.functions, '__iter__'): + if not hasattr(self.functions, "__iter__"): self.functions = [self.functions] * len(args) if len(self.functions) != len(args): - self.functions = self.functions[:] * (len(args) // len(self.functions)) + self.functions = self.functions[:] * ( + len(args) // len(self.functions) + ) # Arguments should be present if args is None: @@ -273,10 +276,12 @@ def worker(self, q, results): """ while True: i = q.get() - if i == 'STOP': + if i == "STOP": return - results.put((i, self.functions[i](*self.args[i], **self.kwargs[i]))) + results.put( + (i, self.functions[i](*self.args[i], **self.kwargs[i])) + ) def run(self): r""" @@ -294,20 +299,23 @@ def run(self): results_list = [] if self.n_jobs == 1: for i in range(self.nruns): - results_list.append((i, self.functions[i](*self.args[i], - **self.kwargs[i]))) + results_list.append( + (i, self.functions[i](*self.args[i], **self.kwargs[i])) + ) else: manager = Manager() q = manager.Queue() results = manager.Queue() - workers = [Process(target=self.worker, args=(q, results)) for i in - range(self.n_jobs)] + workers = [ + Process(target=self.worker, args=(q, results)) + for i in range(self.n_jobs) + ] for i in range(self.nruns): q.put(i) for w in workers: - q.put('STOP') + q.put("STOP") for w in workers: w.start() @@ -315,8 +323,8 @@ def run(self): for w in workers: w.join() - results.put('STOP') - for i in iter(results.get, 'STOP'): + results.put("STOP") + for i in iter(results.get, "STOP"): results_list.append(i) return tuple(sorted(results_list, key=lambda x: x[0])) @@ -361,7 +369,7 @@ def trm_indices_nodiag(n): n : int Matrix size -""" + """ for i in range(1, n): for j in range(i): @@ -377,7 +385,7 @@ def trm_indices_diag(n): n : int Matrix size -""" + """ for i in range(0, n): for j in range(i + 1): @@ -403,6 +411,9 @@ def merge_universes(universes): return mda.Universe( universes[0].filename, - np.concatenate(tuple([e.trajectory.timeseries(order='fac') for e in universes]), - axis=0), - format=MemoryReader) + np.concatenate( + tuple([e.trajectory.timeseries(order="fac") for e in universes]), + axis=0, + ), + format=MemoryReader, + ) diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index ee42bc165ef..f2f373543ea 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -21,9 +21,9 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -#Analyse a trajectory using elastic network models, following the approach of Hall et al (JACS 2007) -#Ben Hall (benjamin.a.hall@ucl.ac.uk) is to blame -#Copyright 2011; Consider under GPL v2 or later +# Analyse a trajectory using elastic network models, following the approach of Hall et al (JACS 2007) +# Ben Hall (benjamin.a.hall@ucl.ac.uk) is to blame +# Copyright 2011; Consider under GPL v2 or later r""" Elastic network analysis of MD trajectories --- :mod:`MDAnalysis.analysis.gnm` ============================================================================== @@ -97,11 +97,11 @@ from MDAnalysis.analysis.base import Results -logger = logging.getLogger('MDAnalysis.analysis.GNM') +logger = logging.getLogger("MDAnalysis.analysis.GNM") def _dsq(a, b): - diff = (a - b) + diff = a - b return np.dot(diff, diff) @@ -133,10 +133,14 @@ def generate_grid(positions, cutoff): low_x = x.min() low_y = y.min() low_z = z.min() - #Ok now generate a list with 3 dimensions representing boxes in x, y and z - grid = [[[[] for i in range(int((high_z - low_z) / cutoff) + 1)] - for j in range(int((high_y - low_y) / cutoff) + 1)] - for k in range(int((high_x - low_x) / cutoff) + 1)] + # Ok now generate a list with 3 dimensions representing boxes in x, y and z + grid = [ + [ + [[] for i in range(int((high_z - low_z) / cutoff) + 1)] + for j in range(int((high_y - low_y) / cutoff) + 1) + ] + for k in range(int((high_x - low_x) / cutoff) + 1) + ] for i, pos in enumerate(positions): x_pos = int((pos[0] - low_x) / cutoff) y_pos = int((pos[1] - low_y) / cutoff) @@ -166,7 +170,8 @@ def neighbour_generator(positions, cutoff): n_y = len(grid[0]) n_z = len(grid[0][0]) for cell_x, cell_y, cell_z in itertools.product( - range(n_x), range(n_y), range(n_z)): + range(n_x), range(n_y), range(n_z) + ): atoms = grid[cell_x][cell_y][cell_z] # collect all atoms in own cell and neighboring cell all_atoms = [] @@ -247,8 +252,8 @@ class GNMAnalysis(AnalysisBase): ``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 + Enabled **parallel execution** with the ``multiprocessing`` and ``dask`` + backends; use the new method :meth:`get_supported_backends` to see all supported backends. """ @@ -257,13 +262,15 @@ class GNMAnalysis(AnalysisBase): @classmethod def get_supported_backends(cls): return ("serial", "multiprocessing", "dask") - - def __init__(self, - universe, - select='protein and name CA', - cutoff=7.0, - ReportVector=None, - Bonus_groups=None): + + def __init__( + self, + universe, + select="protein and name CA", + cutoff=7.0, + ReportVector=None, + Bonus_groups=None, + ): super(GNMAnalysis, self).__init__(universe.trajectory) self.u = universe self.select = select @@ -273,12 +280,16 @@ def __init__(self, self.results.eigenvectors = [] self._timesteps = None # time for each frame self.ReportVector = ReportVector - self.Bonus_groups = [self.u.select_atoms(item) for item in Bonus_groups] \ - if Bonus_groups else [] + self.Bonus_groups = ( + [self.u.select_atoms(item) for item in Bonus_groups] + if Bonus_groups + else [] + ) self.ca = self.u.select_atoms(self.select) - def _generate_output(self, w, v, outputobject, - ReportVector=None, counter=0): + def _generate_output( + self, w, v, outputobject, ReportVector=None, counter=0 + ): """Appends time, eigenvalues and eigenvectors to results. This generates the output by adding eigenvalue and @@ -297,7 +308,8 @@ def _generate_output(self, w, v, outputobject, item[0] + 1, w[list_map[1]], item[1], - file=oup) + file=oup, + ) outputobject.eigenvalues.append(w[list_map[1]]) outputobject.eigenvectors.append(v[list_map[1]]) @@ -316,9 +328,9 @@ def generate_kirchoff(self): """ positions = self.ca.positions - #add the com from each bonus group to the ca_positions list + # add the com from each bonus group to the ca_positions list for item in self.Bonus_groups: - #bonus = self.u.select_atoms(item) + # bonus = self.u.select_atoms(item) positions = np.vstack((positions, item.center_of_mass())) natoms = len(positions) @@ -327,8 +339,10 @@ def generate_kirchoff(self): cutoffsq = self.cutoff**2 for i_atom, j_atom in neighbour_generator(positions, self.cutoff): - if j_atom > i_atom and _dsq(positions[i_atom], - positions[j_atom]) < cutoffsq: + if ( + j_atom > i_atom + and _dsq(positions[i_atom], positions[j_atom]) < cutoffsq + ): matrix[i_atom][j_atom] = -1.0 matrix[j_atom][i_atom] = -1.0 matrix[i_atom][i_atom] = matrix[i_atom][i_atom] + 1 @@ -352,7 +366,8 @@ def _single_frame(self): v, self.results, ReportVector=self.ReportVector, - counter=self._ts.frame) + counter=self._ts.frame, + ) def _conclude(self): self.results.times = self.times @@ -427,16 +442,17 @@ class closeContactGNMAnalysis(GNMAnalysis): ``eigenvectors`` of the `results` attribute. """ - def __init__(self, - universe, - select='protein', - cutoff=4.5, - ReportVector=None, - weights="size"): - super(closeContactGNMAnalysis, self).__init__(universe, - select, - cutoff, - ReportVector) + def __init__( + self, + universe, + select="protein", + cutoff=4.5, + ReportVector=None, + weights="size", + ): + super(closeContactGNMAnalysis, self).__init__( + universe, select, cutoff, ReportVector + ) self.weights = weights def generate_kirchoff(self): @@ -452,17 +468,21 @@ def generate_kirchoff(self): # cache sqrt of residue sizes (slow) so that sr[i]*sr[j] == sqrt(r[i]*r[j]) inv_sqrt_res_sizes = np.ones(len(self.ca.residues)) - if self.weights == 'size': + if self.weights == "size": inv_sqrt_res_sizes = 1 / np.sqrt( - [r.atoms.n_atoms for r in self.ca.residues]) + [r.atoms.n_atoms for r in self.ca.residues] + ) for i_atom, j_atom in neighbour_generator(positions, self.cutoff): - if j_atom > i_atom and _dsq(positions[i_atom], - positions[j_atom]) < cutoffsq: + if ( + j_atom > i_atom + and _dsq(positions[i_atom], positions[j_atom]) < cutoffsq + ): iresidue = residue_index_map[i_atom] jresidue = residue_index_map[j_atom] - contact = (inv_sqrt_res_sizes[iresidue] * - inv_sqrt_res_sizes[jresidue]) + contact = ( + inv_sqrt_res_sizes[iresidue] * inv_sqrt_res_sizes[jresidue] + ) matrix[iresidue][jresidue] -= contact matrix[jresidue][iresidue] -= contact matrix[iresidue][iresidue] += contact diff --git a/package/MDAnalysis/analysis/hbonds/__init__.py b/package/MDAnalysis/analysis/hbonds/__init__.py index b74b96638b4..4f01974ff25 100644 --- a/package/MDAnalysis/analysis/hbonds/__init__.py +++ b/package/MDAnalysis/analysis/hbonds/__init__.py @@ -22,7 +22,8 @@ # __all__ = [ - 'HydrogenBondAutoCorrel', 'find_hydrogen_donors', + "HydrogenBondAutoCorrel", + "find_hydrogen_donors", ] from .hbond_autocorrel import HydrogenBondAutoCorrel, find_hydrogen_donors diff --git a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py index a5204236a07..3f80affc814 100644 --- a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py +++ b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py @@ -45,10 +45,12 @@ import warnings with warnings.catch_warnings(): - warnings.simplefilter('always', DeprecationWarning) - wmsg = ("This module was moved to " - "MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel; " - "hbonds.hbond_autocorrel will be removed in 3.0.0.") + warnings.simplefilter("always", DeprecationWarning) + wmsg = ( + "This module was moved to " + "MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel; " + "hbonds.hbond_autocorrel will be removed in 3.0.0." + ) warnings.warn(wmsg, category=DeprecationWarning) from MDAnalysis.lib.util import deprecate @@ -56,16 +58,18 @@ from ..hydrogenbonds import hbond_autocorrel -find_hydrogen_donors = deprecate(hbond_autocorrel.find_hydrogen_donors, - release="2.0.0", remove="3.0.0", - message="The function was moved to " - "MDAnalysis.analysis.hbonds.hbond_autocorrel.") - -HydrogenBondAutoCorrel = deprecate(hbond_autocorrel.HydrogenBondAutoCorrel, - release="2.0.0", remove="3.0.0", - message="The class was moved to " - "MDAnalysis.analysis.hbonds.hbond_autocorrel.") - - - - +find_hydrogen_donors = deprecate( + hbond_autocorrel.find_hydrogen_donors, + release="2.0.0", + remove="3.0.0", + message="The function was moved to " + "MDAnalysis.analysis.hbonds.hbond_autocorrel.", +) + +HydrogenBondAutoCorrel = deprecate( + hbond_autocorrel.HydrogenBondAutoCorrel, + release="2.0.0", + remove="3.0.0", + message="The class was moved to " + "MDAnalysis.analysis.hbonds.hbond_autocorrel.", +) diff --git a/package/MDAnalysis/analysis/helix_analysis.py b/package/MDAnalysis/analysis/helix_analysis.py index 1b1bdf9ce3f..e0edf763708 100644 --- a/package/MDAnalysis/analysis/helix_analysis.py +++ b/package/MDAnalysis/analysis/helix_analysis.py @@ -126,7 +126,7 @@ def vector_of_best_fit(coordinates): # does vector face first local helix origin? angle = mdamath.angle(centered[0], vector) - if angle > np.pi/2: + if angle > np.pi / 2: vector *= -1 return vector @@ -168,7 +168,7 @@ def local_screw_angles(global_axis, ref_axis, helix_directions): # project helix_directions onto global to remove contribution norm_global_sq = np.dot(global_axis, global_axis) - mag_g = np.matmul(global_axis, helix_directions.T)/norm_global_sq + mag_g = np.matmul(global_axis, helix_directions.T) / norm_global_sq # projection onto global_axis proj_g = mag_g.reshape(-1, 1) @ global_axis.reshape(1, -1) # projection onto plane w/o global_axis contribution @@ -176,9 +176,10 @@ def local_screw_angles(global_axis, ref_axis, helix_directions): # angles from projection to perp refs = np.array([perp, ortho]) # (2, 3) - norms = _, ortho_norm = np.outer(mdamath.pnorm(refs), - mdamath.pnorm(proj_plane)) - cos = cos_perp, cos_ortho = np.matmul(refs, proj_plane.T)/norms + norms = _, ortho_norm = np.outer( + mdamath.pnorm(refs), mdamath.pnorm(proj_plane) + ) + cos = cos_perp, cos_ortho = np.matmul(refs, proj_plane.T) / norms to_perp, to_ortho = np.arccos(np.clip(cos, -1, 1)) # (2, n_vec) to_ortho[ortho_norm == 0] = 0 # ? to_ortho[cos_perp < 0] *= -1 @@ -251,11 +252,11 @@ def helix_analysis(positions, ref_axis=(0, 0, 1)): adjacent_mag = bimags[:-1] * bimags[1:] # (n_res-3,) # find angle between bisectors for twist and n_residue/turn - cos_theta = mdamath.pdot(bisectors[:-1], bisectors[1:])/adjacent_mag + cos_theta = mdamath.pdot(bisectors[:-1], bisectors[1:]) / adjacent_mag cos_theta = np.clip(cos_theta, -1, 1) twists = np.arccos(cos_theta) # (n_res-3,) local_twists = np.rad2deg(twists) - local_nres_per_turn = 2*np.pi / twists + local_nres_per_turn = 2 * np.pi / twists # find normal to bisectors for local axes cross_bi = np.cross(bisectors[:-1], bisectors[1:]) # (n_res-3, 3) @@ -266,42 +267,46 @@ def helix_analysis(positions, ref_axis=(0, 0, 1)): # find angles between axes for bends bend_theta = np.matmul(local_axes, local_axes.T) # (n_res-3, n_res-3) # set angles to 0 between zero-vectors - bend_theta = np.where(zero_vectors+zero_vectors.T, # (n_res-3, n_res-3) - bend_theta, 1) + bend_theta = np.where( + zero_vectors + zero_vectors.T, bend_theta, 1 # (n_res-3, n_res-3) + ) bend_matrix = np.rad2deg(np.arccos(np.clip(bend_theta, -1, 1))) # local bends are between axes 3 windows apart local_bends = np.diagonal(bend_matrix, offset=3) # (n_res-6,) # radius of local cylinder - radii = (adjacent_mag**0.5) / (2*(1.0-cos_theta)) # (n_res-3,) + radii = (adjacent_mag**0.5) / (2 * (1.0 - cos_theta)) # (n_res-3,) # special case: angle b/w bisectors is 0 (should virtually never happen) # guesstimate radius = half bisector magnitude - radii = np.where(cos_theta != 1, radii, (adjacent_mag**0.5)/2) + radii = np.where(cos_theta != 1, radii, (adjacent_mag**0.5) / 2) # height of local cylinder heights = np.abs(mdamath.pdot(vectors[1:-1], local_axes)) # (n_res-3,) - local_helix_directions = (bisectors.T/bimags).T # (n_res-2, 3) + local_helix_directions = (bisectors.T / bimags).T # (n_res-2, 3) # get origins by subtracting radius from atom i+1 origins = positions[1:-1].copy() # (n_res-2, 3) - origins[:-1] -= (radii*local_helix_directions[:-1].T).T + origins[:-1] -= (radii * local_helix_directions[:-1].T).T # subtract radius from atom i+2 in last one - origins[-1] -= radii[-1]*local_helix_directions[-1] + origins[-1] -= radii[-1] * local_helix_directions[-1] helix_axes = vector_of_best_fit(origins) - screw = local_screw_angles(helix_axes, np.asarray(ref_axis), - local_helix_directions) - - results = {'local_twists': local_twists, - 'local_nres_per_turn': local_nres_per_turn, - 'local_axes': local_axes, - 'local_bends': local_bends, - 'local_heights': heights, - 'local_helix_directions': local_helix_directions, - 'local_origins': origins, - 'all_bends': bend_matrix, - 'global_axis': helix_axes, - 'local_screw_angles': screw} + screw = local_screw_angles( + helix_axes, np.asarray(ref_axis), local_helix_directions + ) + + results = { + "local_twists": local_twists, + "local_nres_per_turn": local_nres_per_turn, + "local_axes": local_axes, + "local_bends": local_bends, + "local_heights": heights, + "local_helix_directions": local_helix_directions, + "local_origins": origins, + "all_bends": bend_matrix, + "global_axis": helix_axes, + "local_screw_angles": screw, + } return results @@ -377,14 +382,14 @@ class HELANAL(AnalysisBase): # shapes of properties from each frame, relative to n_residues attr_shapes = { - 'local_twists': (-3,), - 'local_bends': (-6,), - 'local_heights': (-3,), - 'local_nres_per_turn': (-3,), - 'local_origins': (-2, 3), - 'local_axes': (-3, 3), - 'local_helix_directions': (-2, 3), - 'local_screw_angles': (-2,), + "local_twists": (-3,), + "local_bends": (-6,), + "local_heights": (-3,), + "local_nres_per_turn": (-3,), + "local_origins": (-2, 3), + "local_axes": (-3, 3), + "local_helix_directions": (-2, 3), + "local_screw_angles": (-2,), } def __init__( @@ -407,9 +412,9 @@ def __init__( groups = util.group_same_or_consecutive_integers(ag.resindices) counter = 0 if len(groups) > 1: - msg = 'Your selection {} has gaps in the residues.'.format(s) + msg = "Your selection {} has gaps in the residues.".format(s) if split_residue_sequences: - msg += ' Splitting into {} helices.'.format(len(groups)) + msg += " Splitting into {} helices.".format(len(groups)) else: groups = [ag.resindices] warnings.warn(msg) @@ -418,22 +423,26 @@ def __init__( ng = len(g) counter += ng if ng < 9: - warnings.warn('Fewer than 9 atoms found for helix in ' - 'selection {} with these resindices: {}. ' - 'This sequence will be skipped. HELANAL ' - 'is designed to work on at sequences of ' - '≥9 residues.'.format(s, g)) + warnings.warn( + "Fewer than 9 atoms found for helix in " + "selection {} with these resindices: {}. " + "This sequence will be skipped. HELANAL " + "is designed to work on at sequences of " + "≥9 residues.".format(s, g) + ) continue ids, counts = np.unique(g, return_counts=True) if np.any(counts > 1): - dup = ', '.join(map(str, ids[counts > 1])) - warnings.warn('Your selection {} includes multiple atoms ' - 'for residues with these resindices: {}.' - 'HELANAL is designed to work on one alpha-' - 'carbon per residue.'.format(s, dup)) + dup = ", ".join(map(str, ids[counts > 1])) + warnings.warn( + "Your selection {} includes multiple atoms " + "for residues with these resindices: {}." + "HELANAL is designed to work on one alpha-" + "carbon per residue.".format(s, dup) + ) - consecutive.append(ag[counter-ng:counter]) + consecutive.append(ag[counter - ng : counter]) self.atomgroups = consecutive self.ref_axis = np.asarray(ref_axis) @@ -442,19 +451,25 @@ def __init__( def _zeros_per_frame(self, dims, n_positions=0): """Create zero arrays where first 2 dims are n_frames, n_values""" first = dims[0] + n_positions - npdims = (self.n_frames, first,) + dims[1:] # py27 workaround + npdims = ( + self.n_frames, + first, + ) + dims[ + 1: + ] # py27 workaround return np.zeros(npdims, dtype=np.float64) def _prepare(self): n_res = [len(ag) for ag in self.atomgroups] for key, dims in self.attr_shapes.items(): - empty = [self._zeros_per_frame( - dims, n_positions=n) for n in n_res] + empty = [self._zeros_per_frame(dims, n_positions=n) for n in n_res] self.results[key] = empty self.results.global_axis = [self._zeros_per_frame((3,)) for n in n_res] - self.results.all_bends = [self._zeros_per_frame((n-3, n-3)) for n in n_res] + self.results.all_bends = [ + self._zeros_per_frame((n - 3, n - 3)) for n in n_res + ] def _single_frame(self): _f = self._frame_index @@ -469,12 +484,13 @@ def _conclude(self): self.results.global_tilts = tilts = [] norm_ref = (self.ref_axis**2).sum() ** 0.5 for axes in self.results.global_axis: - cos = np.matmul(self.ref_axis, axes.T) / \ - (mdamath.pnorm(axes)*norm_ref) + cos = np.matmul(self.ref_axis, axes.T) / ( + mdamath.pnorm(axes) * norm_ref + ) cos = np.clip(cos, -1.0, 1.0) tilts.append(np.rad2deg(np.arccos(cos))) - global_attrs = ['global_axis', 'global_tilts', 'all_bends'] + global_attrs = ["global_axis", "global_tilts", "all_bends"] attrnames = list(self.attr_shapes.keys()) + global_attrs # summarise self.results.summary = [] @@ -483,15 +499,17 @@ def _conclude(self): for name in attrnames: attr = self.results[name] mean = attr[i].mean(axis=0) - dev = np.abs(attr[i]-mean) - stats[name] = {'mean': mean, - 'sample_sd': attr[i].std(axis=0, ddof=1), - 'abs_dev': dev.mean(axis=0)} + dev = np.abs(attr[i] - mean) + stats[name] = { + "mean": mean, + "sample_sd": attr[i].std(axis=0, ddof=1), + "abs_dev": dev.mean(axis=0), + } self.results.summary.append(stats) # flatten? if len(self.atomgroups) == 1 and self._flatten: - for name in attrnames + ['summary']: + for name in attrnames + ["summary"]: attr = self.results[name] self.results[name] = attr[0] @@ -506,7 +524,7 @@ def universe_from_origins(self): try: origins = self.results.local_origins except AttributeError: - raise ValueError('Call run() before universe_from_origins') + raise ValueError("Call run() before universe_from_origins") if not isinstance(origins, list): origins = [origins] @@ -514,9 +532,12 @@ def universe_from_origins(self): universe = [] for xyz in origins: n_res = xyz.shape[1] - u = mda.Universe.empty(n_res, n_residues=n_res, - atom_resindex=np.arange(n_res), - trajectory=True).load_new(xyz) + u = mda.Universe.empty( + n_res, + n_residues=n_res, + atom_resindex=np.arange(n_res), + trajectory=True, + ).load_new(xyz) universe.append(u) if not isinstance(self.results.local_origins, list): universe = universe[0] diff --git a/package/MDAnalysis/analysis/hole2/__init__.py b/package/MDAnalysis/analysis/hole2/__init__.py index d09359f0917..a958b16adf8 100644 --- a/package/MDAnalysis/analysis/hole2/__init__.py +++ b/package/MDAnalysis/analysis/hole2/__init__.py @@ -40,8 +40,10 @@ from mdahole2.analysis import utils, templates from mdahole2.analysis.utils import create_vmd_surface -wmsg = ("Deprecated in version 2.8.0\n" - "MDAnalysis.analysis.hole2 is deprecated in favour of the " - "MDAKit madahole2 (https://www.mdanalysis.org/mdahole2/) " - "and will be removed in MDAnalysis version 3.0.0") +wmsg = ( + "Deprecated in version 2.8.0\n" + "MDAnalysis.analysis.hole2 is deprecated in favour of the " + "MDAKit madahole2 (https://www.mdanalysis.org/mdahole2/) " + "and will be removed in MDAnalysis version 3.0.0" +) warnings.warn(wmsg, category=DeprecationWarning) diff --git a/package/MDAnalysis/analysis/hydrogenbonds/__init__.py b/package/MDAnalysis/analysis/hydrogenbonds/__init__.py index 7bb75ea625f..7297f5b4141 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/__init__.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/__init__.py @@ -22,9 +22,10 @@ # __all__ = [ - 'HydrogenBondAnalysis', - 'WaterBridgeAnalysis', - 'HydrogenBondAutoCorrel', 'find_hydrogen_donors' + "HydrogenBondAnalysis", + "WaterBridgeAnalysis", + "HydrogenBondAutoCorrel", + "find_hydrogen_donors", ] from .hbond_analysis import HydrogenBondAnalysis diff --git a/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py b/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py index 51fb1bd19aa..749fe3533c1 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/hbond_autocorrel.py @@ -214,14 +214,16 @@ from MDAnalysis.core.groups import requires from MDAnalysis.due import due, Doi -due.cite(Doi("10.1063/1.4922445"), - description="Hydrogen bonding autocorrelation time", - path='MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel', + +due.cite( + Doi("10.1063/1.4922445"), + description="Hydrogen bonding autocorrelation time", + path="MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel", ) del Doi -@requires('bonds') +@requires("bonds") def find_hydrogen_donors(hydrogens): """Returns the donor atom for each hydrogen @@ -287,18 +289,24 @@ class HydrogenBondAutoCorrel(object): :attr:`HydrogenBondAutoCorrel.solution['results']` instead. """ - def __init__(self, universe, - hydrogens=None, acceptors=None, donors=None, - bond_type=None, - exclusions=None, - angle_crit=130.0, dist_crit=3.0, # geometric criteria - sample_time=100, # expected length of the decay in ps - time_cut=None, # cutoff time for intermittent hbonds - nruns=1, # number of times to iterate through the trajectory - nsamples=50, # number of different points to sample in a run - pbc=True): - - #warnings.warn("This class is deprecated, use analysis.hbonds.HydrogenBondAnalysis " + def __init__( + self, + universe, + hydrogens=None, + acceptors=None, + donors=None, + bond_type=None, + exclusions=None, + angle_crit=130.0, + dist_crit=3.0, # geometric criteria + sample_time=100, # expected length of the decay in ps + time_cut=None, # cutoff time for intermittent hbonds + nruns=1, # number of times to iterate through the trajectory + nsamples=50, # number of different points to sample in a run + pbc=True, + ): + + # warnings.warn("This class is deprecated, use analysis.hbonds.HydrogenBondAnalysis " # "which has .autocorrelation function", # category=DeprecationWarning) @@ -313,23 +321,27 @@ def __init__(self, universe, self.a = acceptors self.d = donors if not len(self.h) == len(self.d): - raise ValueError("Donors and Hydrogen groups must be identical " - "length. Try using `find_hydrogen_donors`.") + raise ValueError( + "Donors and Hydrogen groups must be identical " + "length. Try using `find_hydrogen_donors`." + ) if exclusions is not None: if len(exclusions[0]) != len(exclusions[1]): raise ValueError( - "'exclusion' must be two arrays of identical length") - self.exclusions = np.column_stack(( - exclusions[0], exclusions[1] - )).astype(np.intp) + "'exclusion' must be two arrays of identical length" + ) + self.exclusions = np.column_stack( + (exclusions[0], exclusions[1]) + ).astype(np.intp) else: self.exclusions = None self.bond_type = bond_type - if self.bond_type not in ['continuous', 'intermittent']: + if self.bond_type not in ["continuous", "intermittent"]: raise ValueError( - "bond_type must be either 'continuous' or 'intermittent'") + "bond_type must be either 'continuous' or 'intermittent'" + ) self.a_crit = np.deg2rad(angle_crit) self.d_crit = dist_crit @@ -341,11 +353,11 @@ def __init__(self, universe, self.time_cut = time_cut self.solution = { - 'results': None, # Raw results - 'time': None, # Time axis of raw results - 'fit': None, # coefficients for fit - 'tau': None, # integral of exponential fit - 'estimate': None # y values of fit against time + "results": None, # Raw results + "time": None, # Time axis of raw results + "fit": None, # coefficients for fit + "tau": None, # integral of exponential fit + "estimate": None, # y values of fit against time } def _slice_traj(self, sample_time): @@ -357,16 +369,22 @@ def _slice_traj(self, sample_time): n_frames = len(self.u.trajectory) if req_frames > n_frames: - warnings.warn("Number of required frames ({}) greater than the" - " number of frames in trajectory ({})" - .format(req_frames, n_frames), RuntimeWarning) + warnings.warn( + "Number of required frames ({}) greater than the" + " number of frames in trajectory ({})".format( + req_frames, n_frames + ), + RuntimeWarning, + ) numruns = self.nruns if numruns > n_frames: numruns = n_frames - warnings.warn("Number of runs ({}) greater than the number of" - " frames in trajectory ({})" - .format(self.nruns, n_frames), RuntimeWarning) + warnings.warn( + "Number of runs ({}) greater than the number of" + " frames in trajectory ({})".format(self.nruns, n_frames), + RuntimeWarning, + ) self._starts = np.arange(0, n_frames, n_frames / numruns, dtype=int) # limit stop points using clip @@ -374,8 +392,12 @@ def _slice_traj(self, sample_time): self._skip = req_frames // self.nsamples if self._skip == 0: # If nsamples > req_frames - warnings.warn("Desired number of sample points too high, using {0}" - .format(req_frames), RuntimeWarning) + warnings.warn( + "Desired number of sample points too high, using {0}".format( + req_frames + ), + RuntimeWarning, + ) self._skip = 1 def run(self, force=False): @@ -387,19 +409,21 @@ def run(self, force=False): Will overwrite previous results if they exist """ # if results exist, don't waste any time - if self.solution['results'] is not None and not force: + if self.solution["results"] is not None and not force: return - main_results = np.zeros_like(np.arange(self._starts[0], - self._stops[0], - self._skip), - dtype=np.float32) + main_results = np.zeros_like( + np.arange(self._starts[0], self._stops[0], self._skip), + dtype=np.float32, + ) # for normalising later counter = np.zeros_like(main_results, dtype=np.float32) - for i, (start, stop) in ProgressBar(enumerate(zip(self._starts, - self._stops)), total=self.nruns, - desc="Performing run"): + for i, (start, stop) in ProgressBar( + enumerate(zip(self._starts, self._stops)), + total=self.nruns, + desc="Performing run", + ): # needed else trj seek thinks a np.int64 isn't an int? results = self._single_run(int(start), int(stop)) @@ -414,10 +438,12 @@ def run(self, force=False): main_results /= counter - self.solution['time'] = np.arange( - len(main_results), - dtype=np.float32) * self.u.trajectory.dt * self._skip - self.solution['results'] = main_results + self.solution["time"] = ( + np.arange(len(main_results), dtype=np.float32) + * self.u.trajectory.dt + * self._skip + ) + self.solution["results"] = main_results def _single_run(self, start, stop): """Perform a single pass of the trajectory""" @@ -427,49 +453,62 @@ def _single_run(self, start, stop): box = self.u.dimensions if self.pbc else None # 2d array of all distances - pair = capped_distance(self.h.positions, self.a.positions, - max_cutoff=self.d_crit, box=box, - return_distances=False) + pair = capped_distance( + self.h.positions, + self.a.positions, + max_cutoff=self.d_crit, + box=box, + return_distances=False, + ) if self.exclusions is not None: - pair = pair[~ _in2d(pair, self.exclusions)] + pair = pair[~_in2d(pair, self.exclusions)] hidx, aidx = np.transpose(pair) - - a = calc_angles(self.d.positions[hidx], self.h.positions[hidx], - self.a.positions[aidx], box=box) + a = calc_angles( + self.d.positions[hidx], + self.h.positions[hidx], + self.a.positions[aidx], + box=box, + ) # from amongst those, who also satisfiess angle crit idx2 = np.where(a > self.a_crit) hidx = hidx[idx2] aidx = aidx[idx2] nbonds = len(hidx) # number of hbonds at t=0 - results = np.zeros_like(np.arange(start, stop, self._skip), - dtype=np.float32) + results = np.zeros_like( + np.arange(start, stop, self._skip), dtype=np.float32 + ) if self.time_cut: # counter for time criteria count = np.zeros(nbonds, dtype=np.float64) - for i, ts in enumerate(self.u.trajectory[start:stop:self._skip]): + for i, ts in enumerate(self.u.trajectory[start : stop : self._skip]): box = self.u.dimensions if self.pbc else None - d = calc_bonds(self.h.positions[hidx], self.a.positions[aidx], - box=box) - a = calc_angles(self.d.positions[hidx], self.h.positions[hidx], - self.a.positions[aidx], box=box) + d = calc_bonds( + self.h.positions[hidx], self.a.positions[aidx], box=box + ) + a = calc_angles( + self.d.positions[hidx], + self.h.positions[hidx], + self.a.positions[aidx], + box=box, + ) winners = (d < self.d_crit) & (a > self.a_crit) results[i] = winners.sum() - if self.bond_type == 'continuous': + if self.bond_type == "continuous": # Remove losers for continuous definition hidx = hidx[np.where(winners)] aidx = aidx[np.where(winners)] - elif self.bond_type == 'intermittent': + elif self.bond_type == "intermittent": if self.time_cut: # Add to counter of where losers are - count[~ winners] += self._skip * self.u.trajectory.dt + count[~winners] += self._skip * self.u.trajectory.dt count[winners] = 0 # Reset timer for winners # Remove if you've lost too many times @@ -521,14 +560,15 @@ def solve(self, p_guess=None): """ - if self.solution['results'] is None: + if self.solution["results"] is None: raise ValueError( - "Results have not been generated use, the run method first") + "Results have not been generated use, the run method first" + ) # Prevents an odd bug with leastsq where it expects # double precision data sometimes... - time = self.solution['time'].astype(np.float64) - results = self.solution['results'].astype(np.float64) + time = self.solution["time"].astype(np.float64) + results = self.solution["results"].astype(np.float64) def within_bounds(p): """Returns True/False if boundary conditions are met or not. @@ -542,13 +582,19 @@ def within_bounds(p): """ if len(p) == 3: A1, tau1, tau2 = p - return (A1 > 0.0) & (A1 < 1.0) & \ - (tau1 > 0.0) & (tau2 > 0.0) + return (A1 > 0.0) & (A1 < 1.0) & (tau1 > 0.0) & (tau2 > 0.0) elif len(p) == 5: A1, A2, tau1, tau2, tau3 = p - return (A1 > 0.0) & (A1 < 1.0) & (A2 > 0.0) & \ - (A2 < 1.0) & ((A1 + A2) < 1.0) & \ - (tau1 > 0.0) & (tau2 > 0.0) & (tau3 > 0.0) + return ( + (A1 > 0.0) + & (A1 < 1.0) + & (A2 > 0.0) + & (A2 < 1.0) + & ((A1 + A2) < 1.0) + & (tau1 > 0.0) + & (tau2 > 0.0) + & (tau3 > 0.0) + ) def err(p, x, y): """Custom residual function, returns real residual if all @@ -561,52 +607,66 @@ def err(p, x, y): return np.full_like(y, 100000) def double(x, A1, tau1, tau2): - """ Sum of two exponential functions """ + """Sum of two exponential functions""" A2 = 1 - A1 return A1 * np.exp(-x / tau1) + A2 * np.exp(-x / tau2) def triple(x, A1, A2, tau1, tau2, tau3): - """ Sum of three exponential functions """ + """Sum of three exponential functions""" A3 = 1 - (A1 + A2) - return A1 * np.exp(-x / tau1) + A2 * np.exp(-x / tau2) + A3 * np.exp(-x / tau3) + return ( + A1 * np.exp(-x / tau1) + + A2 * np.exp(-x / tau2) + + A3 * np.exp(-x / tau3) + ) - if self.bond_type == 'continuous': + if self.bond_type == "continuous": self._my_solve = double if p_guess is None: p_guess = (0.5, 10 * self.sample_time, self.sample_time) p, cov, infodict, mesg, ier = scipy.optimize.leastsq( - err, p_guess, args=(time, results), full_output=True) - self.solution['fit'] = p + err, p_guess, args=(time, results), full_output=True + ) + self.solution["fit"] = p A1, tau1, tau2 = p A2 = 1 - A1 - self.solution['tau'] = A1 * tau1 + A2 * tau2 + self.solution["tau"] = A1 * tau1 + A2 * tau2 else: self._my_solve = triple if p_guess is None: - p_guess = (0.33, 0.33, 10 * self.sample_time, - self.sample_time, 0.1 * self.sample_time) + p_guess = ( + 0.33, + 0.33, + 10 * self.sample_time, + self.sample_time, + 0.1 * self.sample_time, + ) p, cov, infodict, mesg, ier = scipy.optimize.leastsq( - err, p_guess, args=(time, results), full_output=True) - self.solution['fit'] = p + err, p_guess, args=(time, results), full_output=True + ) + self.solution["fit"] = p A1, A2, tau1, tau2, tau3 = p A3 = 1 - A1 - A2 - self.solution['tau'] = A1 * tau1 + A2 * tau2 + A3 * tau3 + self.solution["tau"] = A1 * tau1 + A2 * tau2 + A3 * tau3 - self.solution['infodict'] = infodict - self.solution['mesg'] = mesg - self.solution['ier'] = ier + self.solution["infodict"] = infodict + self.solution["mesg"] = mesg + self.solution["ier"] = ier if ier in [1, 2, 3, 4]: # solution found if ier is one of these values - self.solution['estimate'] = self._my_solve( - self.solution['time'], *p) + self.solution["estimate"] = self._my_solve( + self.solution["time"], *p + ) else: warnings.warn("Solution to results not found", RuntimeWarning) def __repr__(self): - return ("" - "".format(btype=self.bond_type, n=len(self.h))) + return ( + "" + "".format(btype=self.bond_type, n=len(self.h)) + ) diff --git a/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py b/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py index 69f281b4f75..027c0b71255 100644 --- a/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py +++ b/package/MDAnalysis/analysis/hydrogenbonds/wbridge_analysis.py @@ -706,17 +706,19 @@ def analysis(current, output, u, **kwargs): Will be removed in MDAnalysis 3.0.0. Please use :attr:`results.timeseries` instead. """ -from collections import defaultdict import logging import warnings +from collections import defaultdict + import numpy as np -from ..base import AnalysisBase +from MDAnalysis import MissingDataWarning, NoDataError, SelectionError +from MDAnalysis.lib.distances import calc_angles, capped_distance from MDAnalysis.lib.NeighborSearch import AtomNeighborSearch -from MDAnalysis.lib.distances import capped_distance, calc_angles -from MDAnalysis import NoDataError, MissingDataWarning, SelectionError -logger = logging.getLogger('MDAnalysis.analysis.WaterBridgeAnalysis') +from ..base import AnalysisBase + +logger = logging.getLogger("MDAnalysis.analysis.WaterBridgeAnalysis") class WaterBridgeAnalysis(AnalysisBase): @@ -730,6 +732,7 @@ class WaterBridgeAnalysis(AnalysisBase): .. versionadded:: 0.17.0 """ + # use tuple(set()) here so that one can just copy&paste names from the # table; set() takes care for removing duplicates. At the end the # DEFAULT_DONORS and DEFAULT_ACCEPTORS should simply be tuples. @@ -738,39 +741,91 @@ class WaterBridgeAnalysis(AnalysisBase): #: (see :ref:`Default atom names for water bridge analysis`); #: use the keyword `donors` to add a list of additional donor names. DEFAULT_DONORS = { - 'CHARMM27': tuple( - {'N', 'OH2', 'OW', 'NE', 'NH1', 'NH2', 'ND2', 'SG', 'NE2', 'ND1', - 'NZ', 'OG', 'OG1', 'NE1', 'OH'}), - 'GLYCAM06': tuple({'N', 'NT', 'N3', 'OH', 'OW'}), - 'other': tuple(set([]))} + "CHARMM27": tuple( + { + "N", + "OH2", + "OW", + "NE", + "NH1", + "NH2", + "ND2", + "SG", + "NE2", + "ND1", + "NZ", + "OG", + "OG1", + "NE1", + "OH", + } + ), + "GLYCAM06": tuple({"N", "NT", "N3", "OH", "OW"}), + "other": tuple(set([])), + } #: default atom names that are treated as hydrogen *acceptors* #: (see :ref:`Default atom names for water bridge analysis`); #: use the keyword `acceptors` to add a list of additional acceptor names. DEFAULT_ACCEPTORS = { - 'CHARMM27': tuple( - {'O', 'OC1', 'OC2', 'OH2', 'OW', 'OD1', 'OD2', 'SG', 'OE1', 'OE1', - 'OE2', 'ND1', 'NE2', 'SD', 'OG', 'OG1', 'OH'}), - 'GLYCAM06': - tuple({'N', 'NT', 'O', 'O2', 'OH', 'OS', 'OW', 'OY', 'SM'}), - 'other': tuple(set([]))} + "CHARMM27": tuple( + { + "O", + "OC1", + "OC2", + "OH2", + "OW", + "OD1", + "OD2", + "SG", + "OE1", + "OE1", + "OE2", + "ND1", + "NE2", + "SD", + "OG", + "OG1", + "OH", + } + ), + "GLYCAM06": tuple( + {"N", "NT", "O", "O2", "OH", "OS", "OW", "OY", "SM"} + ), + "other": tuple(set([])), + } #: A :class:`collections.defaultdict` of covalent radii of common donors #: (used in :meth`_get_bonded_hydrogens_list` to check if a hydrogen is #: sufficiently close to its donor heavy atom). Values are stored for #: N, O, P, and S. Any other heavy atoms are assumed to have hydrogens #: covalently bound at a maximum distance of 1.5 Ã…. - r_cov = defaultdict(lambda: 1.5, # default value - N=1.31, O=1.31, P=1.58, S=1.55) # noqa: E741 - - def __init__(self, universe, selection1='protein', - selection2='not resname SOL', water_selection='resname SOL', - order=1, selection1_type='both', update_selection=False, - update_water_selection=True, filter_first=True, - distance_type='hydrogen', distance=3.0, angle=120.0, - forcefield='CHARMM27', donors=None, acceptors=None, - output_format="sele1_sele2", debug=None, - pbc=False, **kwargs): + r_cov = defaultdict( + lambda: 1.5, N=1.31, O=1.31, P=1.58, S=1.55 # default value + ) # noqa: E741 + + def __init__( + self, + universe, + selection1="protein", + selection2="not resname SOL", + water_selection="resname SOL", + order=1, + selection1_type="both", + update_selection=False, + update_water_selection=True, + filter_first=True, + distance_type="hydrogen", + distance=3.0, + angle=120.0, + forcefield="CHARMM27", + donors=None, + acceptors=None, + output_format="sele1_sele2", + debug=None, + pbc=False, + **kwargs, + ): """Set up the calculation of water bridges between two selections in a universe. @@ -904,8 +959,9 @@ def __init__(self, universe, selection1='protein', """ - super(WaterBridgeAnalysis, self).__init__(universe.trajectory, - **kwargs) + super(WaterBridgeAnalysis, self).__init__( + universe.trajectory, **kwargs + ) self.water_selection = water_selection self.update_water_selection = update_water_selection # per-frame debugging output? @@ -929,7 +985,9 @@ def __init__(self, universe, selection1='protein', self.filter_first = filter_first self.distance = distance if distance_type not in {"hydrogen", "heavy"}: - raise ValueError(f"Only 'hydrogen' and 'heavy' are allowed for option `distance_type' ({distance_type}).") + raise ValueError( + f"Only 'hydrogen' and 'heavy' are allowed for option `distance_type' ({distance_type})." + ) self.distance_type = distance_type # will give the default behavior self.angle = angle @@ -943,13 +1001,15 @@ def __init__(self, universe, selection1='protein', acceptors = () self.forcefield = forcefield self.donors = tuple(set(self.DEFAULT_DONORS[forcefield]).union(donors)) - self.acceptors = tuple(set(self.DEFAULT_ACCEPTORS[forcefield]).union( - acceptors)) + self.acceptors = tuple( + set(self.DEFAULT_ACCEPTORS[forcefield]).union(acceptors) + ) - if self.selection1_type not in ('both', 'donor', 'acceptor'): - raise ValueError('WaterBridgeAnalysis: ' - 'Invalid selection type {0!s}'.format( - self.selection1_type)) + if self.selection1_type not in ("both", "donor", "acceptor"): + raise ValueError( + "WaterBridgeAnalysis: " + "Invalid selection type {0!s}".format(self.selection1_type) + ) # final result accessed as self.results.network self.results.network = [] @@ -960,18 +1020,31 @@ def __init__(self, universe, selection1='protein', def _log_parameters(self): """Log important parameters to the logfile.""" - logger.info("WaterBridgeAnalysis: selection = %r (update: %r)", - self.selection2, self.update_selection) - logger.info("WaterBridgeAnalysis: water selection = %r (update: %r)", - self.water_selection, self.update_water_selection) - logger.info("WaterBridgeAnalysis: criterion: donor %s atom and " - "acceptor atom distance <= %.3f A", self.distance_type, - self.distance) - logger.info("WaterBridgeAnalysis: criterion: " - "angle D-H-A >= %.3f degrees", - self.angle) - logger.info("WaterBridgeAnalysis: force field %s to guess donor and \ - acceptor names", self.forcefield) + logger.info( + "WaterBridgeAnalysis: selection = %r (update: %r)", + self.selection2, + self.update_selection, + ) + logger.info( + "WaterBridgeAnalysis: water selection = %r (update: %r)", + self.water_selection, + self.update_water_selection, + ) + logger.info( + "WaterBridgeAnalysis: criterion: donor %s atom and " + "acceptor atom distance <= %.3f A", + self.distance_type, + self.distance, + ) + logger.info( + "WaterBridgeAnalysis: criterion: " "angle D-H-A >= %.3f degrees", + self.angle, + ) + logger.info( + "WaterBridgeAnalysis: force field %s to guess donor and \ + acceptor names", + self.forcefield, + ) def _build_residue_dict(self, selection): # Build the residue_dict where the key is the residue name @@ -985,15 +1058,17 @@ def _build_residue_dict(self, selection): for atom in residue.atoms: if atom.name in self.donors: self._residue_dict[residue.resname][atom.name].update( - self._get_bonded_hydrogens(atom).names) + self._get_bonded_hydrogens(atom).names + ) def _update_donor_h(self, atom_ix, h_donors, donors_h): atom = self.u.atoms[atom_ix] residue = atom.residue hydrogen_names = self._residue_dict[residue.resname][atom.name] if hydrogen_names: - hydrogens = residue.atoms.select_atoms('name {0}'.format( - ' '.join(hydrogen_names))).ix + hydrogens = residue.atoms.select_atoms( + "name {0}".format(" ".join(hydrogen_names)) + ).ix for atom in hydrogens: h_donors[atom] = atom_ix donors_h[atom_ix].append(atom) @@ -1013,68 +1088,108 @@ def _update_selection(self): self._s2 = self.u.select_atoms(self.selection2).ix if self.filter_first and len(self._s1): - self.logger_debug('Size of selection 1 before filtering:' - ' {} atoms'.format(len(self._s1))) - ns_selection_1 = AtomNeighborSearch(self.u.atoms[self._s1], - box=self.box) - self._s1 = ns_selection_1.search(self.u.atoms[self._s2], - self.selection_distance).ix - self.logger_debug("Size of selection 1: {0} atoms".format( - len(self._s1))) + self.logger_debug( + "Size of selection 1 before filtering:" + " {} atoms".format(len(self._s1)) + ) + ns_selection_1 = AtomNeighborSearch( + self.u.atoms[self._s1], box=self.box + ) + self._s1 = ns_selection_1.search( + self.u.atoms[self._s2], self.selection_distance + ).ix + self.logger_debug( + "Size of selection 1: {0} atoms".format(len(self._s1)) + ) if len(self._s1) == 0: - logger.warning('Selection 1 "{0}" did not select any atoms.' - .format(str(self.selection1)[:80])) + logger.warning( + 'Selection 1 "{0}" did not select any atoms.'.format( + str(self.selection1)[:80] + ) + ) return if self.filter_first and len(self._s2): - self.logger_debug('Size of selection 2 before filtering:' - ' {} atoms'.format(len(self._s2))) - ns_selection_2 = AtomNeighborSearch(self.u.atoms[self._s2], - box=self.box) - self._s2 = ns_selection_2.search(self.u.atoms[self._s1], - self.selection_distance).ix - self.logger_debug('Size of selection 2: {0} atoms'.format( - len(self._s2))) + self.logger_debug( + "Size of selection 2 before filtering:" + " {} atoms".format(len(self._s2)) + ) + ns_selection_2 = AtomNeighborSearch( + self.u.atoms[self._s2], box=self.box + ) + self._s2 = ns_selection_2.search( + self.u.atoms[self._s1], self.selection_distance + ).ix + self.logger_debug( + "Size of selection 2: {0} atoms".format(len(self._s2)) + ) if len(self._s2) == 0: - logger.warning('Selection 2 "{0}" did not select any atoms.' - .format(str(self.selection2)[:80])) + logger.warning( + 'Selection 2 "{0}" did not select any atoms.'.format( + str(self.selection2)[:80] + ) + ) return - if self.selection1_type in ('donor', 'both'): - self._s1_donors = self.u.atoms[self._s1].select_atoms( - 'name {0}'.format(' '.join(self.donors))).ix + if self.selection1_type in ("donor", "both"): + self._s1_donors = ( + self.u.atoms[self._s1] + .select_atoms("name {0}".format(" ".join(self.donors))) + .ix + ) for atom_ix in self._s1_donors: - self._update_donor_h(atom_ix, self._s1_h_donors, - self._s1_donors_h) - self.logger_debug("Selection 1 donors: {0}".format( - len(self._s1_donors))) - self.logger_debug("Selection 1 donor hydrogens: {0}".format( - len(self._s1_h_donors))) - if self.selection1_type in ('acceptor', 'both'): - self._s1_acceptors = self.u.atoms[self._s1].select_atoms( - 'name {0}'.format(' '.join(self.acceptors))).ix - self.logger_debug("Selection 1 acceptors: {0}".format( - len(self._s1_acceptors))) + self._update_donor_h( + atom_ix, self._s1_h_donors, self._s1_donors_h + ) + self.logger_debug( + "Selection 1 donors: {0}".format(len(self._s1_donors)) + ) + self.logger_debug( + "Selection 1 donor hydrogens: {0}".format( + len(self._s1_h_donors) + ) + ) + if self.selection1_type in ("acceptor", "both"): + self._s1_acceptors = ( + self.u.atoms[self._s1] + .select_atoms("name {0}".format(" ".join(self.acceptors))) + .ix + ) + self.logger_debug( + "Selection 1 acceptors: {0}".format(len(self._s1_acceptors)) + ) if len(self._s2) == 0: return None - if self.selection1_type in ('donor', 'both'): - self._s2_acceptors = self.u.atoms[self._s2].select_atoms( - 'name {0}'.format(' '.join(self.acceptors))).ix - self.logger_debug("Selection 2 acceptors: {0:d}".format( - len(self._s2_acceptors))) - if self.selection1_type in ('acceptor', 'both'): - self._s2_donors = self.u.atoms[self._s2].select_atoms( - 'name {0}'.format(' '.join(self.donors))).ix + if self.selection1_type in ("donor", "both"): + self._s2_acceptors = ( + self.u.atoms[self._s2] + .select_atoms("name {0}".format(" ".join(self.acceptors))) + .ix + ) + self.logger_debug( + "Selection 2 acceptors: {0:d}".format(len(self._s2_acceptors)) + ) + if self.selection1_type in ("acceptor", "both"): + self._s2_donors = ( + self.u.atoms[self._s2] + .select_atoms("name {0}".format(" ".join(self.donors))) + .ix + ) for atom_ix in self._s2_donors: - self._update_donor_h(atom_ix, self._s2_h_donors, - self._s2_donors_h) - self.logger_debug("Selection 2 donors: {0:d}".format( - len(self._s2_donors))) - self.logger_debug("Selection 2 donor hydrogens: {0:d}".format( - len(self._s2_h_donors))) + self._update_donor_h( + atom_ix, self._s2_h_donors, self._s2_donors_h + ) + self.logger_debug( + "Selection 2 donors: {0:d}".format(len(self._s2_donors)) + ) + self.logger_debug( + "Selection 2 donor hydrogens: {0:d}".format( + len(self._s2_h_donors) + ) + ) def _update_water_selection(self): self._water_donors = [] @@ -1083,37 +1198,55 @@ def _update_water_selection(self): self._water_acceptors = [] self._water = self.u.select_atoms(self.water_selection).ix - self.logger_debug('Size of water selection before filtering:' - ' {} atoms'.format(len(self._water))) + self.logger_debug( + "Size of water selection before filtering:" + " {} atoms".format(len(self._water)) + ) if len(self._water) and self.filter_first: - filtered_s1 = AtomNeighborSearch(self.u.atoms[self._water], - box=self.box).search( - self.u.atoms[self._s1], self.selection_distance) + filtered_s1 = AtomNeighborSearch( + self.u.atoms[self._water], box=self.box + ).search(self.u.atoms[self._s1], self.selection_distance) if filtered_s1: - self._water = AtomNeighborSearch(filtered_s1, - box=self.box).search( - self.u.atoms[self._s2], self.selection_distance).ix - - self.logger_debug("Size of water selection: {0} atoms".format( - len(self._water))) + self._water = ( + AtomNeighborSearch(filtered_s1, box=self.box) + .search(self.u.atoms[self._s2], self.selection_distance) + .ix + ) + + self.logger_debug( + "Size of water selection: {0} atoms".format(len(self._water)) + ) if len(self._water) == 0: - logger.warning("Water selection '{0}' did not select any atoms." - .format(str(self.water_selection)[:80])) + logger.warning( + "Water selection '{0}' did not select any atoms.".format( + str(self.water_selection)[:80] + ) + ) else: - self._water_donors = self.u.atoms[self._water].select_atoms( - 'name {0}'.format(' '.join(self.donors))).ix + self._water_donors = ( + self.u.atoms[self._water] + .select_atoms("name {0}".format(" ".join(self.donors))) + .ix + ) for atom_ix in self._water_donors: - self._update_donor_h(atom_ix, self._water_h_donors, - self._water_donors_h) - self.logger_debug("Water donors: {0}".format( - len(self._water_donors))) - self.logger_debug("Water donor hydrogens: {0}".format( - len(self._water_h_donors))) - self._water_acceptors = self.u.atoms[self._water].select_atoms( - 'name {0}'.format(' '.join(self.acceptors))).ix - self.logger_debug("Water acceptors: {0}".format( - len(self._water_acceptors))) + self._update_donor_h( + atom_ix, self._water_h_donors, self._water_donors_h + ) + self.logger_debug( + "Water donors: {0}".format(len(self._water_donors)) + ) + self.logger_debug( + "Water donor hydrogens: {0}".format(len(self._water_h_donors)) + ) + self._water_acceptors = ( + self.u.atoms[self._water] + .select_atoms("name {0}".format(" ".join(self.acceptors))) + .ix + ) + self.logger_debug( + "Water acceptors: {0}".format(len(self._water_acceptors)) + ) def _get_bonded_hydrogens(self, atom): """Find hydrogens bonded within cutoff to `atom`. @@ -1145,7 +1278,8 @@ def _get_bonded_hydrogens(self, atom): try: return atom.residue.atoms.select_atoms( "(name H* 1H* 2H* 3H* or type H) and around {0:f} name {1!s}" - "".format(self.r_cov[atom.name[0]], atom.name)) + "".format(self.r_cov[atom.name[0]], atom.name) + ) except NoDataError: return [] @@ -1157,7 +1291,7 @@ def _prepare(self): # The distance for selection is defined as twice the maximum bond # length of an O-H bond (2A) plus order of water bridge times the # length of OH bond plus hydrogne bond distance - self.selection_distance = (2 * 2 + self.order * (2 + self.distance)) + self.selection_distance = 2 * 2 + self.order * (2 + self.distance) self.box = self.u.dimensions if self.pbc else None self._residue_dict = {} @@ -1171,42 +1305,51 @@ def _prepare(self): if len(self._s1) and len(self._s2): self._update_water_selection() else: - logger.info("WaterBridgeAnalysis: " - "no atoms found in the selection.") + logger.info( + "WaterBridgeAnalysis: " "no atoms found in the selection." + ) logger.info("WaterBridgeAnalysis: initial checks passed.") logger.info("WaterBridgeAnalysis: starting") logger.debug("WaterBridgeAnalysis: donors %r", self.donors) logger.debug("WaterBridgeAnalysis: acceptors %r", self.acceptors) - logger.debug("WaterBridgeAnalysis: water bridge %r", - self.water_selection) + logger.debug( + "WaterBridgeAnalysis: water bridge %r", self.water_selection + ) if self.debug: logger.debug("Toggling debug to %r", self.debug) else: - logger.debug("WaterBridgeAnalysis: For full step-by-step " - "debugging output use debug=True") + logger.debug( + "WaterBridgeAnalysis: For full step-by-step " + "debugging output use debug=True" + ) - logger.info("Starting analysis " - "(frame index start=%d stop=%d, step=%d)", - self.start, self.stop, self.step) + logger.info( + "Starting analysis " "(frame index start=%d stop=%d, step=%d)", + self.start, + self.stop, + self.step, + ) def _donor2acceptor(self, donors, h_donors, acceptor): if len(donors) == 0 or len(acceptor) == 0: return [] - if self.distance_type != 'heavy': + if self.distance_type != "heavy": donors_idx = list(h_donors.keys()) else: donors_idx = list(donors.keys()) result = [] # Code modified from p-j-smith - pairs, distances = capped_distance(self.u.atoms[donors_idx].positions, - self.u.atoms[acceptor].positions, - max_cutoff=self.distance, - box=self.box, - return_distances=True) - if self.distance_type == 'hydrogen': + pairs, distances = capped_distance( + self.u.atoms[donors_idx].positions, + self.u.atoms[acceptor].positions, + max_cutoff=self.distance, + box=self.box, + return_distances=True, + ) + if self.distance_type == "hydrogen": tmp_distances = distances tmp_donors = [h_donors[donors_idx[idx]] for idx in pairs[:, 0]] tmp_hydrogens = [donors_idx[idx] for idx in pairs[:, 0]] @@ -1231,7 +1374,7 @@ def _donor2acceptor(self, donors, h_donors, acceptor): self.u.atoms[tmp_donors].positions, self.u.atoms[tmp_hydrogens].positions, self.u.atoms[tmp_acceptors].positions, - box=self.box + box=self.box, ) ) hbond_indices = np.where(angles > self.angle)[0] @@ -1239,9 +1382,17 @@ def _donor2acceptor(self, donors, h_donors, acceptor): h = tmp_hydrogens[index] d = tmp_donors[index] a = tmp_acceptors[index] - result.append((h, d, a, self._expand_index(h), - self._expand_index(a), - tmp_distances[index], angles[index])) + result.append( + ( + h, + d, + a, + self._expand_index(h), + self._expand_index(a), + tmp_distances[index], + angles[index], + ) + ) return result def _single_frame(self): @@ -1262,87 +1413,151 @@ def _single_frame(self): next_round_water = set([]) selection_2 = [] - if self.selection1_type in ('donor', 'both'): + if self.selection1_type in ("donor", "both"): # check for direct hbond from s1 to s2 self.logger_debug("Selection 1 Donors <-> Selection 2 Acceptors") results = self._donor2acceptor( - self._s1_donors_h, self._s1_h_donors, self._s2_acceptors) + self._s1_donors_h, self._s1_h_donors, self._s2_acceptors + ) for line in results: - h_index, d_index, a_index, (h_resname, h_resid, h_name), \ - (a_resname, a_resid, a_name), dist, angle = line + ( + h_index, + d_index, + a_index, + (h_resname, h_resid, h_name), + (a_resname, a_resid, a_name), + dist, + angle, + ) = line water_pool[(a_resname, a_resid)] = None selection_1.append( - (h_index, d_index, a_index, None, dist, angle)) + (h_index, d_index, a_index, None, dist, angle) + ) selection_2.append((a_resname, a_resid)) if self.order > 0: self.logger_debug("Selection 1 Donors <-> Water Acceptors") results = self._donor2acceptor( - self._s1_donors_h, self._s1_h_donors, - self._water_acceptors) + self._s1_donors_h, self._s1_h_donors, self._water_acceptors + ) for line in results: - h_index, d_index, a_index, (h_resname, h_resid, h_name), ( - a_resname, a_resid, a_name), dist, angle = line + ( + h_index, + d_index, + a_index, + (h_resname, h_resid, h_name), + (a_resname, a_resid, a_name), + dist, + angle, + ) = line selection_1.append( - (h_index, d_index, a_index, None, dist, angle)) + (h_index, d_index, a_index, None, dist, angle) + ) self.logger_debug("Water Donors <-> Selection 2 Acceptors") results = self._donor2acceptor( - self._water_donors_h, self._water_h_donors, - self._s2_acceptors) + self._water_donors_h, + self._water_h_donors, + self._s2_acceptors, + ) for line in results: - h_index, d_index, a_index, (h_resname, h_resid, h_name), ( - a_resname, a_resid, a_name), dist, angle = line + ( + h_index, + d_index, + a_index, + (h_resname, h_resid, h_name), + (a_resname, a_resid, a_name), + dist, + angle, + ) = line water_pool[(h_resname, h_resid)].append( - (h_index, d_index, a_index, None, dist, angle)) + (h_index, d_index, a_index, None, dist, angle) + ) selection_2.append((a_resname, a_resid)) - if self.selection1_type in ('acceptor', 'both'): + if self.selection1_type in ("acceptor", "both"): self.logger_debug("Selection 2 Donors <-> Selection 1 Acceptors") - results = self._donor2acceptor(self._s2_donors_h, - self._s2_h_donors, - self._s1_acceptors) + results = self._donor2acceptor( + self._s2_donors_h, self._s2_h_donors, self._s1_acceptors + ) for line in results: - h_index, d_index, a_index, (h_resname, h_resid, h_name), \ - (a_resname, a_resid, a_name), dist, angle = line + ( + h_index, + d_index, + a_index, + (h_resname, h_resid, h_name), + (a_resname, a_resid, a_name), + dist, + angle, + ) = line water_pool[(h_resname, h_resid)] = None selection_1.append( - (a_index, None, h_index, d_index, dist, angle)) + (a_index, None, h_index, d_index, dist, angle) + ) selection_2.append((h_resname, h_resid)) if self.order > 0: self.logger_debug("Selection 2 Donors <-> Water Acceptors") results = self._donor2acceptor( - self._s2_donors_h, self._s2_h_donors, - self._water_acceptors) + self._s2_donors_h, self._s2_h_donors, self._water_acceptors + ) for line in results: - h_index, d_index, a_index, (h_resname, h_resid, h_name), ( - a_resname, a_resid, a_name), dist, angle = line + ( + h_index, + d_index, + a_index, + (h_resname, h_resid, h_name), + (a_resname, a_resid, a_name), + dist, + angle, + ) = line water_pool[(a_resname, a_resid)].append( - (a_index, None, h_index, d_index, dist, angle)) + (a_index, None, h_index, d_index, dist, angle) + ) selection_2.append((h_resname, h_resid)) self.logger_debug("Selection 1 Acceptors <-> Water Donors") results = self._donor2acceptor( - self._water_donors_h, self._water_h_donors, - self._s1_acceptors) + self._water_donors_h, + self._water_h_donors, + self._s1_acceptors, + ) for line in results: - h_index, d_index, a_index, (h_resname, h_resid, h_name), ( - a_resname, a_resid, a_name), dist, angle = line + ( + h_index, + d_index, + a_index, + (h_resname, h_resid, h_name), + (a_resname, a_resid, a_name), + dist, + angle, + ) = line selection_1.append( - (a_index, None, h_index, d_index, dist, angle)) + (a_index, None, h_index, d_index, dist, angle) + ) if self.order > 1: self.logger_debug("Water donor <-> Water Acceptors") - results = self._donor2acceptor(self._water_donors_h, - self._water_h_donors, - self._water_acceptors) + results = self._donor2acceptor( + self._water_donors_h, + self._water_h_donors, + self._water_acceptors, + ) for line in results: - h_index, d_index, a_index, (h_resname, h_resid, h_name), ( - a_resname, a_resid, a_name), dist, angle = line + ( + h_index, + d_index, + a_index, + (h_resname, h_resid, h_name), + (a_resname, a_resid, a_name), + dist, + angle, + ) = line water_pool[(a_resname, a_resid)].append( - (a_index, None, h_index, d_index, dist, angle)) + (a_index, None, h_index, d_index, dist, angle) + ) water_pool[(h_resname, h_resid)].append( - (h_index, d_index, a_index, None, dist, angle)) + (h_index, d_index, a_index, None, dist, angle) + ) # solve the connectivity network # The following code attempt to generate a water network which is @@ -1362,24 +1577,25 @@ def _single_frame(self): # If the value of a certain key is None, which means it is reaching # selection 2. - result = {'start': defaultdict(dict), 'water': defaultdict(dict)} + result = {"start": defaultdict(dict), "water": defaultdict(dict)} def add_route(result, route): if len(route) == 1: - result['start'][route[0]] = None + result["start"][route[0]] = None else: # exclude the the selection which goes back to itself - if (sorted(route[0][0:3:2]) == sorted(route[-1][0:3:2])): + if sorted(route[0][0:3:2]) == sorted(route[-1][0:3:2]): return # selection 2 to water - result['water'][route[-1]] = None + result["water"][route[-1]] = None # water to water for i in range(1, len(route) - 1): - result['water'][route[i]][route[i+1]] = \ - result['water'][route[i+1]] + result["water"][route[i]][route[i + 1]] = result["water"][ + route[i + 1] + ] # selection 1 to water - result['start'][route[0]][route[1]] = result['water'][route[1]] + result["start"][route[0]][route[1]] = result["water"][route[1]] def traverse_water_network(graph, node, end, route, maxdepth, result): if len(route) > self.order + 1: @@ -1395,20 +1611,33 @@ def traverse_water_network(graph, node, end, route, maxdepth, result): new_route = route[:] new_route.append(new_node) new_node = self._expand_timeseries( - new_node, 'sele1_sele2')[3][:2] - traverse_water_network(graph, new_node, end, new_route, - maxdepth, result) - for s1 in selection_1: - route = [s1, ] - next_mol = self._expand_timeseries(s1, 'sele1_sele2')[3][:2] - traverse_water_network(water_pool, next_mol, selection_2, route[:], - self.order, result) + new_node, "sele1_sele2" + )[3][:2] + traverse_water_network( + graph, new_node, end, new_route, maxdepth, result + ) - self.results.network.append(result['start']) + for s1 in selection_1: + route = [ + s1, + ] + next_mol = self._expand_timeseries(s1, "sele1_sele2")[3][:2] + traverse_water_network( + water_pool, next_mol, selection_2, route[:], self.order, result + ) - def _traverse_water_network(self, graph, current, analysis_func=None, - output=None, link_func=None, **kwargs): - ''' + self.results.network.append(result["start"]) + + def _traverse_water_network( + self, + graph, + current, + analysis_func=None, + output=None, + link_func=None, + **kwargs, + ): + """ This function recursively traverses the water network self.results.network and finds the hydrogen bonds which connect the current atom to the next atom. The newly found hydrogen bond will be @@ -1426,7 +1655,7 @@ def _traverse_water_network(self, graph, current, analysis_func=None, :param link_func: The new hydrogen bonds will be appended to current. :param kwargs: the keywords which are passed into the analysis_func. :return: - ''' + """ if link_func is None: # If no link_func is provided, the default link_func will be used link_func = self._full_link @@ -1443,19 +1672,24 @@ def _traverse_water_network(self, graph, current, analysis_func=None, for node in graph: # the new hydrogen bond will be added to the existing bonds new = link_func(current, node) - self._traverse_water_network(graph[node], new, - analysis_func, output, - link_func, **kwargs) + self._traverse_water_network( + graph[node], + new, + analysis_func, + output, + link_func, + **kwargs, + ) def _expand_index(self, index): - ''' + """ Expand the index into (resname, resid, name). - ''' + """ atom = self.u.atoms[index] return (atom.resname, atom.resid, atom.name) def _expand_timeseries(self, entry, output_format=None): - ''' + """ Expand the compact data format into the old timeseries form. The old is defined as the format for release up to 0.19.2. As is discussed in Issue #2177, the internal storage of the hydrogen @@ -1479,17 +1713,17 @@ def _expand_timeseries(self, entry, output_format=None): [donor_index, acceptor_index, (donor_resname, donor_resid, donor_name), (acceptor_resname, acceptor_resid, acceptor_name), dist, angle] - ''' + """ output_format = output_format or self.output_format # Expand the compact entry into atom1, which is the first index in the # output and atom2, which is the second # entry. atom1, heavy_atom1, atom2, heavy_atom2, dist, angle = entry - if output_format == 'sele1_sele2': + if output_format == "sele1_sele2": # If the output format is the sele1_sele2, no change will be # executed atom1, atom2 = atom1, atom2 - elif output_format == 'donor_acceptor': + elif output_format == "donor_acceptor": # If the output format is donor_acceptor, use heavy atom position # to check which is donor and which is # acceptor @@ -1503,13 +1737,20 @@ def _expand_timeseries(self, entry, output_format=None): else: raise KeyError( "Only 'sele1_sele2' or 'donor_acceptor' are allowed as output " - "format") + "format" + ) - return (atom1, atom2, self._expand_index(atom1), - self._expand_index(atom2), dist, angle) + return ( + atom1, + atom2, + self._expand_index(atom1), + self._expand_index(atom2), + dist, + angle, + ) def _generate_timeseries(self, output_format=None): - r'''Time series of water bridges. + r"""Time series of water bridges. The output is generated per frame as is explained in :ref:`wb_Analysis_Timeseries`. The format of output can be changed via @@ -1529,7 +1770,7 @@ def _generate_timeseries(self, output_format=None): (resname string, resid, name_string). - ''' + """ output_format = output_format or self.output_format def analysis(current, output, *args, **kwargs): @@ -1538,40 +1779,47 @@ def analysis(current, output, *args, **kwargs): timeseries = [] for frame in self.results.network: new_frame = [] - self._traverse_water_network(frame, new_frame, - analysis_func=analysis, - output=new_frame, - link_func=self._compact_link) - timeseries.append([ - self._expand_timeseries(entry, output_format) - for entry in new_frame]) + self._traverse_water_network( + frame, + new_frame, + analysis_func=analysis, + output=new_frame, + link_func=self._compact_link, + ) + timeseries.append( + [ + self._expand_timeseries(entry, output_format) + for entry in new_frame + ] + ) return timeseries - def set_network(self, network): - wmsg = ("The `set_network` method was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.network` instead") + wmsg = ( + "The `set_network` method was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.network` instead" + ) warnings.warn(wmsg, DeprecationWarning) self.results.network = network @classmethod def _full_link(self, output, node): - ''' + """ A function used in _traverse_water_network to add the new hydrogen bond to the existing bonds. :param output: The existing hydrogen bonds from selection 1 :param node: The new hydrogen bond :return: The hydrogen bonds from selection 1 with the new hydrogen bond added - ''' + """ result = output[:] result.append(node) return result @classmethod def _compact_link(self, output, node): - ''' + """ A function used in _traverse_water_network to add the new hydrogen bond to the existing bonds. In this form no new list is created and thus, one bridge will only appear once. @@ -1579,24 +1827,34 @@ def _compact_link(self, output, node): :param node: The new hydrogen bond :return: The hydrogen bonds from selection 1 with the new hydrogen bond added - ''' + """ output.append(node) return output def _count_by_type_analysis(self, current, output, *args, **kwargs): - ''' + """ Generates the key for count_by_type analysis. :return: - ''' + """ - s1_index, to_index, s1, to_residue, dist, angle = \ + s1_index, to_index, s1, to_residue, dist, angle = ( self._expand_timeseries(current[0]) + ) s1_resname, s1_resid, s1_name = s1 - from_index, s2_index, from_residue, s2, dist, angle = \ + from_index, s2_index, from_residue, s2, dist, angle = ( self._expand_timeseries(current[-1]) + ) s2_resname, s2_resid, s2_name = s2 - key = (s1_index, s2_index, - s1_resname, s1_resid, s1_name, s2_resname, s2_resid, s2_name) + key = ( + s1_index, + s2_index, + s1_resname, + s1_resid, + s1_name, + s2_resname, + s2_resid, + s2_name, + ) output[key] += 1 def count_by_type(self, analysis_func=None, **kwargs): @@ -1625,41 +1883,57 @@ def count_by_type(self, analysis_func=None, **kwargs): output = None if analysis_func is None: analysis_func = self._count_by_type_analysis - output = 'combined' + output = "combined" if self.results.network: length = len(self.results.network) result_dict = defaultdict(int) for frame in self.results.network: frame_dict = defaultdict(int) - self._traverse_water_network(frame, [], - analysis_func=analysis_func, - output=frame_dict, - link_func=self._full_link, - **kwargs) + self._traverse_water_network( + frame, + [], + analysis_func=analysis_func, + output=frame_dict, + link_func=self._full_link, + **kwargs, + ) for key, value in frame_dict.items(): result_dict[key] += frame_dict[key] - if output == 'combined': + if output == "combined": result = [[i for i in key] for key in result_dict] - [result[i].append(result_dict[key]/length) - for i, key in enumerate(result_dict)] + [ + result[i].append(result_dict[key] / length) + for i, key in enumerate(result_dict) + ] else: - result = [(key, - result_dict[key]/length) for key in result_dict] + result = [ + (key, result_dict[key] / length) for key in result_dict + ] return result else: return None def _count_by_time_analysis(self, current, output, *args, **kwargs): - s1_index, to_index, s1, to_residue, dist, angle = \ + s1_index, to_index, s1, to_residue, dist, angle = ( self._expand_timeseries(current[0]) + ) s1_resname, s1_resid, s1_name = s1 - from_index, s2_index, from_residue, s2, dist, angle = \ + from_index, s2_index, from_residue, s2, dist, angle = ( self._expand_timeseries(current[-1]) + ) s2_resname, s2_resid, s2_name = s2 - key = (s1_index, s2_index, - s1_resname, s1_resid, s1_name, s2_resname, s2_resid, s2_name) + key = ( + s1_index, + s2_index, + s1_resname, + s1_resid, + s1_name, + s2_resname, + s2_resid, + s2_name, + ) output[key] += 1 def count_by_time(self, analysis_func=None, **kwargs): @@ -1681,27 +1955,41 @@ def count_by_time(self, analysis_func=None, **kwargs): result = [] for time, frame in zip(self.timesteps, self.results.network): result_dict = defaultdict(int) - self._traverse_water_network(frame, [], - analysis_func=analysis_func, - output=result_dict, - link_func=self._full_link, - **kwargs) - result.append((time, - sum([result_dict[key] for key in result_dict]))) + self._traverse_water_network( + frame, + [], + analysis_func=analysis_func, + output=result_dict, + link_func=self._full_link, + **kwargs, + ) + result.append( + (time, sum([result_dict[key] for key in result_dict])) + ) return result else: return None def _timesteps_by_type_analysis(self, current, output, *args, **kwargs): - s1_index, to_index, s1, to_residue, dist, angle = \ + s1_index, to_index, s1, to_residue, dist, angle = ( self._expand_timeseries(current[0]) + ) s1_resname, s1_resid, s1_name = s1 - from_index, s2_index, from_residue, s2, dist, angle = \ + from_index, s2_index, from_residue, s2, dist, angle = ( self._expand_timeseries(current[-1]) + ) s2_resname, s2_resid, s2_name = s2 - key = (s1_index, s2_index, s1_resname, s1_resid, s1_name, s2_resname, - s2_resid, s2_name) - output[key].append(kwargs.pop('time')) + key = ( + s1_index, + s2_index, + s1_resname, + s1_resid, + s1_name, + s2_resname, + s2_resid, + s2_name, + ) + output[key].append(kwargs.pop("time")) def timesteps_by_type(self, analysis_func=None, **kwargs): """Frames during which each water bridges existed, sorted by each water @@ -1724,7 +2012,7 @@ def timesteps_by_type(self, analysis_func=None, **kwargs): output = None if analysis_func is None: analysis_func = self._timesteps_by_type_analysis - output = 'combined' + output = "combined" if self.results.network: result = defaultdict(list) @@ -1733,16 +2021,20 @@ def timesteps_by_type(self, analysis_func=None, **kwargs): else: timesteps = self.timesteps for time, frame in zip(timesteps, self.results.network): - self._traverse_water_network(frame, [], - analysis_func=analysis_func, - output=result, - link_func=self._full_link, - time=time, **kwargs) + self._traverse_water_network( + frame, + [], + analysis_func=analysis_func, + output=result, + link_func=self._full_link, + time=time, + **kwargs, + ) result_list = [] for key, time_list in result.items(): for time in time_list: - if output == 'combined': + if output == "combined": key = list(key) key.append(time) result_list.append(key) @@ -1783,8 +2075,10 @@ def generate_table(self, output_format=None): logger.warning(msg) return None - if self.results.timeseries is not None \ - and output_format == self.output_format: + if ( + self.results.timeseries is not None + and output_format == self.output_format + ): timeseries = self.results.timeseries else: # Recompute timeseries with correct output format @@ -1792,24 +2086,34 @@ def generate_table(self, output_format=None): num_records = np.sum([len(hframe) for hframe in timeseries]) # build empty output table - if output_format == 'sele1_sele2': + if output_format == "sele1_sele2": dtype = [ ("time", float), - ("sele1_index", int), ("sele2_index", int), - ("sele1_resnm", "|U4"), ("sele1_resid", int), + ("sele1_index", int), + ("sele2_index", int), + ("sele1_resnm", "|U4"), + ("sele1_resid", int), ("sele1_atom", "|U4"), - ("sele2_resnm", "|U4"), ("sele2_resid", int), + ("sele2_resnm", "|U4"), + ("sele2_resid", int), ("sele2_atom", "|U4"), - ("distance", float), ("angle", float)] - elif output_format == 'donor_acceptor': + ("distance", float), + ("angle", float), + ] + elif output_format == "donor_acceptor": dtype = [ ("time", float), - ("donor_index", int), ("acceptor_index", int), - ("donor_resnm", "|U4"), ("donor_resid", int), + ("donor_index", int), + ("acceptor_index", int), + ("donor_resnm", "|U4"), + ("donor_resid", int), ("donor_atom", "|U4"), - ("acceptor_resnm", "|U4"), ("acceptor_resid", int), + ("acceptor_resnm", "|U4"), + ("acceptor_resid", int), ("acceptor_atom", "|U4"), - ("distance", float), ("angle", float)] + ("distance", float), + ("angle", float), + ] # according to Lukas' notes below, using a recarray at this stage is # ineffective and speedups of ~x10 can be achieved by filling a @@ -1817,18 +2121,30 @@ def generate_table(self, output_format=None): out = np.empty((num_records,), dtype=dtype) cursor = 0 # current row for t, hframe in zip(self.timesteps, timeseries): - for (donor_index, acceptor_index, donor, - acceptor, distance, angle) in hframe: + for ( + donor_index, + acceptor_index, + donor, + acceptor, + distance, + angle, + ) in hframe: # donor|acceptor = (resname, resid, atomid) - out[cursor] = (t, donor_index, acceptor_index) + \ - donor + acceptor + (distance, angle) + out[cursor] = ( + (t, donor_index, acceptor_index) + + donor + + acceptor + + (distance, angle) + ) cursor += 1 - assert cursor == num_records, \ - "Internal Error: Not all wb records stored" + assert ( + cursor == num_records + ), "Internal Error: Not all wb records stored" table = out.view(np.rec.recarray) logger.debug( "WBridge: Stored results as table with %(num_records)d entries.", - vars()) + vars(), + ) self.table = table return table @@ -1838,16 +2154,20 @@ def _conclude(self): @property def network(self): - wmsg = ("The `network` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.network` instead") + wmsg = ( + "The `network` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.network` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.network @property def timeseries(self): - wmsg = ("The `timeseries` attribute was deprecated in MDAnalysis " - "2.0.0 and will be removed in MDAnalysis 3.0.0. Please use " - "`results.timeseries` instead") + wmsg = ( + "The `timeseries` attribute was deprecated in MDAnalysis " + "2.0.0 and will be removed in MDAnalysis 3.0.0. Please use " + "`results.timeseries` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.timeseries diff --git a/package/MDAnalysis/analysis/leaflet.py b/package/MDAnalysis/analysis/leaflet.py index 5ea0d362a90..9ba5de87e7f 100644 --- a/package/MDAnalysis/analysis/leaflet.py +++ b/package/MDAnalysis/analysis/leaflet.py @@ -87,10 +87,12 @@ HAS_NX = True -due.cite(Doi("10.1002/jcc.21787"), - description="LeafletFinder algorithm", - path="MDAnalysis.analysis.leaflet", - cite_module=True) +due.cite( + Doi("10.1002/jcc.21787"), + description="LeafletFinder algorithm", + path="MDAnalysis.analysis.leaflet", + cite_module=True, +) del Doi @@ -157,9 +159,11 @@ class LeafletFinder(object): def __init__(self, universe, select, cutoff=15.0, pbc=False, sparse=None): # Raise an error if networkx is not installed if not HAS_NX: - errmsg = ("The LeafletFinder class requires an installation of " - "networkx. Please install networkx " - "https://networkx.org/documentation/stable/install.html") + errmsg = ( + "The LeafletFinder class requires an installation of " + "networkx. Please install networkx " + "https://networkx.org/documentation/stable/install.html" + ) raise ImportError(errmsg) self.universe = universe @@ -194,31 +198,47 @@ def _get_graph(self): if self.sparse is False: # only try distance array try: - adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="numpy", box=box) - except ValueError: # pragma: no cover - warnings.warn('N x N matrix too big, use sparse=True or sparse=None', category=UserWarning, - stacklevel=2) + adj = distances.contact_matrix( + coord, cutoff=self.cutoff, returntype="numpy", box=box + ) + except ValueError: # pragma: no cover + warnings.warn( + "N x N matrix too big, use sparse=True or sparse=None", + category=UserWarning, + stacklevel=2, + ) raise elif self.sparse is True: # only try sparse - adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="sparse", box=box) + adj = distances.contact_matrix( + coord, cutoff=self.cutoff, returntype="sparse", box=box + ) else: # use distance_array and fall back to sparse matrix try: # this works for small-ish systems and depends on system memory - adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="numpy", box=box) - except ValueError: # pragma: no cover + adj = distances.contact_matrix( + coord, cutoff=self.cutoff, returntype="numpy", box=box + ) + except ValueError: # pragma: no cover # but use a sparse matrix method for larger systems for memory reasons warnings.warn( - 'N x N matrix too big - switching to sparse matrix method (works fine, but is currently rather ' - 'slow)', - category=UserWarning, stacklevel=2) - adj = distances.contact_matrix(coord, cutoff=self.cutoff, returntype="sparse", box=box) + "N x N matrix too big - switching to sparse matrix method (works fine, but is currently rather " + "slow)", + category=UserWarning, + stacklevel=2, + ) + adj = distances.contact_matrix( + coord, cutoff=self.cutoff, returntype="sparse", box=box + ) return NX.Graph(adj) def _get_components(self): """Return connected components (as sorted numpy arrays), sorted by size.""" - return [np.sort(list(component)) for component in NX.connected_components(self.graph)] + return [ + np.sort(list(component)) + for component in NX.connected_components(self.graph) + ] def update(self, cutoff=None): """Update components, possibly with a different *cutoff*""" @@ -228,7 +248,12 @@ def update(self, cutoff=None): def sizes(self): """Dict of component index with size of component.""" - return dict(((idx, len(component)) for idx, component in enumerate(self.components))) + return dict( + ( + (idx, len(component)) + for idx, component in enumerate(self.components) + ) + ) def groups(self, component_index=None): """Return a :class:`MDAnalysis.core.groups.AtomGroup` for *component_index*. @@ -265,23 +290,37 @@ def write_selection(self, filename, **kwargs): See :class:`MDAnalysis.selections.base.SelectionWriter` for all options. """ - sw = selections.get_writer(filename, kwargs.pop('format', None)) - with sw(filename, mode=kwargs.pop('mode', 'w'), - preamble="leaflets based on select={selectionstring!r} cutoff={cutoff:f}\n".format( - **vars(self)), - **kwargs) as writer: + sw = selections.get_writer(filename, kwargs.pop("format", None)) + with sw( + filename, + mode=kwargs.pop("mode", "w"), + preamble="leaflets based on select={selectionstring!r} cutoff={cutoff:f}\n".format( + **vars(self) + ), + **kwargs, + ) as writer: for i, ag in enumerate(self.groups_iter()): name = "leaflet_{0:d}".format((i + 1)) writer.write(ag, name=name) def __repr__(self): return "".format( - self.selectionstring, self.cutoff, self.selection.n_atoms, - len(self.components)) - - -def optimize_cutoff(universe, select, dmin=10.0, dmax=20.0, step=0.5, - max_imbalance=0.2, **kwargs): + self.selectionstring, + self.cutoff, + self.selection.n_atoms, + len(self.components), + ) + + +def optimize_cutoff( + universe, + select, + dmin=10.0, + dmax=20.0, + step=0.5, + max_imbalance=0.2, + **kwargs, +): r"""Find cutoff that minimizes number of disconnected groups. Applies heuristics to find best groups: @@ -325,7 +364,7 @@ def optimize_cutoff(universe, select, dmin=10.0, dmax=20.0, step=0.5, .. versionchanged:: 1.0.0 Changed `selection` keyword to `select` """ - kwargs.pop('cutoff', None) # not used, so we filter it + kwargs.pop("cutoff", None) # not used, so we filter it _sizes = [] for cutoff in np.arange(dmin, dmax, step): LF = LeafletFinder(universe, select, cutoff=cutoff, **kwargs) diff --git a/package/MDAnalysis/analysis/legacy/x3dna.py b/package/MDAnalysis/analysis/legacy/x3dna.py index 46a2f5a8f60..2365c181133 100644 --- a/package/MDAnalysis/analysis/legacy/x3dna.py +++ b/package/MDAnalysis/analysis/legacy/x3dna.py @@ -117,31 +117,35 @@ .. autoexception:: ApplicationError """ +import errno import glob +import logging import os -import errno -import shutil -import warnings import os.path +import shutil import subprocess import tempfile import textwrap +import warnings from collections import OrderedDict -import numpy as np import matplotlib.pyplot as plt +import numpy as np from MDAnalysis import ApplicationError -from MDAnalysis.lib.util import which, realpath, asiterable, deprecate - -import logging +from MDAnalysis.lib.util import asiterable, deprecate, realpath, which logger = logging.getLogger("MDAnalysis.analysis.x3dna") -@deprecate(release="2.7.0", remove="3.0.0", - message=("X3DNA module is deprecated and will be removed in" - "MDAnalysis 3.0.0, see #3788")) +@deprecate( + release="2.7.0", + remove="3.0.0", + message=( + "X3DNA module is deprecated and will be removed in" + "MDAnalysis 3.0.0, see #3788" + ), +) def mean_std_from_x3dnaPickle(profile): """Get mean and standard deviation of helicoidal parameters from a saved `profile`. @@ -168,10 +172,15 @@ def mean_std_from_x3dnaPickle(profile): .. deprecated:: 2.7.0 X3DNA will be removed in 3.0.0. """ - warnings.warn("X3DNA module is deprecated and will be removed in MDAnalysis 3.0, see #3788", category=DeprecationWarning) + warnings.warn( + "X3DNA module is deprecated and will be removed in MDAnalysis 3.0, see #3788", + category=DeprecationWarning, + ) if profile.x3dna_param is False: + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = [], [], [], [], [], [], [], [], [], [], [], [] + # fmt: on for i in range(len(profile)): bp_shear.append(profile.values()[i].Shear) bp_stretch.append(profile.values()[i].Stretch) @@ -185,44 +194,94 @@ def mean_std_from_x3dnaPickle(profile): bp_tilt.append(profile.values()[i].Tilt) bp_roll.append(profile.values()[i].Roll) bp_twist.append(profile.values()[i].Twist) + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = np.array(bp_shear), np.array(bp_stretch), np.array(bp_stagger), np.array(bp_rise),\ np.array(bp_shift), np.array(bp_slide), np.array(bp_buckle), np.array(bp_prop), \ np.array(bp_open), np.array(bp_tilt), np.array(bp_roll), np.array(bp_twist) + # fmt: on na_avg, na_std = [], [] for j in range(len(bp_shear[0])): - na_avg.append([ - np.mean(bp_shear[:, j]), np.mean(bp_stretch[:, j]), np.mean(bp_stagger[:, j]), - np.mean(bp_buckle[:, j]), np.mean(bp_prop[:, j]), np.mean(bp_open[:, j]), - np.mean(bp_shift[:, j]), np.mean(bp_slide[:, j]), np.mean(bp_rise[:, j]), - np.mean(bp_tilt[:, j]), np.mean(bp_roll[:, j]), np.mean(bp_twist[:, j])]) - na_std.append([ - np.std(bp_shear[:, j]), np.std(bp_stretch[:, j]), np.std(bp_stagger[:, j]), - np.std(bp_buckle[:, j]), np.std(bp_prop[:, j]), np.std(bp_open[:, j]), - np.std(bp_shift[:, j]), np.std(bp_slide[:, j]), np.std(bp_rise[:, j]), - np.std(bp_tilt[:, j]), np.std(bp_roll[:, j]), np.std(bp_twist[:, j])]) + na_avg.append( + [ + np.mean(bp_shear[:, j]), + np.mean(bp_stretch[:, j]), + np.mean(bp_stagger[:, j]), + np.mean(bp_buckle[:, j]), + np.mean(bp_prop[:, j]), + np.mean(bp_open[:, j]), + np.mean(bp_shift[:, j]), + np.mean(bp_slide[:, j]), + np.mean(bp_rise[:, j]), + np.mean(bp_tilt[:, j]), + np.mean(bp_roll[:, j]), + np.mean(bp_twist[:, j]), + ] + ) + na_std.append( + [ + np.std(bp_shear[:, j]), + np.std(bp_stretch[:, j]), + np.std(bp_stagger[:, j]), + np.std(bp_buckle[:, j]), + np.std(bp_prop[:, j]), + np.std(bp_open[:, j]), + np.std(bp_shift[:, j]), + np.std(bp_slide[:, j]), + np.std(bp_rise[:, j]), + np.std(bp_tilt[:, j]), + np.std(bp_roll[:, j]), + np.std(bp_twist[:, j]), + ] + ) else: + # fmt: off bp_rise, bp_shift, bp_slide, bp_tilt, bp_roll, bp_twist = [], [], [], [], [], [], [], [], [], [], [], [] + # fmt: on for i in range(len(profile)): - #print i + # print i bp_rise.append(profile.values()[i].Rise) bp_shift.append(profile.values()[i].Shift) bp_slide.append(profile.values()[i].Slide) bp_tilt.append(profile.values()[i].Tilt) bp_roll.append(profile.values()[i].Roll) bp_twist.append(profile.values()[i].Twist) - bp_rise, bp_shift, bp_slide, bp_tilt, bp_roll, bp_twist = np.array(bp_shear),np.array(bp_stretch),\ - np.array(bp_stagger), np.array(bp_rise), np.array(bp_shift), np.array(bp_slide),\ - np.array(bp_buckle), np.array(bp_prop), np.array(bp_open), np.array(bp_tilt),\ - np.array(bp_roll), np.array(bp_twist) + bp_rise, bp_shift, bp_slide, bp_tilt, bp_roll, bp_twist = ( + np.array(bp_shear), + np.array(bp_stretch), + np.array(bp_stagger), + np.array(bp_rise), + np.array(bp_shift), + np.array(bp_slide), + np.array(bp_buckle), + np.array(bp_prop), + np.array(bp_open), + np.array(bp_tilt), + np.array(bp_roll), + np.array(bp_twist), + ) na_avg, na_std = [], [] for j in range(len(bp_shift[0])): - na_avg.append([ - np.mean(bp_shift[:, j]), np.mean(bp_slide[:, j]), np.mean(bp_rise[:, j]), - np.mean(bp_tilt[:, j]), np.mean(bp_roll[:, j]), np.mean(bp_twist[:, j])]) - na_std.append([ - np.std(bp_shift[:, j]), np.std(bp_slide[:, j]), np.std(bp_rise[:, j]), - np.std(bp_tilt[:, j]), np.std(bp_roll[:, j]), np.std(bp_twist[:, j])]) + na_avg.append( + [ + np.mean(bp_shift[:, j]), + np.mean(bp_slide[:, j]), + np.mean(bp_rise[:, j]), + np.mean(bp_tilt[:, j]), + np.mean(bp_roll[:, j]), + np.mean(bp_twist[:, j]), + ] + ) + na_std.append( + [ + np.std(bp_shift[:, j]), + np.std(bp_slide[:, j]), + np.std(bp_rise[:, j]), + np.std(bp_tilt[:, j]), + np.std(bp_roll[:, j]), + np.std(bp_twist[:, j]), + ] + ) na_avg, na_std = np.array(na_avg), np.array(na_std) return na_avg, na_std @@ -271,7 +330,9 @@ def save(self, filename="x3dna.pickle"): """ import cPickle - cPickle.dump(self.profiles, open(filename, "wb"), cPickle.HIGHEST_PROTOCOL) + cPickle.dump( + self.profiles, open(filename, "wb"), cPickle.HIGHEST_PROTOCOL + ) def mean_std(self): """Returns the mean and standard deviation of base parameters. @@ -285,8 +346,10 @@ def mean_std(self): roll, twist]``. """ + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = [], [], [], [], [], [], [], [], [], [], [], [] + # fmt: on for i in range(len(self.profiles)): bp_shear.append(self.profiles.values()[i].Shear) bp_stretch.append(self.profiles.values()[i].Stretch) @@ -300,22 +363,46 @@ def mean_std(self): bp_tilt.append(self.profiles.values()[i].Tilt) bp_roll.append(self.profiles.values()[i].Roll) bp_twist.append(self.profiles.values()[i].Twist) + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = np.array(bp_shear), np.array(bp_stretch), np.array(bp_stagger), np.array(bp_rise),\ np.array(bp_shift), np.array(bp_slide), np.array(bp_buckle), np.array(bp_prop),\ np.array(bp_open), np.array(bp_tilt), np.array(bp_roll), np.array(bp_twist) + # fmt: on na_avg, na_std = [], [] for j in range(len(bp_shear[0])): - na_avg.append([ - np.mean(bp_shear[:, j]), np.mean(bp_stretch[:, j]), np.mean(bp_stagger[:, j]), - np.mean(bp_buckle[:, j]), np.mean(bp_prop[:, j]), np.mean(bp_open[:, j]), - np.mean(bp_shift[:, j]), np.mean(bp_slide[:, j]), np.mean(bp_rise[:, j]), - np.mean(bp_tilt[:, j]), np.mean(bp_roll[:, j]), np.mean(bp_twist[:, j])]) - na_std.append([ - np.std(bp_shear[:, j]), np.std(bp_stretch[:, j]), np.std(bp_stagger[:, j]), - np.std(bp_buckle[:, j]), np.std(bp_prop[:, j]), np.std(bp_open[:, j]), - np.std(bp_shift[:, j]), np.std(bp_slide[:, j]), np.std(bp_rise[:, j]), - np.std(bp_tilt[:, j]), np.std(bp_roll[:, j]), np.std(bp_twist[:, j])]) + na_avg.append( + [ + np.mean(bp_shear[:, j]), + np.mean(bp_stretch[:, j]), + np.mean(bp_stagger[:, j]), + np.mean(bp_buckle[:, j]), + np.mean(bp_prop[:, j]), + np.mean(bp_open[:, j]), + np.mean(bp_shift[:, j]), + np.mean(bp_slide[:, j]), + np.mean(bp_rise[:, j]), + np.mean(bp_tilt[:, j]), + np.mean(bp_roll[:, j]), + np.mean(bp_twist[:, j]), + ] + ) + na_std.append( + [ + np.std(bp_shear[:, j]), + np.std(bp_stretch[:, j]), + np.std(bp_stagger[:, j]), + np.std(bp_buckle[:, j]), + np.std(bp_prop[:, j]), + np.std(bp_open[:, j]), + np.std(bp_shift[:, j]), + np.std(bp_slide[:, j]), + np.std(bp_rise[:, j]), + np.std(bp_tilt[:, j]), + np.std(bp_roll[:, j]), + np.std(bp_twist[:, j]), + ] + ) na_avg, na_std = np.array(na_avg), np.array(na_std) return na_avg, na_std @@ -330,8 +417,10 @@ def mean(self): shift, slide, rise, tilt, roll, twist]``. """ + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = [], [], [], [], [], [], [], [], [], [], [], [] + # fmt: on for i in range(len(self.profiles)): bp_shear.append(self.profiles.values()[i].Shear) bp_stretch.append(self.profiles.values()[i].Stretch) @@ -345,18 +434,31 @@ def mean(self): bp_tilt.append(self.profiles.values()[i].Tilt) bp_roll.append(self.profiles.values()[i].Roll) bp_twist.append(self.profiles.values()[i].Twist) + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = np.array(bp_shear), np.array(bp_stretch), np.array(bp_stagger), np.array(bp_rise),\ np.array(bp_shift), np.array(bp_slide), np.array(bp_buckle), np.array(bp_prop),\ np.array(bp_open), np.array(bp_tilt), np.array(bp_roll), np.array(bp_twist) + # fmt: on na_avg = [] for j in range(len(bp_shear[0])): - na_avg.append([ - np.mean(bp_shear[:, j]), np.mean(bp_stretch[:, j]), np.mean(bp_stagger[:, j]), - np.mean(bp_buckle[:j]), np.mean(bp_prop[:, j]), np.mean(bp_open[:, j]), - np.mean(bp_shift[:, j]), np.mean(bp_slide[:, j]), np.mean(bp_rise[:, j]), - np.mean(bp_tilt[:, j]), np.mean(bp_roll[:, j]), np.mean(bp_twist[:, j])]) + na_avg.append( + [ + np.mean(bp_shear[:, j]), + np.mean(bp_stretch[:, j]), + np.mean(bp_stagger[:, j]), + np.mean(bp_buckle[:j]), + np.mean(bp_prop[:, j]), + np.mean(bp_open[:, j]), + np.mean(bp_shift[:, j]), + np.mean(bp_slide[:, j]), + np.mean(bp_rise[:, j]), + np.mean(bp_tilt[:, j]), + np.mean(bp_roll[:, j]), + np.mean(bp_twist[:, j]), + ] + ) na_avg = np.array(na_avg) return na_avg @@ -371,8 +473,10 @@ def std(self): propeller, opening, shift, slide, rise, tilt, roll, twist]``. """ + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = [], [], [], [], [], [], [], [], [], [], [], [] + # fmt: on for i in range(len(self.profiles)): bp_shear.append(self.profiles.values()[i].Shear) bp_stretch.append(self.profiles.values()[i].Stretch) @@ -386,18 +490,31 @@ def std(self): bp_tilt.append(self.profiles.values()[i].Tilt) bp_roll.append(self.profiles.values()[i].Roll) bp_twist.append(self.profiles.values()[i].Twist) + # fmt: off bp_shear, bp_stretch, bp_stagger, bp_rise, bp_shift, bp_slide, bp_buckle, bp_prop, bp_open, bp_tilt, bp_roll,\ bp_twist = np.array(bp_shear), np.array(bp_stretch), np.array(bp_stagger), np.array(bp_rise),\ np.array(bp_shift), np.array(bp_slide), np.array(bp_buckle), np.array(bp_prop),\ np.array(bp_open), np.array(bp_tilt), np.array(bp_roll), np.array(bp_twist) + # fmt: on na_std = [] for j in range(len(bp_shear[0])): - na_std.append([ - np.std(bp_shear[:, j]), np.std(bp_stretch[:, j]), np.std(bp_stagger[:, j]), - np.std(bp_buckle[:j]), np.std(bp_prop[:, j]), np.std(bp_open[:, j]), np.std(bp_shift[:, j]), - np.std(bp_slide[:, j]), np.std(bp_rise[:, j]), np.std(bp_tilt[:, j]), np.std(bp_roll[:, j]), - np.std(bp_twist[:, j])]) + na_std.append( + [ + np.std(bp_shear[:, j]), + np.std(bp_stretch[:, j]), + np.std(bp_stagger[:, j]), + np.std(bp_buckle[:j]), + np.std(bp_prop[:, j]), + np.std(bp_open[:, j]), + np.std(bp_shift[:, j]), + np.std(bp_slide[:, j]), + np.std(bp_rise[:, j]), + np.std(bp_tilt[:, j]), + np.std(bp_roll[:, j]), + np.std(bp_twist[:, j]), + ] + ) na_std = np.array(na_std) return na_std @@ -417,13 +534,20 @@ def plot(self, **kwargs): na_avg, na_std = self.mean_std() for k in range(len(na_avg[0])): - ax = kwargs.pop('ax', plt.subplot(111)) + ax = kwargs.pop("ax", plt.subplot(111)) x = list(range(1, len(na_avg[:, k]) + 1)) - ax.errorbar(x, na_avg[:, k], yerr=na_std[:, k], fmt='-o') + ax.errorbar(x, na_avg[:, k], yerr=na_std[:, k], fmt="-o") ax.set_xlim(0, len(na_avg[:, k]) + 1) ax.set_xlabel(r"Nucleic Acid Number") param = self.profiles.values()[0].dtype.names[k] - if param in ["Shear", "Stretch", "Stagger", "Rise", "Shift", "Slide"]: + if param in [ + "Shear", + "Stretch", + "Stagger", + "Rise", + "Shift", + "Slide", + ]: ax.set_ylabel(r"{!s} ($\AA$)".format(param)) else: ax.set_ylabel("{0!s} (deg)".format((param))) @@ -487,9 +611,14 @@ class X3DNA(BaseX3DNA): .. _`X3DNA docs`: http://forum.x3dna.org/ """ - @deprecate(release="2.7.0", remove="3.0.0", - message=("X3DNA module is deprecated and will be removed in" - "MDAnalysis 3.0.0, see #3788")) + @deprecate( + release="2.7.0", + remove="3.0.0", + message=( + "X3DNA module is deprecated and will be removed in" + "MDAnalysis 3.0.0, see #3788" + ), + ) def __init__(self, filename, **kwargs): """Set up parameters to run X3DNA_ on PDB *filename*. @@ -521,8 +650,17 @@ def __init__(self, filename, **kwargs): """ # list of temporary files, to be cleaned up on __del__ self.tempfiles = [ - "auxiliary.par", "bestpairs.pdb", "bp_order.dat", "bp_helical.par", "cf_7methods.par", - "col_chains.scr", "col_helices.scr", "hel_regions.pdb", "ref_frames.dat", "hstacking.pdb", "stacking.pdb" + "auxiliary.par", + "bestpairs.pdb", + "bp_order.dat", + "bp_helical.par", + "cf_7methods.par", + "col_chains.scr", + "col_helices.scr", + "hel_regions.pdb", + "ref_frames.dat", + "hstacking.pdb", + "stacking.pdb", ] self.tempdirs = [] self.filename = filename @@ -531,28 +669,38 @@ def __init__(self, filename, **kwargs): # guess executables self.exe = {} - x3dna_exe_name = kwargs.pop('executable', 'xdna_ensemble') - self.x3dna_param = kwargs.pop('x3dna_param', True) - self.exe['xdna_ensemble'] = which(x3dna_exe_name) - if self.exe['xdna_ensemble'] is None: - errmsg = "X3DNA binary {x3dna_exe_name!r} not found.".format(**vars()) + x3dna_exe_name = kwargs.pop("executable", "xdna_ensemble") + self.x3dna_param = kwargs.pop("x3dna_param", True) + self.exe["xdna_ensemble"] = which(x3dna_exe_name) + if self.exe["xdna_ensemble"] is None: + errmsg = "X3DNA binary {x3dna_exe_name!r} not found.".format( + **vars() + ) logger.fatal(errmsg) - logger.fatal("%(x3dna_exe_name)r must be on the PATH or provided as keyword argument 'executable'.", - vars()) + logger.fatal( + "%(x3dna_exe_name)r must be on the PATH or provided as keyword argument 'executable'.", + vars(), + ) raise OSError(errno.ENOENT, errmsg) - x3dnapath = os.path.dirname(self.exe['xdna_ensemble']) + x3dnapath = os.path.dirname(self.exe["xdna_ensemble"]) self.logfile = kwargs.pop("logfile", "bp_step.par") if self.x3dna_param is False: - self.template = textwrap.dedent("""x3dna_ensemble analyze -b 355d.bps --one %(filename)r """) + self.template = textwrap.dedent( + """x3dna_ensemble analyze -b 355d.bps --one %(filename)r """ + ) else: - self.template = textwrap.dedent("""find_pair -s %(filename)r stdout |analyze stdin """) + self.template = textwrap.dedent( + """find_pair -s %(filename)r stdout |analyze stdin """ + ) # sanity checks for program, path in self.exe.items(): if path is None or which(path) is None: - logger.error("Executable %(program)r not found, should have been %(path)r.", - vars()) + logger.error( + "Executable %(program)r not found, should have been %(path)r.", + vars(), + ) # results self.profiles = OrderedDict() @@ -563,7 +711,7 @@ def run(self, **kwargs): x3dnaargs = vars(self).copy() x3dnaargs.update(kwargs) - x3dna_param = kwargs.pop('x3dna_param', self.x3dna_param) + x3dna_param = kwargs.pop("x3dna_param", self.x3dna_param) inp = self.template % x3dnaargs if inpname: @@ -571,18 +719,24 @@ def run(self, **kwargs): f.write(inp) logger.debug("Wrote X3DNA input file %r for inspection", inpname) - logger.info("Starting X3DNA on %(filename)r (trajectory: %(dcd)r)", x3dnaargs) - logger.debug("%s", self.exe['xdna_ensemble']) + logger.info( + "Starting X3DNA on %(filename)r (trajectory: %(dcd)r)", x3dnaargs + ) + logger.debug("%s", self.exe["xdna_ensemble"]) with open(outname, "w") as output: x3dna = subprocess.call([inp], shell=True) with open(outname, "r") as output: # X3DNA is not very good at setting returncodes so check ourselves for line in output: - if line.strip().startswith(('*** ERROR ***', 'ERROR')): + if line.strip().startswith(("*** ERROR ***", "ERROR")): x3dna.returncode = 255 break if x3dna.bit_length != 0: - logger.fatal("X3DNA Failure (%d). Check output %r", x3dna.bit_length, outname) + logger.fatal( + "X3DNA Failure (%d). Check output %r", + x3dna.bit_length, + outname, + ) logger.info("X3DNA finished: output file %(outname)r", vars()) def collect(self, **kwargs): @@ -607,10 +761,10 @@ def collect(self, **kwargs): """ # Shear Stretch Stagger Buckle Prop-Tw Opening Shift Slide Rise Tilt Roll Twist - #0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789. + # 0123456789.0123456789.0123456789.0123456789.0123456789.0123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789. # 11 22 33 44 - #T-A -0.033 -0.176 0.158 -12.177 -8.979 1.440 0.000 0.000 0.000 0.000 0.000 0.000 - #C-G -0.529 0.122 -0.002 -7.983 -10.083 -0.091 -0.911 1.375 3.213 -0.766 -4.065 41.492 + # T-A -0.033 -0.176 0.158 -12.177 -8.979 1.440 0.000 0.000 0.000 0.000 0.000 0.000 + # C-G -0.529 0.122 -0.002 -7.983 -10.083 -0.091 -0.911 1.375 3.213 -0.766 -4.065 41.492 # only parse bp_step.par x3dna_output = kwargs.pop("x3dnaout", self.logfile) @@ -619,20 +773,35 @@ def collect(self, **kwargs): logger.info("Collecting X3DNA profiles for run with id %s", run) length = 1 # length of trajectory --- is this really needed?? No... just for info - if '*' in self.filename: + if "*" in self.filename: import glob filenames = glob.glob(self.filename) length = len(filenames) if length == 0: - logger.error("Glob pattern %r did not find any files.", self.filename) - raise ValueError("Glob pattern {0!r} did not find any files.".format(self.filename)) - logger.info("Found %d input files based on glob pattern %s", length, self.filename) + logger.error( + "Glob pattern %r did not find any files.", self.filename + ) + raise ValueError( + "Glob pattern {0!r} did not find any files.".format( + self.filename + ) + ) + logger.info( + "Found %d input files based on glob pattern %s", + length, + self.filename, + ) # one recarray for each frame, indexed by frame number self.profiles = OrderedDict() - logger.info("Run %s: Reading %d X3DNA profiles from %r", run, length, x3dna_output) + logger.info( + "Run %s: Reading %d X3DNA profiles from %r", + run, + length, + x3dna_output, + ) x3dna_profile_no = 0 records = [] with open(x3dna_output, "r") as x3dna: @@ -644,22 +813,55 @@ def collect(self, **kwargs): read_data = True logger.debug("Started reading data") fields = line.split() - x3dna_profile_no = int(1) # useless int value code based off hole plugin + x3dna_profile_no = int( + 1 + ) # useless int value code based off hole plugin records = [] continue if read_data: if len(line.strip()) != 0: try: - Sequence, Shear, Stretch, Stagger, Buckle, Propeller, Opening, Shift, Slide, Rise, \ - Tilt, Roll, Twist = line.split() + ( + Sequence, + Shear, + Stretch, + Stagger, + Buckle, + Propeller, + Opening, + Shift, + Slide, + Rise, + Tilt, + Roll, + Twist, + ) = line.split() except: - logger.critical("Run %d: Problem parsing line %r", run, line.strip()) - logger.exception("Check input file %r.", x3dna_output) + logger.critical( + "Run %d: Problem parsing line %r", + run, + line.strip(), + ) + logger.exception( + "Check input file %r.", x3dna_output + ) raise records.append( - [float(Shear), float(Stretch), float(Stagger), float(Buckle), float(Propeller), - float(Opening), float(Shift), float(Slide), float(Rise), float(Tilt), float(Roll), - float(Twist)]) + [ + float(Shear), + float(Stretch), + float(Stagger), + float(Buckle), + float(Propeller), + float(Opening), + float(Shift), + float(Slide), + float(Rise), + float(Tilt), + float(Roll), + float(Twist), + ] + ) continue else: # end of records (empty line) @@ -669,34 +871,67 @@ def collect(self, **kwargs): read_data = True logger.debug("Started reading data") fields = line.split() - x3dna_profile_no = int(1) # useless int value code based off hole plugin + x3dna_profile_no = int( + 1 + ) # useless int value code based off hole plugin records = [] continue if read_data: if len(line.strip()) != 0: try: - Sequence, Shift, Slide, Rise, Tilt, Roll, Twist = line.split() + ( + Sequence, + Shift, + Slide, + Rise, + Tilt, + Roll, + Twist, + ) = line.split() except: - logger.critical("Run %d: Problem parsing line %r", run, line.strip()) - logger.exception("Check input file %r.", x3dna_output) + logger.critical( + "Run %d: Problem parsing line %r", + run, + line.strip(), + ) + logger.exception( + "Check input file %r.", x3dna_output + ) raise records.append( - [float(Shift), float(Slide), float(Rise), float(Tilt), float(Roll), float(Twist)]) + [ + float(Shift), + float(Slide), + float(Rise), + float(Tilt), + float(Roll), + float(Twist), + ] + ) continue else: # end of records (empty line) read_data = False if self.x3dna_param is False: - frame_x3dna_output = np.rec.fromrecords(records, formats="f8,f8,f8,f8,f8,f8,f8,f8,f8,f8,f8,f8", - names="Shear,Stretch,Stagger,Buckle,Propeller,Opening," - "Shift,Slide,Rise,Tilt,Roll,Twist") + frame_x3dna_output = np.rec.fromrecords( + records, + formats="f8,f8,f8,f8,f8,f8,f8,f8,f8,f8,f8,f8", + names="Shear,Stretch,Stagger,Buckle,Propeller,Opening," + "Shift,Slide,Rise,Tilt,Roll,Twist", + ) else: - frame_x3dna_output = np.rec.fromrecords(records, formats="f8,f8,f8,f8,f8,f8", - names="Shift,Slide,Rise,Tilt,Roll,Twist") + frame_x3dna_output = np.rec.fromrecords( + records, + formats="f8,f8,f8,f8,f8,f8", + names="Shift,Slide,Rise,Tilt,Roll,Twist", + ) # store the profile self.profiles[x3dna_profile_no] = frame_x3dna_output - logger.debug("Collected X3DNA profile for frame %d (%d datapoints)", - x3dna_profile_no, len(frame_x3dna_output)) + logger.debug( + "Collected X3DNA profile for frame %d (%d datapoints)", + x3dna_profile_no, + len(frame_x3dna_output), + ) # save a profile for each frame (for debugging and scripted processing) # a tmp folder for each trajectory if outdir is not None: @@ -704,14 +939,29 @@ def collect(self, **kwargs): os.system("rm -f tmp*.out") if not os.path.exists(rundir): os.makedirs(rundir) - frame_x3dna_txt = os.path.join(rundir, "bp_step_{0!s}_{1:04d}.dat.gz".format(run, x3dna_profile_no)) + frame_x3dna_txt = os.path.join( + rundir, + "bp_step_{0!s}_{1:04d}.dat.gz".format( + run, x3dna_profile_no + ), + ) np.savetxt(frame_x3dna_txt, frame_x3dna_output) - logger.debug("Finished with frame %d, saved as %r", x3dna_profile_no, frame_x3dna_txt) + logger.debug( + "Finished with frame %d, saved as %r", + x3dna_profile_no, + frame_x3dna_txt, + ) # if we get here then we haven't found anything interesting if len(self.profiles) == length: - logger.info("Collected X3DNA profiles for %d frames", len(self.profiles)) + logger.info( + "Collected X3DNA profiles for %d frames", len(self.profiles) + ) else: - logger.warning("Missing data: Found %d X3DNA profiles from %d frames.", len(self.profiles), length) + logger.warning( + "Missing data: Found %d X3DNA profiles from %d frames.", + len(self.profiles), + length, + ) def __del__(self): for f in self.tempfiles: @@ -736,9 +986,15 @@ class X3DNAtraj(BaseX3DNA): .. deprecated:: 2.7.0 X3DNA will be removed in 3.0.0. """ - @deprecate(release="2.7.0", remove="3.0.0", - message=("X3DNA module is deprecated and will be removed in" - "MDAnalysis 3.0.0, see #3788")) + + @deprecate( + release="2.7.0", + remove="3.0.0", + message=( + "X3DNA module is deprecated and will be removed in" + "MDAnalysis 3.0.0, see #3788" + ), + ) def __init__(self, universe, **kwargs): """Set up the class. @@ -776,10 +1032,10 @@ def __init__(self, universe, **kwargs): self.universe = universe self.selection = kwargs.pop("selection", "nucleic") - self.x3dna_param = kwargs.pop('x3dna_param', True) - self.start = kwargs.pop('start', None) - self.stop = kwargs.pop('stop', None) - self.step = kwargs.pop('step', None) + self.x3dna_param = kwargs.pop("x3dna_param", True) + self.start = kwargs.pop("start", None) + self.stop = kwargs.pop("stop", None) + self.step = kwargs.pop("step", None) self.x3dna_kwargs = kwargs @@ -792,10 +1048,10 @@ def run(self, **kwargs): analyse part of the trajectory. The defaults are the values provided to the class constructor. """ - start = kwargs.pop('start', self.start) - stop = kwargs.pop('stop', self.stop) - step = kwargs.pop('step', self.step) - x3dna_param = kwargs.pop('x3dna_param', self.x3dna_param) + start = kwargs.pop("start", self.start) + stop = kwargs.pop("stop", self.stop) + step = kwargs.pop("step", self.step) + x3dna_param = kwargs.pop("x3dna_param", self.x3dna_param) x3dna_kw = self.x3dna_kwargs.copy() x3dna_kw.update(kwargs) @@ -818,7 +1074,8 @@ def run(self, **kwargs): pass if len(x3dna_profiles) != 1: err_msg = "Got {0} profiles ({1}) --- should be 1 (time step {2})".format( - len(x3dna_profiles), x3dna_profiles.keys(), ts) + len(x3dna_profiles), x3dna_profiles.keys(), ts + ) logger.error(err_msg) warnings.warn(err_msg) profiles[ts.frame] = x3dna_profiles.values()[0] @@ -826,7 +1083,7 @@ def run(self, **kwargs): def run_x3dna(self, pdbfile, **kwargs): """Run X3DNA on a single PDB file `pdbfile`.""" - kwargs['x3dna_param'] = self.x3dna_param + kwargs["x3dna_param"] = self.x3dna_param H = X3DNA(pdbfile, **kwargs) H.run() H.collect() diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 8970d68d8a0..a0377b47b35 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -44,21 +44,26 @@ class Results(Results): the docstring for LinearDensity for details. The Results class is defined here to implement deprecation warnings for the user.""" - _deprecation_dict = {"pos": "mass_density", - "pos_std": "mass_density_stddev", - "char": "charge_density", - "char_std": "charge_density_stddev"} + _deprecation_dict = { + "pos": "mass_density", + "pos_std": "mass_density_stddev", + "char": "charge_density", + "char_std": "charge_density_stddev", + } def _deprecation_warning(self, key): warnings.warn( f"`{key}` is deprecated and will be removed in version 3.0.0. " f"Please use `{self._deprecation_dict[key]}` instead.", - DeprecationWarning) + DeprecationWarning, + ) def __getitem__(self, key): if key in self._deprecation_dict.keys(): self._deprecation_warning(key) - return super(Results, self).__getitem__(self._deprecation_dict[key]) + return super(Results, self).__getitem__( + self._deprecation_dict[key] + ) return super(Results, self).__getitem__(key) def __getattr__(self, attr): @@ -193,9 +198,10 @@ class LinearDensity(AnalysisBase): and :attr:`results.x.charge_density_stddev` instead. """ - def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): - super(LinearDensity, self).__init__(select.universe.trajectory, - **kwargs) + def __init__(self, select, grouping="atoms", binsize=0.25, **kwargs): + super(LinearDensity, self).__init__( + select.universe.trajectory, **kwargs + ) # allows use of run(parallel=True) self._ags = [select] self._universe = select.universe @@ -222,13 +228,17 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): self.nbins = bins.max() slices_vol = self.volume / bins - self.keys = ['mass_density', 'mass_density_stddev', - 'charge_density', 'charge_density_stddev'] + self.keys = [ + "mass_density", + "mass_density_stddev", + "charge_density", + "charge_density_stddev", + ] # Initialize results array with zeros for dim in self.results: - idx = self.results[dim]['dim'] - self.results[dim]['slice_volume'] = slices_vol[idx] + idx = self.results[dim]["dim"] + self.results[dim]["slice_volume"] = slices_vol[idx] for key in self.keys: self.results[dim][key] = np.zeros(self.nbins) @@ -249,7 +259,8 @@ def _single_frame(self): else: raise AttributeError( - f"{self.grouping} is not a valid value for grouping.") + f"{self.grouping} is not a valid value for grouping." + ) self.totalmass = np.sum(self.masses) @@ -257,37 +268,41 @@ def _single_frame(self): self._ags[0].wrap(compound=self.grouping) # Find position of atom/group of atoms - if self.grouping == 'atoms': + if self.grouping == "atoms": positions = self._ags[0].positions # faster for atoms else: # Centre of mass for residues, segments, fragments positions = self._ags[0].center_of_mass(compound=self.grouping) - for dim in ['x', 'y', 'z']: - idx = self.results[dim]['dim'] + for dim in ["x", "y", "z"]: + idx = self.results[dim]["dim"] - key = 'mass_density' - key_std = 'mass_density_stddev' + key = "mass_density" + key_std = "mass_density_stddev" # histogram for positions weighted on masses - hist, _ = np.histogram(positions[:, idx], - weights=self.masses, - bins=self.nbins, - range=(0.0, max(self.dimensions))) + hist, _ = np.histogram( + positions[:, idx], + weights=self.masses, + bins=self.nbins, + range=(0.0, max(self.dimensions)), + ) self.results[dim][key] += hist self.results[dim][key_std] += np.square(hist) - key = 'charge_density' - key_std = 'charge_density_stddev' + key = "charge_density" + key_std = "charge_density_stddev" # histogram for positions weighted on charges - hist, bin_edges = np.histogram(positions[:, idx], - weights=self.charges, - bins=self.nbins, - range=(0.0, max(self.dimensions))) + hist, bin_edges = np.histogram( + positions[:, idx], + weights=self.charges, + bins=self.nbins, + range=(0.0, max(self.dimensions)), + ) self.results[dim][key] += hist self.results[dim][key_std] += np.square(hist) - self.results[dim]['hist_bin_edges'] = bin_edges + self.results[dim]["hist_bin_edges"] = bin_edges def _conclude(self): avogadro = constants["N_Avogadro"] # unit: mol^{-1} @@ -296,9 +311,13 @@ def _conclude(self): k = avogadro * volume_conversion # Average results over the number of configurations - for dim in ['x', 'y', 'z']: - for key in ['mass_density', 'mass_density_stddev', - 'charge_density', 'charge_density_stddev']: + for dim in ["x", "y", "z"]: + for key in [ + "mass_density", + "mass_density_stddev", + "charge_density", + "charge_density_stddev", + ]: self.results[dim][key] /= self.n_frames # Compute standard deviation for the error # For certain tests in testsuite, floating point imprecision @@ -306,29 +325,35 @@ def _conclude(self): # radicand_mass and radicand_charge are therefore calculated first # and negative values set to 0 before the square root # is calculated. - radicand_mass = self.results[dim]['mass_density_stddev'] - \ - np.square(self.results[dim]['mass_density']) + radicand_mass = self.results[dim][ + "mass_density_stddev" + ] - np.square(self.results[dim]["mass_density"]) radicand_mass[radicand_mass < 0] = 0 - self.results[dim]['mass_density_stddev'] = np.sqrt(radicand_mass) + self.results[dim]["mass_density_stddev"] = np.sqrt(radicand_mass) - radicand_charge = self.results[dim]['charge_density_stddev'] - \ - np.square(self.results[dim]['charge_density']) + radicand_charge = self.results[dim][ + "charge_density_stddev" + ] - np.square(self.results[dim]["charge_density"]) radicand_charge[radicand_charge < 0] = 0 - self.results[dim]['charge_density_stddev'] = \ - np.sqrt(radicand_charge) + self.results[dim]["charge_density_stddev"] = np.sqrt( + radicand_charge + ) - for dim in ['x', 'y', 'z']: + for dim in ["x", "y", "z"]: # norming factor, units of mol^-1 cm^3 - norm = k * self.results[dim]['slice_volume'] + norm = k * self.results[dim]["slice_volume"] for key in self.keys: self.results[dim][key] /= norm # TODO: Remove in 3.0.0 - @deprecate(release="2.2.0", remove="3.0.0", - message="It will be replaced by a :meth:`_reduce` " - "method in the future") + @deprecate( + release="2.2.0", + remove="3.0.0", + message="It will be replaced by a :meth:`_reduce` " + "method in the future", + ) def _add_other_results(self, other): """For parallel analysis""" - for dim in ['x', 'y', 'z']: + for dim in ["x", "y", "z"]: for key in self.keys: self.results[dim][key] += other.results[dim][key] diff --git a/package/MDAnalysis/analysis/msd.py b/package/MDAnalysis/analysis/msd.py index 4515ed40983..8d0e7a39e1e 100644 --- a/package/MDAnalysis/analysis/msd.py +++ b/package/MDAnalysis/analysis/msd.py @@ -246,16 +246,20 @@ from ..core import groups from tqdm import tqdm -logger = logging.getLogger('MDAnalysis.analysis.msd') - -due.cite(Doi("10.21105/joss.00877"), - description="Mean Squared Displacements with tidynamics", - path="MDAnalysis.analysis.msd", - cite_module=True) -due.cite(Doi("10.1051/sfn/201112010"), - description="FCA fast correlation algorithm", - path="MDAnalysis.analysis.msd", - cite_module=True) +logger = logging.getLogger("MDAnalysis.analysis.msd") + +due.cite( + Doi("10.21105/joss.00877"), + description="Mean Squared Displacements with tidynamics", + path="MDAnalysis.analysis.msd", + cite_module=True, +) +due.cite( + Doi("10.1051/sfn/201112010"), + description="FCA fast correlation algorithm", + path="MDAnalysis.analysis.msd", + cite_module=True, +) del Doi @@ -297,7 +301,7 @@ class EinsteinMSD(AnalysisBase): .. versionadded:: 2.0.0 """ - def __init__(self, u, select='all', msd_type='xyz', fft=True, **kwargs): + def __init__(self, u, select="all", msd_type="xyz", fft=True, **kwargs): r""" Parameters ---------- @@ -314,8 +318,9 @@ def __init__(self, u, select='all', msd_type='xyz', fft=True, **kwargs): The tidynamics package is required for `fft=True`. """ if isinstance(u, groups.UpdatingAtomGroup): - raise TypeError("UpdatingAtomGroups are not valid for MSD " - "computation") + raise TypeError( + "UpdatingAtomGroups are not valid for MSD " "computation" + ) super(EinsteinMSD, self).__init__(u.universe.trajectory, **kwargs) @@ -337,18 +342,25 @@ def __init__(self, u, select='all', msd_type='xyz', fft=True, **kwargs): def _prepare(self): # self.n_frames only available here # these need to be zeroed prior to each run() call - self.results.msds_by_particle = np.zeros((self.n_frames, - self.n_particles)) + self.results.msds_by_particle = np.zeros( + (self.n_frames, self.n_particles) + ) self._position_array = np.zeros( - (self.n_frames, self.n_particles, self.dim_fac)) + (self.n_frames, self.n_particles, self.dim_fac) + ) # self.results.timeseries not set here def _parse_msd_type(self): - r""" Sets up the desired dimensionality of the MSD. - - """ - keys = {'x': [0], 'y': [1], 'z': [2], 'xy': [0, 1], - 'xz': [0, 2], 'yz': [1, 2], 'xyz': [0, 1, 2]} + r"""Sets up the desired dimensionality of the MSD.""" + keys = { + "x": [0], + "y": [1], + "z": [2], + "xy": [0, 1], + "xz": [0, 2], + "yz": [1, 2], + "xyz": [0, 1, 2], + } self.msd_type = self.msd_type.lower() @@ -356,19 +368,19 @@ def _parse_msd_type(self): self._dim = keys[self.msd_type] except KeyError: raise ValueError( - 'invalid msd_type: {} specified, please specify one of xyz, ' - 'xy, xz, yz, x, y, z'.format(self.msd_type)) + "invalid msd_type: {} specified, please specify one of xyz, " + "xy, xz, yz, x, y, z".format(self.msd_type) + ) self.dim_fac = len(self._dim) def _single_frame(self): - r""" Constructs array of positions for MSD calculation. - - """ + r"""Constructs array of positions for MSD calculation.""" # shape of position array set here, use span in last dimension # from this point on - self._position_array[self._frame_index] = ( - self.ag.positions[:, self._dim]) + self._position_array[self._frame_index] = self.ag.positions[ + :, self._dim + ] def _conclude(self): if self.fft: @@ -377,9 +389,7 @@ def _conclude(self): self._conclude_simple() def _conclude_simple(self): - r""" Calculates the MSD via the simple "windowed" algorithm. - - """ + r"""Calculates the MSD via the simple "windowed" algorithm.""" lagtimes = np.arange(1, self.n_frames) positions = self._position_array.astype(np.float64) for lag in tqdm(lagtimes): @@ -389,13 +399,12 @@ def _conclude_simple(self): self.results.timeseries = self.results.msds_by_particle.mean(axis=1) def _conclude_fft(self): # with FFT, np.float64 bit prescision required. - r""" Calculates the MSD via the FCA fast correlation algorithm. - - """ + r"""Calculates the MSD via the FCA fast correlation algorithm.""" try: import tidynamics except ImportError: - raise ImportError("""ERROR --- tidynamics was not found! + raise ImportError( + """ERROR --- tidynamics was not found! tidynamics is required to compute an FFT based MSD (default) @@ -403,10 +412,12 @@ def _conclude_fft(self): # with FFT, np.float64 bit prescision required. pip install tidynamics - or set fft=False""") + or set fft=False""" + ) positions = self._position_array.astype(np.float64) for n in tqdm(range(self.n_particles)): self.results.msds_by_particle[:, n] = tidynamics.msd( - positions[:, n, :]) + positions[:, n, :] + ) self.results.timeseries = self.results.msds_by_particle.mean(axis=1) diff --git a/package/MDAnalysis/analysis/nucleicacids.py b/package/MDAnalysis/analysis/nucleicacids.py index b0f5013e799..bf68a954cb3 100644 --- a/package/MDAnalysis/analysis/nucleicacids.py +++ b/package/MDAnalysis/analysis/nucleicacids.py @@ -170,20 +170,21 @@ class NucPairDist(AnalysisBase): @classmethod def get_supported_backends(cls): - return ('serial', 'multiprocessing', 'dask') + return ("serial", "multiprocessing", "dask") _s1: mda.AtomGroup _s2: mda.AtomGroup _n_sel: int - - def __init__(self, selection1: List[mda.AtomGroup], - selection2: List[mda.AtomGroup], - **kwargs) -> None: - super( - NucPairDist, - self).__init__( - selection1[0].universe.trajectory, - **kwargs) + + def __init__( + self, + selection1: List[mda.AtomGroup], + selection2: List[mda.AtomGroup], + **kwargs, + ) -> None: + super(NucPairDist, self).__init__( + selection1[0].universe.trajectory, **kwargs + ) if len(selection1) != len(selection2): raise ValueError("Selections must be same length") @@ -199,10 +200,15 @@ def __init__(self, selection1: List[mda.AtomGroup], @staticmethod def select_strand_atoms( - strand1: ResidueGroup, strand2: ResidueGroup, - a1_name: str, a2_name: str, g_name: str = 'G', - a_name: str = 'A', u_name: str = 'U', - t_name: str = 'T', c_name: str = 'C' + strand1: ResidueGroup, + strand2: ResidueGroup, + a1_name: str, + a2_name: str, + g_name: str = "G", + a_name: str = "A", + u_name: str = "U", + t_name: str = "T", + c_name: str = "C", ) -> Tuple[List[mda.AtomGroup], List[mda.AtomGroup]]: r""" A helper method for nucleic acid pair distance analyses. @@ -266,17 +272,18 @@ def select_strand_atoms( f"AtomGroup in {pair} is not a valid nucleic acid" ) - ag1 = pair[0].atoms.select_atoms(f'name {a1}') - ag2 = pair[1].atoms.select_atoms(f'name {a2}') + ag1 = pair[0].atoms.select_atoms(f"name {a1}") + ag2 = pair[1].atoms.select_atoms(f"name {a2}") if not all(len(ag) > 0 for ag in [ag1, ag2]): - err_info: Tuple[Residue, str] = (pair[0], a1) \ - if len(ag1) == 0 else (pair[1], a2) + err_info: Tuple[Residue, str] = ( + (pair[0], a1) if len(ag1) == 0 else (pair[1], a2) + ) raise ValueError( ( f"{err_info[0]} returns an empty AtomGroup" - "with selection string \"name {a2}\"" + 'with selection string "name {a2}"' ) ) @@ -291,22 +298,22 @@ def _prepare(self) -> None: ) def _single_frame(self) -> None: - dist: np.ndarray = calc_bonds( - self._s1.positions, self._s2.positions - ) + dist: np.ndarray = calc_bonds(self._s1.positions, self._s2.positions) self.results.distances[self._frame_index, :] = dist def _conclude(self) -> None: - self.results['pair_distances'] = self.results['distances'] + 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, - } + return ResultsGroup( + lookup={ + "distances": ResultsGroup.ndarray_vstack, + } ) + class WatsonCrickDist(NucPairDist): r""" Watson-Crick base pair distance for selected @@ -426,11 +433,19 @@ class WatsonCrickDist(NucPairDist): but it is **deprecated** and will be removed in release 3.0.0. """ - def __init__(self, strand1: ResidueClass, strand2: ResidueClass, - n1_name: str = 'N1', n3_name: str = "N3", - g_name: str = 'G', a_name: str = 'A', u_name: str = 'U', - t_name: str = 'T', c_name: str = 'C', - **kwargs) -> None: + def __init__( + self, + strand1: ResidueClass, + strand2: ResidueClass, + n1_name: str = "N1", + n3_name: str = "N3", + g_name: str = "G", + a_name: str = "A", + u_name: str = "U", + t_name: str = "T", + c_name: str = "C", + **kwargs, + ) -> None: def verify_strand(strand: ResidueClass) -> ResidueGroup: # Helper method to verify the strands @@ -456,11 +471,18 @@ def verify_strand(strand: ResidueClass) -> ResidueGroup: strand1: ResidueGroup = verify_strand(strand1) strand2: ResidueGroup = verify_strand(strand2) - strand_atomgroups: Tuple[List[mda.AtomGroup], List[mda.AtomGroup]] = \ + strand_atomgroups: Tuple[List[mda.AtomGroup], List[mda.AtomGroup]] = ( self.select_strand_atoms( - strand1, strand2, n1_name, n3_name, - g_name=g_name, a_name=a_name, - t_name=t_name, u_name=u_name, c_name=c_name + strand1, + strand2, + n1_name, + n3_name, + g_name=g_name, + a_name=a_name, + t_name=t_name, + u_name=u_name, + c_name=c_name, + ) ) super(WatsonCrickDist, self).__init__( @@ -531,17 +553,32 @@ class MinorPairDist(NucPairDist): .. versionadded:: 2.7.0 """ - def __init__(self, strand1: ResidueGroup, strand2: ResidueGroup, - o2_name: str = 'O2', c2_name: str = "C2", - g_name: str = 'G', a_name: str = 'A', u_name: str = 'U', - t_name: str = 'T', c_name: str = 'C', - **kwargs) -> None: - - selections: Tuple[List[mda.AtomGroup], List[mda.AtomGroup]] = \ + def __init__( + self, + strand1: ResidueGroup, + strand2: ResidueGroup, + o2_name: str = "O2", + c2_name: str = "C2", + g_name: str = "G", + a_name: str = "A", + u_name: str = "U", + t_name: str = "T", + c_name: str = "C", + **kwargs, + ) -> None: + + selections: Tuple[List[mda.AtomGroup], List[mda.AtomGroup]] = ( self.select_strand_atoms( - strand1, strand2, c2_name, o2_name, - g_name=g_name, a_name=a_name, - t_name=t_name, u_name=u_name, c_name=c_name + strand1, + strand2, + c2_name, + o2_name, + g_name=g_name, + a_name=a_name, + t_name=t_name, + u_name=u_name, + c_name=c_name, + ) ) super(MinorPairDist, self).__init__( @@ -614,17 +651,32 @@ class MajorPairDist(NucPairDist): .. versionadded:: 2.7.0 """ - def __init__(self, strand1: ResidueGroup, strand2: ResidueGroup, - n4_name: str = 'N4', o6_name: str = "O6", - g_name: str = 'G', a_name: str = 'A', u_name: str = 'U', - t_name: str = 'T', c_name: str = 'C', - **kwargs) -> None: - - selections: Tuple[List[mda.AtomGroup], List[mda.AtomGroup]] = \ + def __init__( + self, + strand1: ResidueGroup, + strand2: ResidueGroup, + n4_name: str = "N4", + o6_name: str = "O6", + g_name: str = "G", + a_name: str = "A", + u_name: str = "U", + t_name: str = "T", + c_name: str = "C", + **kwargs, + ) -> None: + + selections: Tuple[List[mda.AtomGroup], List[mda.AtomGroup]] = ( self.select_strand_atoms( - strand1, strand2, o6_name, n4_name, g_name=g_name, - a_name=a_name, t_name=t_name, u_name=u_name, - c_name=c_name + strand1, + strand2, + o6_name, + n4_name, + g_name=g_name, + a_name=a_name, + t_name=t_name, + u_name=u_name, + c_name=c_name, + ) ) super(MajorPairDist, self).__init__( diff --git a/package/MDAnalysis/analysis/nuclinfo.py b/package/MDAnalysis/analysis/nuclinfo.py index 39be7191d8a..a6772599142 100644 --- a/package/MDAnalysis/analysis/nuclinfo.py +++ b/package/MDAnalysis/analysis/nuclinfo.py @@ -130,13 +130,32 @@ def wc_pair(universe, i, bp, seg1="SYSTEM", seg2="SYSTEM"): .. versionadded:: 0.7.6 """ - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DC", "DT", "U", "C", "T", "CYT", "THY", "URA"]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DC", + "DT", + "U", + "C", + "T", + "CYT", + "THY", + "URA", + ]: a1, a2 = "N3", "N1" - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DG", "DA", "A", "G", "ADE", "GUA"]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DG", + "DA", + "A", + "G", + "ADE", + "GUA", + ]: a1, a2 = "N1", "N3" - wc_dist = universe.select_atoms("(segid {0!s} and resid {1!s} and name {2!s}) " - "or (segid {3!s} and resid {4!s} and name {5!s}) " - .format(seg1, i, a1, seg2, bp, a2)) + wc_dist = universe.select_atoms( + "(segid {0!s} and resid {1!s} and name {2!s}) " + "or (segid {3!s} and resid {4!s} and name {5!s}) ".format( + seg1, i, a1, seg2, bp, a2 + ) + ) wc = mdamath.norm(wc_dist[0].position - wc_dist[1].position) return wc @@ -172,13 +191,32 @@ def minor_pair(universe, i, bp, seg1="SYSTEM", seg2="SYSTEM"): .. versionadded:: 0.7.6 """ - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DC", "DT", "U", "C", "T", "CYT", "THY", "URA"]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DC", + "DT", + "U", + "C", + "T", + "CYT", + "THY", + "URA", + ]: a1, a2 = "O2", "C2" - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DG", "DA", "A", "G", "ADE", "GUA"]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DG", + "DA", + "A", + "G", + "ADE", + "GUA", + ]: a1, a2 = "C2", "O2" - c2o2_dist = universe.select_atoms("(segid {0!s} and resid {1!s} and name {2!s}) " - "or (segid {3!s} and resid {4!s} and name {5!s})" - .format(seg1, i, a1, seg2, bp, a2)) + c2o2_dist = universe.select_atoms( + "(segid {0!s} and resid {1!s} and name {2!s}) " + "or (segid {3!s} and resid {4!s} and name {5!s})".format( + seg1, i, a1, seg2, bp, a2 + ) + ) c2o2 = mdamath.norm(c2o2_dist[0].position - c2o2_dist[1].position) return c2o2 @@ -215,19 +253,48 @@ def major_pair(universe, i, bp, seg1="SYSTEM", seg2="SYSTEM"): .. versionadded:: 0.7.6 """ - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DC", "DG", "C", "G", "CYT", "GUA"]: - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DC", "C", "CYT"]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DC", + "DG", + "C", + "G", + "CYT", + "GUA", + ]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DC", + "C", + "CYT", + ]: a1, a2 = "N4", "O6" else: a1, a2 = "O6", "N4" - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DT", "DA", "A", "T", "U", "ADE", "THY", "URA"]: - if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in ["DT", "T", "THY", "U", "URA"]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DT", + "DA", + "A", + "T", + "U", + "ADE", + "THY", + "URA", + ]: + if universe.select_atoms(" resid {0!s} ".format(i)).resnames[0] in [ + "DT", + "T", + "THY", + "U", + "URA", + ]: a1, a2 = "O4", "N6" else: a1, a2 = "N6", "O4" - no_dist = universe.select_atoms("(segid {0!s} and resid {1!s} and name {2!s}) " - "or (segid {3!s} and resid {4!s} and name {5!s}) " - .format(seg1, i, a1, seg2, bp, a2)) + no_dist = universe.select_atoms( + "(segid {0!s} and resid {1!s} and name {2!s}) " + "or (segid {3!s} and resid {4!s} and name {5!s}) ".format( + seg1, i, a1, seg2, bp, a2 + ) + ) major = mdamath.norm(no_dist[0].position - no_dist[1].position) return major @@ -255,11 +322,11 @@ def phase_cp(universe, seg, i): .. versionadded:: 0.7.6 """ - atom1 = universe.select_atoms(" atom {0!s} {1!s} O4\' ".format(seg, i)) - atom2 = universe.select_atoms(" atom {0!s} {1!s} C1\' ".format(seg, i)) - atom3 = universe.select_atoms(" atom {0!s} {1!s} C2\' ".format(seg, i)) - atom4 = universe.select_atoms(" atom {0!s} {1!s} C3\' ".format(seg, i)) - atom5 = universe.select_atoms(" atom {0!s} {1!s} C4\' ".format(seg, i)) + atom1 = universe.select_atoms(" atom {0!s} {1!s} O4' ".format(seg, i)) + atom2 = universe.select_atoms(" atom {0!s} {1!s} C1' ".format(seg, i)) + atom3 = universe.select_atoms(" atom {0!s} {1!s} C2' ".format(seg, i)) + atom4 = universe.select_atoms(" atom {0!s} {1!s} C3' ".format(seg, i)) + atom5 = universe.select_atoms(" atom {0!s} {1!s} C4' ".format(seg, i)) data1 = atom1.positions data2 = atom2.positions @@ -274,13 +341,21 @@ def phase_cp(universe, seg, i): r4 = data4 - r0 r5 = data5 - r0 - R1 = ((r1 * sin(2 * pi * 0.0 / 5.0)) + (r2 * sin(2 * pi * 1.0 / 5.0)) + - (r3 * sin(2 * pi * 2.0 / 5.0)) + (r4 * sin(2 * pi * 3.0 / 5.0)) + - (r5 * sin(2 * pi * 4.0 / 5.0))) - - R2 = ((r1 * cos(2 * pi * 0.0 / 5.0)) + (r2 * cos(2 * pi * 1.0 / 5.0)) + - (r3 * cos(2 * pi * 2.0 / 5.0)) + (r4 * cos(2 * pi * 3.0 / 5.0)) + - (r5 * cos(2 * pi * 4.0 / 5.0))) + R1 = ( + (r1 * sin(2 * pi * 0.0 / 5.0)) + + (r2 * sin(2 * pi * 1.0 / 5.0)) + + (r3 * sin(2 * pi * 2.0 / 5.0)) + + (r4 * sin(2 * pi * 3.0 / 5.0)) + + (r5 * sin(2 * pi * 4.0 / 5.0)) + ) + + R2 = ( + (r1 * cos(2 * pi * 0.0 / 5.0)) + + (r2 * cos(2 * pi * 1.0 / 5.0)) + + (r3 * cos(2 * pi * 2.0 / 5.0)) + + (r4 * cos(2 * pi * 3.0 / 5.0)) + + (r5 * cos(2 * pi * 4.0 / 5.0)) + ) x = np.cross(R1[0], R2[0]) n = x / sqrt(pow(x[0], 2) + pow(x[1], 2) + pow(x[2], 2)) @@ -291,15 +366,27 @@ def phase_cp(universe, seg, i): r4_d = np.dot(r4, n) r5_d = np.dot(r5, n) - D = ((r1_d * sin(4 * pi * 0.0 / 5.0)) + (r2_d * sin(4 * pi * 1.0 / 5.0)) - + (r3_d * sin(4 * pi * 2.0 / 5.0)) + (r4_d * sin(4 * pi * 3.0 / 5.0)) - + (r5_d * sin(4 * pi * 4.0 / 5.0))) * -1 * sqrt(2.0 / 5.0) - - C = ((r1_d * cos(4 * pi * 0.0 / 5.0)) + (r2_d * cos(4 * pi * 1.0 / 5.0)) - + (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 = (np.arctan2(D, C) + (pi / 2.)) * 180. / pi + D = ( + ( + (r1_d * sin(4 * pi * 0.0 / 5.0)) + + (r2_d * sin(4 * pi * 1.0 / 5.0)) + + (r3_d * sin(4 * pi * 2.0 / 5.0)) + + (r4_d * sin(4 * pi * 3.0 / 5.0)) + + (r5_d * sin(4 * pi * 4.0 / 5.0)) + ) + * -1 + * sqrt(2.0 / 5.0) + ) + + C = ( + (r1_d * cos(4 * pi * 0.0 / 5.0)) + + (r2_d * cos(4 * pi * 1.0 / 5.0)) + + (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 = (np.arctan2(D, C) + (pi / 2.0)) * 180.0 / pi return phase_ang % 360 @@ -325,30 +412,40 @@ def phase_as(universe, seg, i): .. versionadded:: 0.7.6 """ - angle1 = universe.select_atoms(" atom {0!s} {1!s} C1\' ".format(seg, i), - " atom {0!s} {1!s} C2\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i)) - - angle2 = universe.select_atoms(" atom {0!s} {1!s} C2\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} O4\' ".format(seg, i)) - - angle3 = universe.select_atoms(" atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} O4\' ".format(seg, i), - " atom {0!s} {1!s} C1\' ".format(seg, i)) - - angle4 = universe.select_atoms(" atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} O4\' ".format(seg, i), - " atom {0!s} {1!s} C1\' ".format(seg, i), - " atom {0!s} {1!s} C2\' ".format(seg, i)) - - angle5 = universe.select_atoms(" atom {0!s} {1!s} O4\' ".format(seg, i), - " atom {0!s} {1!s} C1\' ".format(seg, i), - " atom {0!s} {1!s} C2\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i)) + angle1 = universe.select_atoms( + " atom {0!s} {1!s} C1' ".format(seg, i), + " atom {0!s} {1!s} C2' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + ) + + angle2 = universe.select_atoms( + " atom {0!s} {1!s} C2' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} O4' ".format(seg, i), + ) + + angle3 = universe.select_atoms( + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} O4' ".format(seg, i), + " atom {0!s} {1!s} C1' ".format(seg, i), + ) + + angle4 = universe.select_atoms( + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} O4' ".format(seg, i), + " atom {0!s} {1!s} C1' ".format(seg, i), + " atom {0!s} {1!s} C2' ".format(seg, i), + ) + + angle5 = universe.select_atoms( + " atom {0!s} {1!s} O4' ".format(seg, i), + " atom {0!s} {1!s} C1' ".format(seg, i), + " atom {0!s} {1!s} C2' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + ) data1 = angle1.dihedral.value() data2 = angle2.dihedral.value() @@ -356,19 +453,31 @@ def phase_as(universe, seg, i): data4 = angle4.dihedral.value() data5 = angle5.dihedral.value() - B = ((data1 * sin(2 * 2 * pi * (1 - 1.) / 5.)) - + (data2 * sin(2 * 2 * pi * (2 - 1.) / 5.)) - + (data3 * sin(2 * 2 * pi * (3 - 1.) / 5.)) - + (data4 * sin(2 * 2 * pi * (4 - 1.) / 5.)) - + (data5 * sin(2 * 2 * pi * (5 - 1.) / 5.))) * -2. / 5. - - A = ((data1 * cos(2 * 2 * pi * (1 - 1.) / 5.)) - + (data2 * cos(2 * 2 * pi * (2 - 1.) / 5.)) - + (data3 * cos(2 * 2 * pi * (3 - 1.) / 5.)) - + (data4 * cos(2 * 2 * pi * (4 - 1.) / 5.)) - + (data5 * cos(2 * 2 * pi * (5 - 1.) / 5.))) * 2. / 5. - - phase_ang = np.arctan2(B, A) * 180. / pi + B = ( + ( + (data1 * sin(2 * 2 * pi * (1 - 1.0) / 5.0)) + + (data2 * sin(2 * 2 * pi * (2 - 1.0) / 5.0)) + + (data3 * sin(2 * 2 * pi * (3 - 1.0) / 5.0)) + + (data4 * sin(2 * 2 * pi * (4 - 1.0) / 5.0)) + + (data5 * sin(2 * 2 * pi * (5 - 1.0) / 5.0)) + ) + * -2.0 + / 5.0 + ) + + A = ( + ( + (data1 * cos(2 * 2 * pi * (1 - 1.0) / 5.0)) + + (data2 * cos(2 * 2 * pi * (2 - 1.0) / 5.0)) + + (data3 * cos(2 * 2 * pi * (3 - 1.0) / 5.0)) + + (data4 * cos(2 * 2 * pi * (4 - 1.0) / 5.0)) + + (data5 * cos(2 * 2 * pi * (5 - 1.0) / 5.0)) + ) + * 2.0 + / 5.0 + ) + + phase_ang = np.arctan2(B, A) * 180.0 / pi return phase_ang % 360 @@ -401,44 +510,60 @@ def tors(universe, seg, i): .. versionadded:: 0.7.6 """ - a = universe.select_atoms(" atom {0!s} {1!s} O3\' ".format(seg, i - 1), - " atom {0!s} {1!s} P ".format(seg, i), - " atom {0!s} {1!s} O5\' ".format(seg, i), - " atom {0!s} {1!s} C5\' ".format(seg, i)) - - b = universe.select_atoms(" atom {0!s} {1!s} P ".format(seg, i), - " atom {0!s} {1!s} O5\' ".format(seg, i), - " atom {0!s} {1!s} C5\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i)) - - g = universe.select_atoms(" atom {0!s} {1!s} O5\' ".format(seg, i), - " atom {0!s} {1!s} C5\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i)) - - d = universe.select_atoms(" atom {0!s} {1!s} C5\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} O3\' ".format(seg, i)) - - e = universe.select_atoms(" atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} O3\' ".format(seg, i), - " atom {0!s} {1!s} P ".format(seg, i + 1)) - - z = universe.select_atoms(" atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} O3\' ".format(seg, i), - " atom {0!s} {1!s} P ".format(seg, i + 1), - " atom {0!s} {1!s} O5\' ".format(seg, i + 1)) - c = universe.select_atoms(" atom {0!s} {1!s} O4\' ".format(seg, i), - " atom {0!s} {1!s} C1\' ".format(seg, i), - " atom {0!s} {1!s} N9 ".format(seg, i), - " atom {0!s} {1!s} C4 ".format(seg, i)) + a = universe.select_atoms( + " atom {0!s} {1!s} O3' ".format(seg, i - 1), + " atom {0!s} {1!s} P ".format(seg, i), + " atom {0!s} {1!s} O5' ".format(seg, i), + " atom {0!s} {1!s} C5' ".format(seg, i), + ) + + b = universe.select_atoms( + " atom {0!s} {1!s} P ".format(seg, i), + " atom {0!s} {1!s} O5' ".format(seg, i), + " atom {0!s} {1!s} C5' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + ) + + g = universe.select_atoms( + " atom {0!s} {1!s} O5' ".format(seg, i), + " atom {0!s} {1!s} C5' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + ) + + d = universe.select_atoms( + " atom {0!s} {1!s} C5' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} O3' ".format(seg, i), + ) + + e = universe.select_atoms( + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} O3' ".format(seg, i), + " atom {0!s} {1!s} P ".format(seg, i + 1), + ) + + z = universe.select_atoms( + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} O3' ".format(seg, i), + " atom {0!s} {1!s} P ".format(seg, i + 1), + " atom {0!s} {1!s} O5' ".format(seg, i + 1), + ) + c = universe.select_atoms( + " atom {0!s} {1!s} O4' ".format(seg, i), + " atom {0!s} {1!s} C1' ".format(seg, i), + " atom {0!s} {1!s} N9 ".format(seg, i), + " atom {0!s} {1!s} C4 ".format(seg, i), + ) if len(c) < 4: - c = universe.select_atoms(" atom {0!s} {1!s} O4\' ".format(seg, i), - " atom {0!s} {1!s} C1\' ".format(seg, i), - " atom {0!s} {1!s} N1 ".format(seg, i), - " atom {0!s} {1!s} C2 ".format(seg, i)) + c = universe.select_atoms( + " atom {0!s} {1!s} O4' ".format(seg, i), + " atom {0!s} {1!s} C1' ".format(seg, i), + " atom {0!s} {1!s} N1 ".format(seg, i), + " atom {0!s} {1!s} C2 ".format(seg, i), + ) alpha = a.dihedral.value() % 360 beta = b.dihedral.value() % 360 @@ -473,10 +598,12 @@ def tors_alpha(universe, seg, i): .. versionadded:: 0.7.6 """ - a = universe.select_atoms(" atom {0!s} {1!s} O3\' ".format(seg, i - 1), - " atom {0!s} {1!s} P ".format(seg, i), - " atom {0!s} {1!s} O5\' ".format(seg, i), - " atom {0!s} {1!s} C5\' ".format(seg, i)) + a = universe.select_atoms( + " atom {0!s} {1!s} O3' ".format(seg, i - 1), + " atom {0!s} {1!s} P ".format(seg, i), + " atom {0!s} {1!s} O5' ".format(seg, i), + " atom {0!s} {1!s} C5' ".format(seg, i), + ) alpha = a.dihedral.value() % 360 return alpha @@ -503,16 +630,18 @@ def tors_beta(universe, seg, i): .. versionadded:: 0.7.6 """ - b = universe.select_atoms(" atom {0!s} {1!s} P ".format(seg, i), - " atom {0!s} {1!s} O5\' ".format(seg, i), - " atom {0!s} {1!s} C5\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i)) + b = universe.select_atoms( + " atom {0!s} {1!s} P ".format(seg, i), + " atom {0!s} {1!s} O5' ".format(seg, i), + " atom {0!s} {1!s} C5' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + ) beta = b.dihedral.value() % 360 return beta def tors_gamma(universe, seg, i): - """ Gamma backbone dihedral + """Gamma backbone dihedral The dihedral is computed based on position atoms for resid `i`. @@ -533,10 +662,12 @@ def tors_gamma(universe, seg, i): .. versionadded:: 0.7.6 """ - g = universe.select_atoms(" atom {0!s} {1!s} O5\' ".format(seg, i), - " atom {0!s} {1!s} C5\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i)) + g = universe.select_atoms( + " atom {0!s} {1!s} O5' ".format(seg, i), + " atom {0!s} {1!s} C5' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + ) gamma = g.dihedral.value() % 360 return gamma @@ -563,10 +694,12 @@ def tors_delta(universe, seg, i): .. versionadded:: 0.7.6 """ - d = universe.select_atoms(" atom {0!s} {1!s} C5\' ".format(seg, i), - " atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} O3\' ".format(seg, i)) + d = universe.select_atoms( + " atom {0!s} {1!s} C5' ".format(seg, i), + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} O3' ".format(seg, i), + ) delta = d.dihedral.value() % 360 return delta @@ -593,10 +726,12 @@ def tors_eps(universe, seg, i): .. versionadded:: 0.7.6 """ - e = universe.select_atoms(" atom {0!s} {1!s} C4\' ".format(seg, i), - " atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} O3\' ".format(seg, i), - " atom {0!s} {1!s} P ".format(seg, i + 1)) + e = universe.select_atoms( + " atom {0!s} {1!s} C4' ".format(seg, i), + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} O3' ".format(seg, i), + " atom {0!s} {1!s} P ".format(seg, i + 1), + ) epsilon = e.dihedral.value() % 360 return epsilon @@ -623,10 +758,12 @@ def tors_zeta(universe, seg, i): .. versionadded:: 0.7.6 """ - z = universe.select_atoms(" atom {0!s} {1!s} C3\' ".format(seg, i), - " atom {0!s} {1!s} O3\' ".format(seg, i), - " atom {0!s} {1!s} P ".format(seg, i + 1), - " atom {0!s} {1!s} O5\' ".format(seg, i + 1)) + z = universe.select_atoms( + " atom {0!s} {1!s} C3' ".format(seg, i), + " atom {0!s} {1!s} O3' ".format(seg, i), + " atom {0!s} {1!s} P ".format(seg, i + 1), + " atom {0!s} {1!s} O5' ".format(seg, i + 1), + ) zeta = z.dihedral.value() % 360 return zeta @@ -653,15 +790,19 @@ def tors_chi(universe, seg, i): .. versionadded:: 0.7.6 """ - c = universe.select_atoms(" atom {0!s} {1!s} O4\' ".format(seg, i), - " atom {0!s} {1!s} C1\' ".format(seg, i), - " atom {0!s} {1!s} N9 ".format(seg, i), - " atom {0!s} {1!s} C4 ".format(seg, i)) + c = universe.select_atoms( + " atom {0!s} {1!s} O4' ".format(seg, i), + " atom {0!s} {1!s} C1' ".format(seg, i), + " atom {0!s} {1!s} N9 ".format(seg, i), + " atom {0!s} {1!s} C4 ".format(seg, i), + ) if len(c) < 4: - c = universe.select_atoms(" atom {0!s} {1!s} O4\' ".format(seg, i), - " atom {0!s} {1!s} C1\' ".format(seg, i), - " atom {0!s} {1!s} N1 ".format(seg, i), - " atom {0!s} {1!s} C2 ".format(seg, i)) + c = universe.select_atoms( + " atom {0!s} {1!s} O4' ".format(seg, i), + " atom {0!s} {1!s} C1' ".format(seg, i), + " atom {0!s} {1!s} N1 ".format(seg, i), + " atom {0!s} {1!s} C2 ".format(seg, i), + ) chi = c.dihedral.value() % 360 return chi @@ -691,22 +832,27 @@ def hydroxyl(universe, seg, i): .. versionadded:: 0.7.6 """ - h = universe.select_atoms("atom {0!s} {1!s} C1'".format(seg, i), - "atom {0!s} {1!s} C2'".format(seg, i), - "atom {0!s} {1!s} O2'".format(seg, i), - "atom {0!s} {1!s} H2'".format(seg, i)) + h = universe.select_atoms( + "atom {0!s} {1!s} C1'".format(seg, i), + "atom {0!s} {1!s} C2'".format(seg, i), + "atom {0!s} {1!s} O2'".format(seg, i), + "atom {0!s} {1!s} H2'".format(seg, i), + ) try: hydr = h.dihedral.value() % 360 except ValueError: - errmsg = (f"Resid {i} does not contain atoms C1', C2', O2', H2' but " - f"atoms {list(h.atoms)}") + errmsg = ( + f"Resid {i} does not contain atoms C1', C2', O2', H2' but " + f"atoms {list(h.atoms)}" + ) raise ValueError(errmsg) from None return hydr -def pseudo_dihe_baseflip(universe, bp1, bp2, i, - seg1="SYSTEM", seg2="SYSTEM", seg3="SYSTEM"): +def pseudo_dihe_baseflip( + universe, bp1, bp2, i, seg1="SYSTEM", seg2="SYSTEM", seg3="SYSTEM" +): """pseudo dihedral for flipped bases. Useful only for nucleic acid base flipping The dihedral is computed based on position atoms for resid `i` @@ -742,13 +888,25 @@ def pseudo_dihe_baseflip(universe, bp1, bp2, i, """ bf1 = universe.select_atoms( " ( segid {0!s} and resid {1!s} and nucleicbase ) " - "or ( segid {2!s} and resid {3!s} and nucleicbase ) " - .format( seg1, bp1, seg2, bp2)) - bf4 = universe.select_atoms("(segid {0!s} and resid {1!s} and nucleicbase) ".format(seg3, i)) - bf2 = universe.select_atoms("(segid {0!s} and resid {1!s} and nucleicsugar) ".format(seg2, bp2)) - bf3 = universe.select_atoms("(segid {0!s} and resid {1!s} and nucleicsugar) ".format(seg3, i)) - x = [bf1.center_of_mass(), bf2.center_of_mass(), - bf3.center_of_mass(), bf4.center_of_mass()] + "or ( segid {2!s} and resid {3!s} and nucleicbase ) ".format( + seg1, bp1, seg2, bp2 + ) + ) + bf4 = universe.select_atoms( + "(segid {0!s} and resid {1!s} and nucleicbase) ".format(seg3, i) + ) + bf2 = universe.select_atoms( + "(segid {0!s} and resid {1!s} and nucleicsugar) ".format(seg2, bp2) + ) + bf3 = universe.select_atoms( + "(segid {0!s} and resid {1!s} and nucleicsugar) ".format(seg3, i) + ) + x = [ + bf1.center_of_mass(), + bf2.center_of_mass(), + bf3.center_of_mass(), + bf4.center_of_mass(), + ] pseudo = mdamath.dihedral(x[0] - x[1], x[1] - x[2], x[2] - x[3]) pseudo = np.rad2deg(pseudo) % 360 return pseudo diff --git a/package/MDAnalysis/analysis/pca.py b/package/MDAnalysis/analysis/pca.py index cbf4cb588c8..fddbf7d0092 100644 --- a/package/MDAnalysis/analysis/pca.py +++ b/package/MDAnalysis/analysis/pca.py @@ -239,10 +239,18 @@ 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): + def __init__( + self, + universe, + select="all", + align=False, + mean=None, + n_components=None, + **kwargs, + ): super(PCA, self).__init__(universe.trajectory, **kwargs) self._u = universe @@ -268,15 +276,18 @@ def _prepare(self): else: self.mean = np.asarray(self._mean) if self.mean.shape[0] != self._n_atoms: - raise ValueError('Number of atoms in reference ({}) does ' - 'not match number of atoms in the ' - 'selection ({})'.format(self._n_atoms, - self.mean.shape[0])) + raise ValueError( + "Number of atoms in reference ({}) does " + "not match number of atoms in the " + "selection ({})".format(self._n_atoms, self.mean.shape[0]) + ) self._calc_mean = False if self.n_frames == 1: - raise ValueError('No covariance information can be gathered from a' - 'single trajectory frame.\n') + raise ValueError( + "No covariance information can be gathered from a" + "single trajectory frame.\n" + ) n_dim = self._n_atoms * 3 self.cov = np.zeros((n_dim, n_dim)) self._ref_atom_positions = self._reference.positions @@ -284,15 +295,20 @@ def _prepare(self): self._ref_atom_positions -= self._ref_cog if self._calc_mean: - for ts in ProgressBar(self._sliced_trajectory, - verbose=self._verbose, desc="Mean Calculation"): + for ts in ProgressBar( + self._sliced_trajectory, + verbose=self._verbose, + desc="Mean Calculation", + ): if self.align: mobile_cog = self._atoms.center_of_geometry() - mobile_atoms, old_rmsd = _fit_to(self._atoms.positions - mobile_cog, - self._ref_atom_positions, - self._atoms, - mobile_com=mobile_cog, - ref_com=self._ref_cog) + mobile_atoms, old_rmsd = _fit_to( + self._atoms.positions - mobile_cog, + self._ref_atom_positions, + self._atoms, + mobile_com=mobile_cog, + ref_com=self._ref_cog, + ) self.mean += self._atoms.positions self.mean /= self.n_frames @@ -301,11 +317,13 @@ def _prepare(self): def _single_frame(self): if self.align: mobile_cog = self._atoms.center_of_geometry() - mobile_atoms, old_rmsd = _fit_to(self._atoms.positions - mobile_cog, - self._ref_atom_positions, - self._atoms, - mobile_com=mobile_cog, - ref_com=self._ref_cog) + mobile_atoms, old_rmsd = _fit_to( + self._atoms.positions - mobile_cog, + self._ref_atom_positions, + self._atoms, + mobile_com=mobile_cog, + ref_com=self._ref_cog, + ) # now all structures are aligned to reference x = mobile_atoms.positions.ravel() else: @@ -324,25 +342,31 @@ def _conclude(self): @property def p_components(self): - wmsg = ("The `p_components` attribute was deprecated in " - "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " - "Please use `results.p_components` instead.") + wmsg = ( + "The `p_components` attribute was deprecated in " + "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " + "Please use `results.p_components` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.p_components @property def variance(self): - wmsg = ("The `variance` attribute was deprecated in " - "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " - "Please use `results.variance` instead.") + wmsg = ( + "The `variance` attribute was deprecated in " + "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " + "Please use `results.variance` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.variance @property def cumulated_variance(self): - wmsg = ("The `cumulated_variance` attribute was deprecated in " - "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " - "Please use `results.cumulated_variance` instead.") + wmsg = ( + "The `cumulated_variance` attribute was deprecated in " + "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " + "Please use `results.cumulated_variance` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.cumulated_variance @@ -356,13 +380,21 @@ def n_components(self, n): if n is None: n = len(self._variance) self.results.variance = self._variance[:n] - self.results.cumulated_variance = (np.cumsum(self._variance) / - np.sum(self._variance))[:n] + self.results.cumulated_variance = ( + np.cumsum(self._variance) / np.sum(self._variance) + )[:n] self.results.p_components = self._p_components[:, :n] self._n_components = n - def transform(self, atomgroup, n_components=None, start=None, stop=None, - step=None, verbose=False): + def transform( + self, + atomgroup, + n_components=None, + start=None, + stop=None, + step=None, + verbose=False, + ): """Apply the dimensionality reduction on a trajectory Parameters @@ -403,31 +435,37 @@ def transform(self, atomgroup, n_components=None, start=None, stop=None, on with ``verbose = True``, or off with ``verbose = False`` """ if not self._calculated: - raise ValueError('Call run() on the PCA before using transform') + raise ValueError("Call run() on the PCA before using transform") if isinstance(atomgroup, Universe): atomgroup = atomgroup.atoms if self._n_atoms != atomgroup.n_atoms: - raise ValueError('PCA has been fit for' - '{} atoms. Your atomgroup' - 'has {} atoms'.format(self._n_atoms, - atomgroup.n_atoms)) + raise ValueError( + "PCA has been fit for" + "{} atoms. Your atomgroup" + "has {} atoms".format(self._n_atoms, atomgroup.n_atoms) + ) if not (self._atoms.types == atomgroup.types).all(): - warnings.warn('Atom types do not match with types used to fit PCA') + warnings.warn("Atom types do not match with types used to fit PCA") traj = atomgroup.universe.trajectory start, stop, step = traj.check_slice_indices(start, stop, step) n_frames = len(range(start, stop, step)) - dim = (n_components if n_components is not None else - self.results.p_components.shape[1]) + dim = ( + n_components + if n_components is not None + else self.results.p_components.shape[1] + ) dot = np.zeros((n_frames, dim)) - for i, ts in tqdm(enumerate(traj[start:stop:step]), disable=not verbose, - total=len(traj[start:stop:step]) - ): + for i, ts in tqdm( + enumerate(traj[start:stop:step]), + disable=not verbose, + total=len(traj[start:stop:step]), + ): xyz = atomgroup.positions.ravel() - self._xmean dot[i] = np.dot(xyz, self._p_components[:, :dim]) @@ -547,20 +585,22 @@ def project_single_frame(self, components=None, group=None, anchor=None): .. versionadded:: 2.2.0 """ if not self._calculated: - raise ValueError('Call run() on the PCA before projecting') + raise ValueError("Call run() on the PCA before projecting") if group is not None: if anchor is None: - raise ValueError("'anchor' cannot be 'None'" + - " if 'group' is not 'None'") + raise ValueError( + "'anchor' cannot be 'None'" + " if 'group' is not 'None'" + ) anchors = group.select_atoms(anchor) anchors_res_ids = anchors.resindices if np.unique(anchors_res_ids).size != anchors_res_ids.size: raise ValueError("More than one 'anchor' found in residues") if not np.isin(group.resindices, anchors_res_ids).all(): - raise ValueError("Some residues in 'group'" + - " do not have an 'anchor'") + raise ValueError( + "Some residues in 'group'" + " do not have an 'anchor'" + ) if not anchors.issubset(self._atoms): raise ValueError("Some 'anchors' are not part of PCA class") @@ -568,18 +608,22 @@ def project_single_frame(self, components=None, group=None, anchor=None): # sure that extrapolation works on residues, not random atoms. non_pca = group.residues.atoms - self._atoms pca_res_indices, pca_res_counts = np.unique( - self._atoms.resindices, return_counts=True) + self._atoms.resindices, return_counts=True + ) non_pca_atoms = np.array([], dtype=int) for res in group.residues: # n_common is the number of pca atoms in a residue - n_common = pca_res_counts[np.where( - pca_res_indices == res.resindex)][0] - non_pca_atoms = np.append(non_pca_atoms, - res.atoms.n_atoms - n_common) + n_common = pca_res_counts[ + np.where(pca_res_indices == res.resindex) + ][0] + non_pca_atoms = np.append( + non_pca_atoms, res.atoms.n_atoms - n_common + ) # index_extrapolate records the anchor number for each non-PCA atom - index_extrapolate = np.repeat(np.arange(anchors.atoms.n_atoms), - non_pca_atoms) + index_extrapolate = np.repeat( + np.arange(anchors.atoms.n_atoms), non_pca_atoms + ) if components is None: components = np.arange(self.results.p_components.shape[1]) @@ -591,22 +635,29 @@ def wrapped(ts): xyz = self._atoms.positions.ravel() - self._xmean self._atoms.positions = np.reshape( - (np.dot(np.dot(xyz, self._p_components[:, components]), - self._p_components[:, components].T) - + self._xmean), (-1, 3) + ( + np.dot( + np.dot(xyz, self._p_components[:, components]), + self._p_components[:, components].T, + ) + + self._xmean + ), + (-1, 3), ) if group is not None: - non_pca.positions += (anchors.positions - - anchors_coords_old)[index_extrapolate] + non_pca.positions += (anchors.positions - anchors_coords_old)[ + index_extrapolate + ] return ts + return wrapped @due.dcite( - Doi('10.1002/(SICI)1097-0134(19990901)36:4<419::AID-PROT5>3.0.CO;2-U'), - Doi('10.1529/biophysj.104.052449'), + Doi("10.1002/(SICI)1097-0134(19990901)36:4<419::AID-PROT5>3.0.CO;2-U"), + Doi("10.1529/biophysj.104.052449"), description="RMSIP", - path='MDAnalysis.analysis.pca', + path="MDAnalysis.analysis.pca", ) def rmsip(self, other, n_components=None): """Compute the root mean square inner product between subspaces. @@ -667,23 +718,24 @@ def rmsip(self, other, n_components=None): try: a = self.results.p_components except AttributeError: - raise ValueError('Call run() on the PCA before using rmsip') + raise ValueError("Call run() on the PCA before using rmsip") try: b = other.results.p_components except AttributeError: if isinstance(other, type(self)): raise ValueError( - 'Call run() on the other PCA before using rmsip') + "Call run() on the other PCA before using rmsip" + ) else: - raise ValueError('other must be another PCA class') + raise ValueError("other must be another PCA class") return rmsip(a.T, b.T, n_components=n_components) @due.dcite( - Doi('10.1016/j.str.2007.12.011'), + Doi("10.1016/j.str.2007.12.011"), description="Cumulative overlap", - path='MDAnalysis.analysis.pca', + path="MDAnalysis.analysis.pca", ) def cumulative_overlap(self, other, i=0, n_components=None): """Compute the cumulative overlap of a vector in a subspace. @@ -722,16 +774,18 @@ def cumulative_overlap(self, other, i=0, n_components=None): a = self.results.p_components except AttributeError: raise ValueError( - 'Call run() on the PCA before using cumulative_overlap') + "Call run() on the PCA before using cumulative_overlap" + ) try: b = other.results.p_components except AttributeError: if isinstance(other, type(self)): raise ValueError( - 'Call run() on the other PCA before using cumulative_overlap') + "Call run() on the other PCA before using cumulative_overlap" + ) else: - raise ValueError('other must be another PCA class') + raise ValueError("other must be another PCA class") return cumulative_overlap(a.T, b.T, i=i, n_components=n_components) @@ -767,15 +821,18 @@ def cosine_content(pca_space, i): t = np.arange(len(pca_space)) T = len(pca_space) cos = np.cos(np.pi * t * (i + 1) / T) - return ((2.0 / T) * (scipy.integrate.simpson(cos*pca_space[:, i])) ** 2 / - scipy.integrate.simpson(pca_space[:, i] ** 2)) + return ( + (2.0 / T) + * (scipy.integrate.simpson(cos * pca_space[:, i])) ** 2 + / scipy.integrate.simpson(pca_space[:, i] ** 2) + ) @due.dcite( - Doi('10.1002/(SICI)1097-0134(19990901)36:4<419::AID-PROT5>3.0.CO;2-U'), - Doi('10.1529/biophysj.104.052449'), + Doi("10.1002/(SICI)1097-0134(19990901)36:4<419::AID-PROT5>3.0.CO;2-U"), + Doi("10.1529/biophysj.104.052449"), description="RMSIP", - path='MDAnalysis.analysis.pca', + path="MDAnalysis.analysis.pca", ) def rmsip(a, b, n_components=None): """Compute the root mean square inner product between subspaces. @@ -845,7 +902,7 @@ def rmsip(a, b, n_components=None): elif len(n_components) == 2: n_a, n_b = n_components else: - raise ValueError('Too many values provided for n_components') + raise ValueError("Too many values provided for n_components") if n_a is None: n_a = len(a) @@ -853,14 +910,14 @@ def rmsip(a, b, n_components=None): n_b = len(b) sip = np.matmul(a[:n_a], b[:n_b].T) ** 2 - msip = sip.sum()/n_a + msip = sip.sum() / n_a return msip**0.5 @due.dcite( - Doi('10.1016/j.str.2007.12.011'), + Doi("10.1016/j.str.2007.12.011"), description="Cumulative overlap", - path='MDAnalysis.analysis.pca', + path="MDAnalysis.analysis.pca", ) def cumulative_overlap(a, b, i=0, n_components=None): """Compute the cumulative overlap of a vector in a subspace. @@ -906,5 +963,5 @@ def cumulative_overlap(a, b, i=0, n_components=None): b = b[:n_components] b_norms = (b**2).sum(axis=1) ** 0.5 - o = np.abs(np.matmul(vec, b.T)) / (b_norms*vec_norm) + o = np.abs(np.matmul(vec, b.T)) / (b_norms * vec_norm) return (o**2).sum() ** 0.5 diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index a38cf68daac..9c3cc39dee2 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -45,7 +45,7 @@ logger = logging.getLogger(__name__) -@requires('bonds') +@requires("bonds") def sort_backbone(backbone): """Rearrange a linear AtomGroup into backbone order @@ -68,25 +68,30 @@ def sort_backbone(backbone): .. versionadded:: 0.20.0 """ if not backbone.n_fragments == 1: - raise ValueError("{} fragments found in backbone. " - "backbone must be a single contiguous AtomGroup" - "".format(backbone.n_fragments)) + raise ValueError( + "{} fragments found in backbone. " + "backbone must be a single contiguous AtomGroup" + "".format(backbone.n_fragments) + ) - branches = [at for at in backbone - if len(at.bonded_atoms & backbone) > 2] + branches = [at for at in backbone if len(at.bonded_atoms & backbone) > 2] if branches: # find which atom has too many bonds for easier debug raise ValueError( "Backbone is not linear. " "The following atoms have more than two bonds in backbone: {}." - "".format(','.join(str(a) for a in branches))) + "".format(",".join(str(a) for a in branches)) + ) - caps = [atom for atom in backbone - if len(atom.bonded_atoms & backbone) == 1] + caps = [ + atom for atom in backbone if len(atom.bonded_atoms & backbone) == 1 + ] if not caps: # cyclical structure - raise ValueError("Could not find starting point of backbone, " - "is the backbone cyclical?") + raise ValueError( + "Could not find starting point of backbone, " + "is the backbone cyclical?" + ) # arbitrarily choose one of the capping atoms to be the startpoint sorted_backbone = AtomGroup([caps[0]]) @@ -229,9 +234,11 @@ class PersistenceLength(AnalysisBase): :attr:`lb`, :attr:`lp`, :attr:`fit` are now stored in a :class:`MDAnalysis.analysis.base.Results` instance. """ + def __init__(self, atomgroups, **kwargs): super(PersistenceLength, self).__init__( - atomgroups[0].universe.trajectory, **kwargs) + atomgroups[0].universe.trajectory, **kwargs + ) self._atomgroups = atomgroups # Check that all chains are the same length @@ -256,30 +263,36 @@ def _single_frame(self): vecs /= np.sqrt((vecs * vecs).sum(axis=1))[:, None] inner_pr = np.inner(vecs, vecs) - for i in range(n-1): - self._results[:(n-1)-i] += inner_pr[i, i:] + for i in range(n - 1): + self._results[: (n - 1) - i] += inner_pr[i, i:] @property def lb(self): - wmsg = ("The `lb` attribute was deprecated in " - "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " - "Please use `results.variance` instead.") + wmsg = ( + "The `lb` attribute was deprecated in " + "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " + "Please use `results.variance` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.lb @property def lp(self): - wmsg = ("The `lp` attribute was deprecated in " - "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " - "Please use `results.variance` instead.") + wmsg = ( + "The `lp` attribute was deprecated in " + "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " + "Please use `results.variance` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.lp @property def fit(self): - wmsg = ("The `fit` attribute was deprecated in " - "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " - "Please use `results.variance` instead.") + wmsg = ( + "The `fit` attribute was deprecated in " + "MDAnalysis 2.0.0 and will be removed in MDAnalysis 3.0.0. " + "Please use `results.variance` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.fit @@ -309,13 +322,15 @@ def _perform_fit(self): self.results.bond_autocorrelation except AttributeError: raise NoDataError("Use the run method first") from None - self.results.x = self.results.lb *\ - np.arange(len(self.results.bond_autocorrelation)) + self.results.x = self.results.lb * np.arange( + len(self.results.bond_autocorrelation) + ) - self.results.lp = fit_exponential_decay(self.results.x, - self.results.bond_autocorrelation) + self.results.lp = fit_exponential_decay( + self.results.x, self.results.bond_autocorrelation + ) - self.results.fit = np.exp(-self.results.x/self.results.lp) + self.results.fit = np.exp(-self.results.x / self.results.lp) def plot(self, ax=None): """Visualize the results and fit @@ -330,20 +345,21 @@ def plot(self, ax=None): ax : the axis that the graph was plotted on """ import matplotlib.pyplot as plt + if ax is None: fig, ax = plt.subplots() - ax.plot(self.results.x, - self.results.bond_autocorrelation, - 'ro', - label='Result') - ax.plot(self.results.x, - self.results.fit, - label='Fit') - ax.set_xlabel(r'x') - ax.set_ylabel(r'$C(x)$') + ax.plot( + self.results.x, + self.results.bond_autocorrelation, + "ro", + label="Result", + ) + ax.plot(self.results.x, self.results.fit, label="Fit") + ax.set_xlabel(r"x") + ax.set_ylabel(r"$C(x)$") ax.set_xlim(0.0, 40 * self.results.lb) - ax.legend(loc='best') + ax.legend(loc="best") return ax @@ -368,8 +384,9 @@ def fit_exponential_decay(x, y): This function assumes that data starts at 1.0 and decays to 0.0 """ + def expfunc(x, a): - return np.exp(-x/a) + return np.exp(-x / a) a = scipy.optimize.curve_fit(expfunc, x, y)[0][0] diff --git a/package/MDAnalysis/analysis/psa.py b/package/MDAnalysis/analysis/psa.py index b93ea90c64b..853d6ecf622 100644 --- a/package/MDAnalysis/analysis/psa.py +++ b/package/MDAnalysis/analysis/psa.py @@ -64,8 +64,10 @@ ) -wmsg = ('Deprecation in version 2.8.0:\n' - 'MDAnalysis.analysis.psa is deprecated in favour of the MDAKit ' - 'PathSimAnalysis (https://github.com/MDAnalysis/PathSimAnalysis) ' - 'and will be removed in MDAnalysis version 3.0.0') +wmsg = ( + "Deprecation in version 2.8.0:\n" + "MDAnalysis.analysis.psa is deprecated in favour of the MDAKit " + "PathSimAnalysis (https://github.com/MDAnalysis/PathSimAnalysis) " + "and will be removed in MDAnalysis version 3.0.0" +) warnings.warn(wmsg, category=DeprecationWarning) diff --git a/package/MDAnalysis/analysis/rdf.py b/package/MDAnalysis/analysis/rdf.py index 891be116ca5..02ac014d47c 100644 --- a/package/MDAnalysis/analysis/rdf.py +++ b/package/MDAnalysis/analysis/rdf.py @@ -217,24 +217,30 @@ class InterRDF(AnalysisBase): of the `results` attribute of :class:`~MDAnalysis.analysis.AnalysisBase`. """ - def __init__(self, - g1, - g2, - nbins=75, - range=(0.0, 15.0), - norm="rdf", - exclusion_block=None, - exclude_same=None, - **kwargs): + + def __init__( + self, + g1, + g2, + nbins=75, + range=(0.0, 15.0), + norm="rdf", + exclusion_block=None, + exclude_same=None, + **kwargs, + ): super(InterRDF, self).__init__(g1.universe.trajectory, **kwargs) self.g1 = g1 self.g2 = g2 self.norm = str(norm).lower() - self.rdf_settings = {'bins': nbins, - 'range': range} + self.rdf_settings = {"bins": nbins, "range": range} self._exclusion_block = exclusion_block - if exclude_same is not None and exclude_same not in ['residue', 'segment', 'chain']: + if exclude_same is not None and exclude_same not in [ + "residue", + "segment", + "chain", + ]: raise ValueError( "The exclude_same argument to InterRDF must be None, 'residue', 'segment' " "or 'chain'." @@ -243,12 +249,18 @@ def __init__(self, raise ValueError( "The exclude_same argument to InterRDF cannot be used with exclusion_block." ) - name_to_attr = {'residue': 'resindices', 'segment': 'segindices', 'chain': 'chainIDs'} + name_to_attr = { + "residue": "resindices", + "segment": "segindices", + "chain": "chainIDs", + } self.exclude_same = name_to_attr.get(exclude_same) - if self.norm not in ['rdf', 'density', 'none']: - raise ValueError(f"'{self.norm}' is an invalid norm. " - "Use 'rdf', 'density' or 'none'.") + if self.norm not in ["rdf", "density", "none"]: + raise ValueError( + f"'{self.norm}' is an invalid norm. " + "Use 'rdf', 'density' or 'none'." + ) def _prepare(self): # Empty histogram to store the RDF @@ -263,17 +275,19 @@ def _prepare(self): # Cumulative volume for rdf normalization self.volume_cum = 0 # Set the max range to filter the search radius - self._maxrange = self.rdf_settings['range'][1] + self._maxrange = self.rdf_settings["range"][1] def _single_frame(self): - pairs, dist = distances.capped_distance(self.g1.positions, - self.g2.positions, - self._maxrange, - box=self._ts.dimensions) + pairs, dist = distances.capped_distance( + self.g1.positions, + self.g2.positions, + self._maxrange, + box=self._ts.dimensions, + ) # Maybe exclude same molecule distances if self._exclusion_block is not None: - idxA = pairs[:, 0]//self._exclusion_block[0] - idxB = pairs[:, 1]//self._exclusion_block[1] + idxA = pairs[:, 0] // self._exclusion_block[0] + idxB = pairs[:, 1] // self._exclusion_block[1] mask = np.where(idxA != idxB)[0] dist = dist[mask] @@ -295,7 +309,7 @@ def _conclude(self): if self.norm in ["rdf", "density"]: # Volume in each radial shell vols = np.power(self.results.edges, 3) - norm *= 4/3 * np.pi * np.diff(vols) + norm *= 4 / 3 * np.pi * np.diff(vols) if self.norm == "rdf": # Number of each selection @@ -317,33 +331,41 @@ def _conclude(self): @property def edges(self): - wmsg = ("The `edges` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.bins` instead") + wmsg = ( + "The `edges` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.bins` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.edges @property def count(self): - wmsg = ("The `count` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.bins` instead") + wmsg = ( + "The `count` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.bins` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.count @property def bins(self): - wmsg = ("The `bins` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.bins` instead") + wmsg = ( + "The `bins` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.bins` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.bins @property def rdf(self): - wmsg = ("The `rdf` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.rdf` instead") + wmsg = ( + "The `rdf` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.rdf` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.rdf @@ -540,54 +562,69 @@ class InterRDF_s(AnalysisBase): .. deprecated:: 2.3.0 The `universe` parameter is superflous. """ - def __init__(self, - u, - ags, - nbins=75, - range=(0.0, 15.0), - norm="rdf", - density=False, - **kwargs): - super(InterRDF_s, self).__init__(ags[0][0].universe.trajectory, - **kwargs) - - warnings.warn("The `u` attribute is superflous and will be removed " - "in MDAnalysis 3.0.0.", DeprecationWarning) + + def __init__( + self, + u, + ags, + nbins=75, + range=(0.0, 15.0), + norm="rdf", + density=False, + **kwargs, + ): + super(InterRDF_s, self).__init__( + ags[0][0].universe.trajectory, **kwargs + ) + + warnings.warn( + "The `u` attribute is superflous and will be removed " + "in MDAnalysis 3.0.0.", + DeprecationWarning, + ) self.ags = ags self.norm = str(norm).lower() - self.rdf_settings = {'bins': nbins, - 'range': range} + self.rdf_settings = {"bins": nbins, "range": range} - if self.norm not in ['rdf', 'density', 'none']: - raise ValueError(f"'{self.norm}' is an invalid norm. " - "Use 'rdf', 'density' or 'none'.") + if self.norm not in ["rdf", "density", "none"]: + raise ValueError( + f"'{self.norm}' is an invalid norm. " + "Use 'rdf', 'density' or 'none'." + ) if density: - warnings.warn("The `density` attribute was deprecated in " - "MDAnalysis 2.3.0 and will be removed in " - "MDAnalysis 3.0.0. Please use `norm=density` " - "instead.", DeprecationWarning) + warnings.warn( + "The `density` attribute was deprecated in " + "MDAnalysis 2.3.0 and will be removed in " + "MDAnalysis 3.0.0. Please use `norm=density` " + "instead.", + DeprecationWarning, + ) self.norm = "density" def _prepare(self): count, edges = np.histogram([-1], **self.rdf_settings) - self.results.count = [np.zeros((ag1.n_atoms, ag2.n_atoms, len(count)), - dtype=np.float64) for ag1, ag2 in self.ags] + self.results.count = [ + np.zeros((ag1.n_atoms, ag2.n_atoms, len(count)), dtype=np.float64) + for ag1, ag2 in self.ags + ] self.results.edges = edges self.results.bins = 0.5 * (edges[:-1] + edges[1:]) if self.norm == "rdf": # Cumulative volume for rdf normalization self.volume_cum = 0 - self._maxrange = self.rdf_settings['range'][1] + self._maxrange = self.rdf_settings["range"][1] def _single_frame(self): for i, (ag1, ag2) in enumerate(self.ags): - pairs, dist = distances.capped_distance(ag1.positions, - ag2.positions, - self._maxrange, - box=self._ts.dimensions) + pairs, dist = distances.capped_distance( + ag1.positions, + ag2.positions, + self._maxrange, + box=self._ts.dimensions, + ) for j, (idx1, idx2) in enumerate(pairs): count, _ = np.histogram(dist[j], **self.rdf_settings) @@ -601,7 +638,7 @@ def _conclude(self): if self.norm in ["rdf", "density"]: # Volume in each radial shell vols = np.power(self.results.edges, 3) - norm *= 4/3 * np.pi * np.diff(vols) + norm *= 4 / 3 * np.pi * np.diff(vols) if self.norm == "rdf": # Average number density @@ -639,40 +676,50 @@ def get_cdf(self): @property def edges(self): - wmsg = ("The `edges` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.bins` instead") + wmsg = ( + "The `edges` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.bins` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.edges @property def count(self): - wmsg = ("The `count` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.bins` instead") + wmsg = ( + "The `count` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.bins` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.count @property def bins(self): - wmsg = ("The `bins` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.bins` instead") + wmsg = ( + "The `bins` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.bins` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.bins @property def rdf(self): - wmsg = ("The `rdf` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.rdf` instead") + wmsg = ( + "The `rdf` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.rdf` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.rdf @property def cdf(self): - wmsg = ("The `cdf` attribute was deprecated in MDAnalysis 2.0.0 " - "and will be removed in MDAnalysis 3.0.0. Please use " - "`results.cdf` instead") + wmsg = ( + "The `cdf` attribute was deprecated in MDAnalysis 2.0.0 " + "and will be removed in MDAnalysis 3.0.0. Please use " + "`results.cdf` instead" + ) warnings.warn(wmsg, DeprecationWarning) return self.results.cdf diff --git a/package/MDAnalysis/analysis/results.py b/package/MDAnalysis/analysis/results.py index 8aa2062d2bc..7708f3dd881 100644 --- a/package/MDAnalysis/analysis/results.py +++ b/package/MDAnalysis/analysis/results.py @@ -48,6 +48,7 @@ assert r.masses == list((*r1.masses, *r2.masses)) assert (r.vectors == np.vstack([r1.vectors, r2.vectors])).all() """ + from collections import UserDict import numpy as np from typing import Callable, Sequence @@ -100,7 +101,9 @@ class in `scikit-learn`_. def _validate_key(self, key): if key in dir(self): - raise AttributeError(f"'{key}' is a protected dictionary attribute") + raise AttributeError( + f"'{key}' is a protected dictionary attribute" + ) elif isinstance(key, str) and not key.isidentifier(): raise ValueError(f"'{key}' is not a valid attribute") @@ -125,13 +128,17 @@ def __getattr__(self, attr): try: return self[attr] except KeyError as err: - raise AttributeError(f"'Results' object has no attribute '{attr}'") from err + raise AttributeError( + f"'Results' object has no attribute '{attr}'" + ) from err def __delattr__(self, attr): try: del self[attr] except KeyError as err: - raise AttributeError(f"'Results' object has no attribute '{attr}'") from err + raise AttributeError( + f"'Results' object has no attribute '{attr}'" + ) from err def __getstate__(self): return self.data @@ -166,7 +173,7 @@ class ResultsGroup: obj1 = Results(mass=1) obj2 = Results(mass=3) assert {'mass': 2.0} == group.merge([obj1, obj2]) - + .. code-block:: python @@ -182,10 +189,12 @@ class ResultsGroup: def __init__(self, lookup: dict[str, Callable] = None): self._lookup = lookup - def merge(self, objects: Sequence[Results], require_all_aggregators: bool = True) -> Results: - """Merge multiple Results into a single Results instance. + def merge( + self, objects: Sequence[Results], require_all_aggregators: bool = True + ) -> Results: + """Merge multiple Results into a single Results instance. - Merge multiple :class:`Results` instances into a single one, using the + Merge multiple :class:`Results` instances into a single one, using the `lookup` dictionary to determine the appropriate aggregator functions for each named results attribute. If the resulting object only contains a single element, it just returns it without using any aggregators. @@ -213,7 +222,7 @@ def merge(self, objects: Sequence[Results], require_all_aggregators: bool = True if len(objects) == 1: merged_results = objects[0] return merged_results - + merged_results = Results() for key in objects[0].keys(): agg_function = self._lookup.get(key, None) diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index f33d1b761fb..55a1322a75e 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -172,7 +172,7 @@ from ..lib.util import asiterable, iterable, get_weights -logger = logging.getLogger('MDAnalysis.analysis.rmsd') +logger = logging.getLogger("MDAnalysis.analysis.rmsd") def rmsd(a, b, weights=None, center=False, superposition=False): @@ -258,7 +258,7 @@ def rmsd(a, b, weights=None, center=False, superposition=False): b = np.asarray(b, dtype=np.float64) N = b.shape[0] if a.shape != b.shape: - raise ValueError('a and b must have same shape') + raise ValueError("a and b must have same shape") # superposition only works if structures are centered if center or superposition: @@ -269,7 +269,7 @@ def rmsd(a, b, weights=None, center=False, superposition=False): if weights is not None: if len(weights) != len(a): - raise ValueError('weights must have same length as a and b') + raise ValueError("weights must have same length as a and b") # weights are constructed as relative to the mean weights = np.asarray(weights, dtype=np.float64) / np.mean(weights) @@ -277,8 +277,7 @@ def rmsd(a, b, weights=None, center=False, superposition=False): return qcp.CalcRMSDRotationalMatrix(a, b, N, None, weights) else: if weights is not None: - return np.sqrt(np.sum(weights[:, np.newaxis] - * ((a - b) ** 2)) / N) + return np.sqrt(np.sum(weights[:, np.newaxis] * ((a - b) ** 2)) / N) else: return np.sqrt(np.sum((a - b) ** 2) / N) @@ -307,28 +306,29 @@ def process_selection(select): """ if isinstance(select, str): - select = {'reference': str(select), 'mobile': str(select)} + select = {"reference": str(select), "mobile": str(select)} elif type(select) is tuple: try: - select = {'mobile': select[0], 'reference': select[1]} + select = {"mobile": select[0], "reference": select[1]} except IndexError: raise IndexError( "select must contain two selection strings " - "(reference, mobile)") from None + "(reference, mobile)" + ) from None elif type(select) is dict: # compatability hack to use new nomenclature try: - select['mobile'] - select['reference'] + select["mobile"] + select["reference"] except KeyError: raise KeyError( - "select dictionary must contain entries for keys " - "'mobile' and 'reference'." - ) from None + "select dictionary must contain entries for keys " + "'mobile' and 'reference'." + ) from None else: raise TypeError("'select' must be either a string, 2-tuple, or dict") - select['mobile'] = asiterable(select['mobile']) - select['reference'] = asiterable(select['reference']) + select["mobile"] = asiterable(select["mobile"]) + select["reference"] = asiterable(select["reference"]) return select @@ -362,16 +362,29 @@ class RMSD(AnalysisBase): 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, reference=None, select='all', - groupselections=None, weights=None, weights_groupselections=False, - tol_mass=0.1, ref_frame=0, **kwargs): + return ( + "serial", + "multiprocessing", + "dask", + ) + + def __init__( + self, + atomgroup, + reference=None, + select="all", + groupselections=None, + weights=None, + weights_groupselections=False, + tol_mass=0.1, + ref_frame=0, + **kwargs, + ): r"""Parameters ---------- atomgroup : AtomGroup or Universe @@ -512,49 +525,67 @@ def __init__(self, atomgroup, reference=None, select='all', `filename` keyword was removed. """ - super(RMSD, self).__init__(atomgroup.universe.trajectory, - **kwargs) + super(RMSD, self).__init__(atomgroup.universe.trajectory, **kwargs) self.atomgroup = atomgroup self.reference = reference if reference is not None else self.atomgroup select = process_selection(select) - self.groupselections = ([process_selection(s) for s in groupselections] - if groupselections is not None else []) + self.groupselections = ( + [process_selection(s) for s in groupselections] + if groupselections is not None + else [] + ) self.weights = weights self.tol_mass = tol_mass self.ref_frame = ref_frame self.weights_groupselections = weights_groupselections - self.ref_atoms = self.reference.select_atoms(*select['reference']) - self.mobile_atoms = self.atomgroup.select_atoms(*select['mobile']) + self.ref_atoms = self.reference.select_atoms(*select["reference"]) + self.mobile_atoms = self.atomgroup.select_atoms(*select["mobile"]) if len(self.ref_atoms) != len(self.mobile_atoms): - err = ("Reference and trajectory atom selections do " - "not contain the same number of atoms: " - "N_ref={0:d}, N_traj={1:d}".format(self.ref_atoms.n_atoms, - self.mobile_atoms.n_atoms)) + err = ( + "Reference and trajectory atom selections do " + "not contain the same number of atoms: " + "N_ref={0:d}, N_traj={1:d}".format( + self.ref_atoms.n_atoms, self.mobile_atoms.n_atoms + ) + ) logger.exception(err) raise SelectionError(err) - logger.info("RMS calculation " - "for {0:d} atoms.".format(len(self.ref_atoms))) - mass_mismatches = (np.absolute((self.ref_atoms.masses - - self.mobile_atoms.masses)) > - self.tol_mass) + logger.info( + "RMS calculation " "for {0:d} atoms.".format(len(self.ref_atoms)) + ) + mass_mismatches = ( + np.absolute((self.ref_atoms.masses - self.mobile_atoms.masses)) + > self.tol_mass + ) if np.any(mass_mismatches): # diagnostic output: logger.error("Atoms: reference | mobile") for ar, at in zip(self.ref_atoms, self.mobile_atoms): if ar.name != at.name: - logger.error("{0!s:>4} {1:3d} {2!s:>3} {3!s:>3} {4:6.3f}" - "| {5!s:>4} {6:3d} {7!s:>3} {8!s:>3}" - "{9:6.3f}".format(ar.segid, ar.resid, - ar.resname, ar.name, - ar.mass, at.segid, at.resid, - at.resname, at.name, - at.mass)) - errmsg = ("Inconsistent selections, masses differ by more than" - "{0:f}; mis-matching atoms" - "are shown above.".format(self.tol_mass)) + logger.error( + "{0!s:>4} {1:3d} {2!s:>3} {3!s:>3} {4:6.3f}" + "| {5!s:>4} {6:3d} {7!s:>3} {8!s:>3}" + "{9:6.3f}".format( + ar.segid, + ar.resid, + ar.resname, + ar.name, + ar.mass, + at.segid, + at.resid, + at.resname, + at.name, + at.mass, + ) + ) + errmsg = ( + "Inconsistent selections, masses differ by more than" + "{0:f}; mis-matching atoms" + "are shown above.".format(self.tol_mass) + ) logger.error(errmsg) raise SelectionError(errmsg) del mass_mismatches @@ -565,27 +596,38 @@ def __init__(self, atomgroup, reference=None, select='all', # *groupselections* groups each a dict with reference/mobile self._groupselections_atoms = [ { - 'reference': self.reference.universe.select_atoms(*s['reference']), - 'mobile': self.atomgroup.universe.select_atoms(*s['mobile']), + "reference": self.reference.universe.select_atoms( + *s["reference"] + ), + "mobile": self.atomgroup.universe.select_atoms(*s["mobile"]), } - for s in self.groupselections] + for s in self.groupselections + ] # sanity check - for igroup, (sel, atoms) in enumerate(zip(self.groupselections, - self._groupselections_atoms)): - if len(atoms['mobile']) != len(atoms['reference']): - logger.exception('SelectionError: Group Selection') + for igroup, (sel, atoms) in enumerate( + zip(self.groupselections, self._groupselections_atoms) + ): + if len(atoms["mobile"]) != len(atoms["reference"]): + logger.exception("SelectionError: Group Selection") raise SelectionError( "Group selection {0}: {1} | {2}: Reference and trajectory " "atom selections do not contain the same number of atoms: " "N_ref={3}, N_traj={4}".format( - igroup, sel['reference'], sel['mobile'], - len(atoms['reference']), len(atoms['mobile']))) + igroup, + sel["reference"], + sel["mobile"], + len(atoms["reference"]), + len(atoms["mobile"]), + ) + ) # check weights type - acceptable_dtypes = (np.dtype('float64'), np.dtype('int64')) - msg = ("weights should only be 'mass', None or 1D float array." - "For weights on groupselections, " - "use **weight_groupselections**") + acceptable_dtypes = (np.dtype("float64"), np.dtype("int64")) + msg = ( + "weights should only be 'mass', None or 1D float array." + "For weights on groupselections, " + "use **weight_groupselections**" + ) if iterable(self.weights): element_lens = [] @@ -605,42 +647,57 @@ def __init__(self, atomgroup, reference=None, select='all', if self.weights_groupselections: if len(self.weights_groupselections) != len(self.groupselections): - raise ValueError("Length of weights_groupselections is not equal to " - "length of groupselections ") - for weights, atoms, selection in zip(self.weights_groupselections, - self._groupselections_atoms, - self.groupselections): + raise ValueError( + "Length of weights_groupselections is not equal to " + "length of groupselections " + ) + for weights, atoms, selection in zip( + self.weights_groupselections, + self._groupselections_atoms, + self.groupselections, + ): try: if iterable(weights) or weights != "mass": - get_weights(atoms['mobile'], weights) + get_weights(atoms["mobile"], weights) except Exception as e: - raise type(e)(str(e) + ' happens in selection %s' % selection['mobile']) - + raise type(e)( + str(e) + + " happens in selection %s" % selection["mobile"] + ) def _prepare(self): self._n_atoms = self.mobile_atoms.n_atoms if not self.weights_groupselections: - if not iterable(self.weights): # apply 'mass' or 'None' to groupselections - self.weights_groupselections = [self.weights] * len(self.groupselections) + if not iterable( + self.weights + ): # apply 'mass' or 'None' to groupselections + self.weights_groupselections = [self.weights] * len( + self.groupselections + ) else: - self.weights_groupselections = [None] * len(self.groupselections) - - for igroup, (weights, atoms) in enumerate(zip(self.weights_groupselections, - self._groupselections_atoms)): - if str(weights) == 'mass': - self.weights_groupselections[igroup] = atoms['mobile'].masses + self.weights_groupselections = [None] * len( + self.groupselections + ) + + for igroup, (weights, atoms) in enumerate( + zip(self.weights_groupselections, self._groupselections_atoms) + ): + if str(weights) == "mass": + self.weights_groupselections[igroup] = atoms["mobile"].masses if weights is not None: - self.weights_groupselections[igroup] = np.asarray(self.weights_groupselections[igroup], - dtype=np.float64) / \ - np.mean(self.weights_groupselections[igroup]) + self.weights_groupselections[igroup] = np.asarray( + self.weights_groupselections[igroup], dtype=np.float64 + ) / np.mean(self.weights_groupselections[igroup]) # add the array of weights to weights_select self.weights_select = get_weights(self.mobile_atoms, self.weights) self.weights_ref = get_weights(self.ref_atoms, self.weights) if self.weights_select is not None: - self.weights_select = np.asarray(self.weights_select, dtype=np.float64) / \ - np.mean(self.weights_select) - self.weights_ref = np.asarray(self.weights_ref, dtype=np.float64) / \ - np.mean(self.weights_ref) + self.weights_select = np.asarray( + self.weights_select, dtype=np.float64 + ) / np.mean(self.weights_select) + self.weights_ref = np.asarray( + self.weights_ref, dtype=np.float64 + ) / np.mean(self.weights_ref) current_frame = self.reference.universe.trajectory.ts.frame @@ -653,10 +710,14 @@ def _prepare(self): # makes a copy self._ref_coordinates = self.ref_atoms.positions - self._ref_com if self._groupselections_atoms: - self._groupselections_ref_coords64 = [(self.reference. - select_atoms(*s['reference']). - positions.astype(np.float64)) for s in - self.groupselections] + self._groupselections_ref_coords64 = [ + ( + self.reference.select_atoms( + *s["reference"] + ).positions.astype(np.float64) + ) + for s in self.groupselections + ] finally: # Move back to the original frame self.reference.universe.trajectory[current_frame] @@ -674,20 +735,28 @@ def _prepare(self): else: self._rot = None - self.results.rmsd = np.zeros((self.n_frames, - 3 + len(self._groupselections_atoms))) + self.results.rmsd = np.zeros( + (self.n_frames, 3 + len(self._groupselections_atoms)) + ) - self._mobile_coordinates64 = self.mobile_atoms.positions.copy().astype(np.float64) + self._mobile_coordinates64 = self.mobile_atoms.positions.copy().astype( + np.float64 + ) def _get_aggregator(self): - return ResultsGroup(lookup={'rmsd': ResultsGroup.ndarray_vstack}) + return ResultsGroup(lookup={"rmsd": ResultsGroup.ndarray_vstack}) def _single_frame(self): - mobile_com = self.mobile_atoms.center(self.weights_select).astype(np.float64) + mobile_com = self.mobile_atoms.center(self.weights_select).astype( + np.float64 + ) self._mobile_coordinates64[:] = self.mobile_atoms.positions self._mobile_coordinates64 -= mobile_com - self.results.rmsd[self._frame_index, :2] = self._ts.frame, self._trajectory.time + self.results.rmsd[self._frame_index, :2] = ( + self._ts.frame, + self._trajectory.time, + ) if self._groupselections_atoms: # superimpose structures: MDAnalysis qcprot needs Nx3 coordinate @@ -696,9 +765,15 @@ def _single_frame(self): # left** so that we can easily use broadcasting and save one # expensive numpy transposition. - self.results.rmsd[self._frame_index, 2] = qcp.CalcRMSDRotationalMatrix( - self._ref_coordinates64, self._mobile_coordinates64, - self._n_atoms, self._rot, self.weights_select) + self.results.rmsd[self._frame_index, 2] = ( + qcp.CalcRMSDRotationalMatrix( + self._ref_coordinates64, + self._mobile_coordinates64, + self._n_atoms, + self._rot, + self.weights_select, + ) + ) self._R[:, :] = self._rot.reshape(3, 3) # Transform each atom in the trajectory (use inplace ops to @@ -713,24 +788,39 @@ def _single_frame(self): # 2) calculate secondary RMSDs (without any further # superposition) for igroup, (refpos, atoms) in enumerate( - zip(self._groupselections_ref_coords64, - self._groupselections_atoms), 3): + zip( + self._groupselections_ref_coords64, + self._groupselections_atoms, + ), + 3, + ): self.results.rmsd[self._frame_index, igroup] = rmsd( - refpos, atoms['mobile'].positions, - weights=self.weights_groupselections[igroup-3], - center=False, superposition=False) + refpos, + atoms["mobile"].positions, + weights=self.weights_groupselections[igroup - 3], + center=False, + superposition=False, + ) else: # only calculate RMSD by setting the Rmatrix to None (no need # to carry out the rotation as we already get the optimum RMSD) - self.results.rmsd[self._frame_index, 2] = qcp.CalcRMSDRotationalMatrix( - self._ref_coordinates64, self._mobile_coordinates64, - self._n_atoms, None, self.weights_select) + self.results.rmsd[self._frame_index, 2] = ( + qcp.CalcRMSDRotationalMatrix( + self._ref_coordinates64, + self._mobile_coordinates64, + self._n_atoms, + None, + self.weights_select, + ) + ) @property def rmsd(self): - wmsg = ("The `rmsd` attribute was deprecated in MDAnalysis 2.0.0 and " - "will be removed in MDAnalysis 3.0.0. Please use " - "`results.rmsd` instead.") + wmsg = ( + "The `rmsd` attribute was deprecated in MDAnalysis 2.0.0 and " + "will be removed in MDAnalysis 3.0.0. Please use " + "`results.rmsd` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.rmsd @@ -754,7 +844,7 @@ class RMSF(AnalysisBase): @classmethod def get_supported_backends(cls): - return ('serial',) + return ("serial",) def __init__(self, atomgroup, **kwargs): r"""Parameters @@ -885,7 +975,9 @@ def _prepare(self): def _single_frame(self): k = self._frame_index - self.sumsquares += (k / (k+1.0)) * (self.atomgroup.positions - self.mean) ** 2 + self.sumsquares += (k / (k + 1.0)) * ( + self.atomgroup.positions - self.mean + ) ** 2 self.mean = (k * self.mean + self.atomgroup.positions) / (k + 1) def _conclude(self): @@ -893,13 +985,17 @@ def _conclude(self): self.results.rmsf = np.sqrt(self.sumsquares.sum(axis=1) / (k + 1)) if not (self.results.rmsf >= 0).all(): - raise ValueError("Some RMSF values negative; overflow " + - "or underflow occurred") + raise ValueError( + "Some RMSF values negative; overflow " + + "or underflow occurred" + ) @property def rmsf(self): - wmsg = ("The `rmsf` attribute was deprecated in MDAnalysis 2.0.0 and " - "will be removed in MDAnalysis 3.0.0. Please use " - "`results.rmsd` instead.") + wmsg = ( + "The `rmsf` attribute was deprecated in MDAnalysis 2.0.0 and " + "will be removed in MDAnalysis 3.0.0. Please use " + "`results.rmsd` instead." + ) warnings.warn(wmsg, DeprecationWarning) return self.results.rmsf diff --git a/package/MDAnalysis/analysis/waterdynamics.py b/package/MDAnalysis/analysis/waterdynamics.py index 2c7a1c4bec3..aaeb1b296f3 100644 --- a/package/MDAnalysis/analysis/waterdynamics.py +++ b/package/MDAnalysis/analysis/waterdynamics.py @@ -49,8 +49,10 @@ ) -wmsg = ("Deprecation in version 2.8.0\n" - "MDAnalysis.analysis.waterdynamics is deprecated in favour of the " - "MDAKit waterdynamics (https://www.mdanalysis.org/waterdynamics/) " - "and will be removed in MDAnalysis version 3.0.0") +wmsg = ( + "Deprecation in version 2.8.0\n" + "MDAnalysis.analysis.waterdynamics is deprecated in favour of the " + "MDAKit waterdynamics (https://www.mdanalysis.org/waterdynamics/) " + "and will be removed in MDAnalysis version 3.0.0" +) warnings.warn(wmsg, category=DeprecationWarning) diff --git a/package/pyproject.toml b/package/pyproject.toml index 19d07228e7f..ae0d34422dc 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -135,6 +135,7 @@ tables\.py | MDAnalysis/visualization/.*\.py | MDAnalysis/lib/.*\.py^ | MDAnalysis/transformations/.*\.py +| MDAnalysis/analysis/.*\.py | MDAnalysis/guesser/.*\.py | MDAnalysis/converters/.*\.py | MDAnalysis/coordinates/.*\.py diff --git a/testsuite/MDAnalysisTests/analysis/conftest.py b/testsuite/MDAnalysisTests/analysis/conftest.py index df17c05c064..5848c503165 100644 --- a/testsuite/MDAnalysisTests/analysis/conftest.py +++ b/testsuite/MDAnalysisTests/analysis/conftest.py @@ -47,107 +47,116 @@ def params_for_cls(cls, exclude: list[str] = None): ] params = [ - pytest.param({ - "backend": backend, - "n_workers": nproc - }, ) for backend in installed_backends for nproc in (2, ) + pytest.param( + {"backend": backend, "n_workers": nproc}, + ) + for backend in installed_backends + for nproc in (2,) if backend != "serial" ] params.extend([{"backend": "serial"}]) return params -@pytest.fixture(scope='module', params=params_for_cls(FrameAnalysis)) +@pytest.fixture(scope="module", params=params_for_cls(FrameAnalysis)) def client_FrameAnalysis(request): return request.param -@pytest.fixture(scope='module', params=params_for_cls(AnalysisBase)) +@pytest.fixture(scope="module", params=params_for_cls(AnalysisBase)) def client_AnalysisBase(request): return request.param -@pytest.fixture(scope='module', params=params_for_cls(AnalysisFromFunction)) +@pytest.fixture(scope="module", params=params_for_cls(AnalysisFromFunction)) def client_AnalysisFromFunction(request): return request.param -@pytest.fixture(scope='module', - params=params_for_cls(AnalysisFromFunction, - exclude=['multiprocessing'])) +@pytest.fixture( + scope="module", + params=params_for_cls(AnalysisFromFunction, exclude=["multiprocessing"]), +) def client_AnalysisFromFunctionAnalysisClass(request): return request.param -@pytest.fixture(scope='module', params=params_for_cls(IncompleteAnalysis)) +@pytest.fixture(scope="module", params=params_for_cls(IncompleteAnalysis)) def client_IncompleteAnalysis(request): return request.param -@pytest.fixture(scope='module', params=params_for_cls(OldAPIAnalysis)) +@pytest.fixture(scope="module", params=params_for_cls(OldAPIAnalysis)) def client_OldAPIAnalysis(request): return request.param # MDAnalysis.analysis.rms -@pytest.fixture(scope='module', params=params_for_cls(RMSD)) + +@pytest.fixture(scope="module", params=params_for_cls(RMSD)) def client_RMSD(request): return request.param -@pytest.fixture(scope='module', params=params_for_cls(RMSF)) +@pytest.fixture(scope="module", params=params_for_cls(RMSF)) def client_RMSF(request): return request.param # MDAnalysis.analysis.dihedrals -@pytest.fixture(scope='module', params=params_for_cls(Dihedral)) + +@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)) +@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)) +@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)) + + +@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)) + +@pytest.fixture(scope="module", params=params_for_cls(BAT)) 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)) + + +@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 @@ -155,6 +164,7 @@ def client_NucPairDist(request): # MDAnalysis.analysis.contacts + @pytest.fixture(scope="module", params=params_for_cls(Contacts)) def client_Contacts(request): return request.param @@ -162,6 +172,7 @@ def client_Contacts(request): # MDAnalysis.analysis.density -@pytest.fixture(scope='module', params=params_for_cls(DensityAnalysis)) + +@pytest.fixture(scope="module", params=params_for_cls(DensityAnalysis)) def client_DensityAnalysis(request): return request.param diff --git a/testsuite/MDAnalysisTests/analysis/test_align.py b/testsuite/MDAnalysisTests/analysis/test_align.py index 31455198bec..fbda36b1580 100644 --- a/testsuite/MDAnalysisTests/analysis/test_align.py +++ b/testsuite/MDAnalysisTests/analysis/test_align.py @@ -31,15 +31,23 @@ import pytest from MDAnalysis import SelectionError, SelectionWarning from MDAnalysisTests import executable_not_found -from MDAnalysisTests.datafiles import (PSF, DCD, CRD, FASTA, ALIGN_BOUND, - ALIGN_UNBOUND, PDB_helix) +from MDAnalysisTests.datafiles import ( + PSF, + DCD, + CRD, + FASTA, + ALIGN_BOUND, + ALIGN_UNBOUND, + PDB_helix, +) from numpy.testing import ( assert_equal, assert_array_equal, assert_allclose, ) -#Function for Parametrizing conditional raising + +# Function for Parametrizing conditional raising @contextmanager def does_not_raise(): yield @@ -50,10 +58,13 @@ class TestRotationMatrix: b = np.array([[0.1, 0.1, 0.1], [1.1, 1.1, 1.1]]) w = np.array([1.3, 2.3]) - @pytest.mark.parametrize('a, b, weights, expected', ( + @pytest.mark.parametrize( + "a, b, weights, expected", + ( (a, b, None, 0.15785647734415692), (a, b, w, 0.13424643502242328), - )) + ), + ) def test_rotation_matrix_input(self, a, b, weights, expected): rot, rmsd = align.rotation_matrix(a, b, weights) assert_equal(rot, np.eye(3)) @@ -68,11 +79,8 @@ def test_list_args(self): assert rmsd == pytest.approx(0.13424643502242328) def test_exception(self): - a = [[0.1, 0.2, 0.3], - [1.1, 1.1, 1.1], - [2, 2, 2]] - b = [[0.1, 0.1, 0.1], - [1.1, 1.1, 1.1]] + a = [[0.1, 0.2, 0.3], [1.1, 1.1, 1.1], [2, 2, 2]] + b = [[0.1, 0.1, 0.1], [1.1, 1.1, 1.1]] with pytest.raises(ValueError): align.rotation_matrix(a, b) @@ -91,20 +99,23 @@ def reference(): @staticmethod @pytest.fixture() def reference_small(reference): - return mda.Merge(reference.select_atoms( - "not name H* and not atom 4AKE 1 CA")) + return mda.Merge( + reference.select_atoms("not name H* and not atom 4AKE 1 CA") + ) @pytest.mark.parametrize("strict", (True, False)) - def test_match(self, universe, reference, strict, - selection="protein and backbone"): + def test_match( + self, universe, reference, strict, selection="protein and backbone" + ): ref = reference.select_atoms(selection) mobile = universe.select_atoms(selection) groups = align.get_matching_atoms(ref, mobile, strict=strict) assert_equal(groups[0].names, groups[1].names) @pytest.mark.parametrize("strict", (True, False)) - def test_nomatch_atoms_raise(self, universe, reference, - strict, selection="protein and backbone"): + def test_nomatch_atoms_raise( + self, universe, reference, strict, selection="protein and backbone" + ): # one atom less but same residues; with strict=False should try # to get selections (but current code fails, so we also raise SelectionError) ref = reference.select_atoms(selection).atoms[1:] @@ -115,11 +126,18 @@ def test_nomatch_atoms_raise(self, universe, reference, else: with pytest.warns(SelectionWarning): with pytest.raises(SelectionError): - groups = align.get_matching_atoms(ref, mobile, strict=strict) + groups = align.get_matching_atoms( + ref, mobile, strict=strict + ) @pytest.mark.parametrize("strict", (True, False)) - def test_nomatch_residues_raise_empty(self, universe, reference_small, - strict, selection="protein and backbone"): + def test_nomatch_residues_raise_empty( + self, + universe, + reference_small, + strict, + selection="protein and backbone", + ): # one atom less and all residues different: will currently create # empty selections with strict=False, see also # https://gist.github.com/orbeckst/2686badcd15031e6c946baf9164a683d @@ -131,55 +149,78 @@ def test_nomatch_residues_raise_empty(self, universe, reference_small, else: with pytest.warns(SelectionWarning): with pytest.raises(SelectionError): - groups = align.get_matching_atoms(ref, mobile, strict=strict) + groups = align.get_matching_atoms( + ref, mobile, strict=strict + ) def test_toggle_atom_mismatch_default_error(self, universe, reference): - selection = ('resname ALA and name CA', 'resname ALA and name O') + selection = ("resname ALA and name CA", "resname ALA and name O") with pytest.raises(SelectionError): rmsd = align.alignto(universe, reference, select=selection) def test_toggle_atom_mismatch_kwarg_error(self, universe, reference): - selection = ('resname ALA and name CA', 'resname ALA and name O') + selection = ("resname ALA and name CA", "resname ALA and name O") with pytest.raises(SelectionError): - rmsd = align.alignto(universe, reference, select=selection, match_atoms=True) + rmsd = align.alignto( + universe, reference, select=selection, match_atoms=True + ) def test_toggle_atom_nomatch(self, universe, reference): - selection = ('resname ALA and name CA', 'resname ALA and name O') - rmsd = align.alignto(universe, reference, select=selection, match_atoms=False) + selection = ("resname ALA and name CA", "resname ALA and name O") + rmsd = align.alignto( + universe, reference, select=selection, match_atoms=False + ) assert rmsd[0] > 0.01 def test_toggle_atom_nomatch_mismatch_atoms(self, universe, reference): # mismatching number of atoms, but same number of residues - u = universe.select_atoms('resname ALA and name CA') - u += universe.select_atoms('resname ALA and name O')[-1] - ref = reference.select_atoms('resname ALA and name CA') + u = universe.select_atoms("resname ALA and name CA") + u += universe.select_atoms("resname ALA and name O")[-1] + ref = reference.select_atoms("resname ALA and name CA") with pytest.raises(SelectionError): - align.alignto(u, ref, select='all', match_atoms=False) - - @pytest.mark.parametrize('subselection, expectation', [ - ('resname ALA and name CA', does_not_raise()), - (mda.Universe(PSF, DCD).select_atoms('resname ALA and name CA'), does_not_raise()), - (1234, pytest.raises(TypeError)), - ]) - def test_subselection_alignto(self, universe, reference, subselection, expectation): + align.alignto(u, ref, select="all", match_atoms=False) + + @pytest.mark.parametrize( + "subselection, expectation", + [ + ("resname ALA and name CA", does_not_raise()), + ( + mda.Universe(PSF, DCD).select_atoms("resname ALA and name CA"), + does_not_raise(), + ), + (1234, pytest.raises(TypeError)), + ], + ) + def test_subselection_alignto( + self, universe, reference, subselection, expectation + ): with expectation: - rmsd = align.alignto(universe, reference, subselection=subselection) + rmsd = align.alignto( + universe, reference, subselection=subselection + ) assert_allclose(rmsd[1], 0.0, rtol=0, atol=1.5e-9) def test_no_atom_masses(self, universe): - #if no masses are present - u = mda.Universe.empty(6, 2, atom_resindex=[0, 0, 0, 1, 1, 1], trajectory=True) + # if no masses are present + u = mda.Universe.empty( + 6, 2, atom_resindex=[0, 0, 0, 1, 1, 1], trajectory=True + ) with pytest.warns(SelectionWarning): align.get_matching_atoms(u.atoms, u.atoms) def test_one_universe_has_masses(self, universe): - u = mda.Universe.empty(6, 2, atom_resindex=[0, 0, 0, 1, 1, 1], trajectory=True) - ref = mda.Universe.empty(6, 2, atom_resindex=[0, 0, 0, 1, 1, 1], trajectory=True) - ref.add_TopologyAttr('masses') + u = mda.Universe.empty( + 6, 2, atom_resindex=[0, 0, 0, 1, 1, 1], trajectory=True + ) + ref = mda.Universe.empty( + 6, 2, atom_resindex=[0, 0, 0, 1, 1, 1], trajectory=True + ) + ref.add_TopologyAttr("masses") with pytest.warns(SelectionWarning): align.get_matching_atoms(u.atoms, ref.atoms) + class TestAlign(object): @staticmethod @pytest.fixture() @@ -193,45 +234,62 @@ def reference(): def test_rmsd(self, universe, reference): universe.trajectory[0] # ensure first frame - bb = universe.select_atoms('backbone') + bb = universe.select_atoms("backbone") first_frame = bb.positions universe.trajectory[-1] last_frame = bb.positions - assert_allclose(rms.rmsd(first_frame, first_frame), 0.0, rtol=0, atol=1.5e-5, - err_msg="error: rmsd(X,X) should be 0") + assert_allclose( + rms.rmsd(first_frame, first_frame), + 0.0, + rtol=0, + atol=1.5e-5, + err_msg="error: rmsd(X,X) should be 0", + ) # rmsd(A,B) = rmsd(B,A) should be exact but spurious failures in the # 9th decimal have been observed (see Issue 57 comment #1) so we relax # the test to 6 decimals. rmsd = rms.rmsd(first_frame, last_frame, superposition=True) assert_allclose( - rms.rmsd(last_frame, first_frame, superposition=True), rmsd, rtol=0, atol=1.5e-6, - err_msg="error: rmsd() is not symmetric") - assert_allclose(rmsd, 6.820321761927005, rtol=0, atol=1.5e-5, - err_msg="RMSD calculation between 1st and last AdK frame gave wrong answer") + rms.rmsd(last_frame, first_frame, superposition=True), + rmsd, + rtol=0, + atol=1.5e-6, + err_msg="error: rmsd() is not symmetric", + ) + assert_allclose( + rmsd, + 6.820321761927005, + rtol=0, + atol=1.5e-5, + err_msg="RMSD calculation between 1st and last AdK frame gave wrong answer", + ) # test masses as weights last_atoms_weight = universe.atoms.masses A = universe.trajectory[0] B = reference.trajectory[-1] - rmsd = align.alignto(universe, reference, weights='mass') - rmsd_sup_weight = rms.rmsd(A, B, weights=last_atoms_weight, center=True, - superposition=True) + rmsd = align.alignto(universe, reference, weights="mass") + rmsd_sup_weight = rms.rmsd( + A, B, weights=last_atoms_weight, center=True, superposition=True + ) assert_allclose(rmsd[1], rmsd_sup_weight, rtol=0, atol=1.5e-6) def test_rmsd_custom_mass_weights(self, universe, reference): last_atoms_weight = universe.atoms.masses A = universe.trajectory[0] B = reference.trajectory[-1] - rmsd = align.alignto(universe, reference, - weights=reference.atoms.masses) - rmsd_sup_weight = rms.rmsd(A, B, weights=last_atoms_weight, center=True, - superposition=True) + rmsd = align.alignto( + universe, reference, weights=reference.atoms.masses + ) + rmsd_sup_weight = rms.rmsd( + A, B, weights=last_atoms_weight, center=True, superposition=True + ) assert_allclose(rmsd[1], rmsd_sup_weight, rtol=0, atol=1.5e-6) def test_rmsd_custom_weights(self, universe, reference): weights = np.zeros(universe.atoms.n_atoms) - ca = universe.select_atoms('name CA') + ca = universe.select_atoms("name CA") weights[ca.indices] = 1 - rmsd = align.alignto(universe, reference, select='name CA') + rmsd = align.alignto(universe, reference, select="name CA") rmsd_weights = align.alignto(universe, reference, weights=weights) assert_allclose(rmsd[1], rmsd_weights[1], rtol=0, atol=1.5e-6) @@ -240,19 +298,23 @@ def test_AlignTraj_outfile_default(self, universe, reference, tmpdir): reference.trajectory[-1] x = align.AlignTraj(universe, reference) try: - assert os.path.basename(x.filename) == 'rmsfit_adk_dims.dcd' + assert os.path.basename(x.filename) == "rmsfit_adk_dims.dcd" finally: x._writer.close() - def test_AlignTraj_outfile_default_exists(self, universe, reference, tmpdir): + def test_AlignTraj_outfile_default_exists( + self, universe, reference, tmpdir + ): reference.trajectory[-1] - outfile = str(tmpdir.join('align_test.dcd')) + outfile = str(tmpdir.join("align_test.dcd")) align.AlignTraj(universe, reference, filename=outfile).run() fitted = mda.Universe(PSF, outfile) # ensure default file exists - with mda.Writer(str(tmpdir.join("rmsfit_align_test.dcd")), - n_atoms=fitted.atoms.n_atoms) as w: + with mda.Writer( + str(tmpdir.join("rmsfit_align_test.dcd")), + n_atoms=fitted.atoms.n_atoms, + ) as w: w.write(fitted.atoms) with tmpdir.as_cwd(): @@ -264,13 +326,13 @@ def test_AlignTraj_outfile_default_exists(self, universe, reference, tmpdir): def test_AlignTraj_step_works(self, universe, reference, tmpdir): reference.trajectory[-1] - outfile = str(tmpdir.join('align_test.dcd')) + outfile = str(tmpdir.join("align_test.dcd")) # this shouldn't throw an exception align.AlignTraj(universe, reference, filename=outfile).run(step=10) def test_AlignTraj_deprecated_attribute(self, universe, reference, tmpdir): reference.trajectory[-1] - outfile = str(tmpdir.join('align_test.dcd')) + outfile = str(tmpdir.join("align_test.dcd")) x = align.AlignTraj(universe, reference, filename=outfile).run(stop=2) wmsg = "The `rmsd` attribute was deprecated in MDAnalysis 2.0.0" @@ -279,7 +341,7 @@ def test_AlignTraj_deprecated_attribute(self, universe, reference, tmpdir): def test_AlignTraj(self, universe, reference, tmpdir): reference.trajectory[-1] - outfile = str(tmpdir.join('align_test.dcd')) + outfile = str(tmpdir.join("align_test.dcd")) x = align.AlignTraj(universe, reference, filename=outfile).run() fitted = mda.Universe(PSF, outfile) @@ -293,58 +355,84 @@ def test_AlignTraj(self, universe, reference, tmpdir): self._assert_rmsd(reference, fitted, -1, 0.0) def test_AlignTraj_weighted(self, universe, reference, tmpdir): - outfile = str(tmpdir.join('align_test.dcd')) - x = align.AlignTraj(universe, reference, - filename=outfile, weights='mass').run() + outfile = str(tmpdir.join("align_test.dcd")) + x = align.AlignTraj( + universe, reference, filename=outfile, weights="mass" + ).run() fitted = mda.Universe(PSF, outfile) assert_allclose(x.results.rmsd[0], 0, rtol=0, atol=1.5e-3) assert_allclose(x.results.rmsd[-1], 6.9033, rtol=0, atol=1.5e-3) - self._assert_rmsd(reference, fitted, 0, 0.0, - weights=universe.atoms.masses) - self._assert_rmsd(reference, fitted, -1, 6.929083032629219, - weights=universe.atoms.masses) + self._assert_rmsd( + reference, fitted, 0, 0.0, weights=universe.atoms.masses + ) + self._assert_rmsd( + reference, + fitted, + -1, + 6.929083032629219, + weights=universe.atoms.masses, + ) def test_AlignTraj_custom_weights(self, universe, reference, tmpdir): weights = np.zeros(universe.atoms.n_atoms) - ca = universe.select_atoms('name CA') + ca = universe.select_atoms("name CA") weights[ca.indices] = 1 - outfile = str(tmpdir.join('align_test.dcd')) + outfile = str(tmpdir.join("align_test.dcd")) - x = align.AlignTraj(universe, reference, - filename=outfile, select='name CA').run() - x_weights = align.AlignTraj(universe, reference, - filename=outfile, weights=weights).run() + x = align.AlignTraj( + universe, reference, filename=outfile, select="name CA" + ).run() + x_weights = align.AlignTraj( + universe, reference, filename=outfile, weights=weights + ).run() - assert_allclose(x.results.rmsd, x_weights.results.rmsd, rtol=0, atol=1.5e-7) + assert_allclose( + x.results.rmsd, x_weights.results.rmsd, rtol=0, atol=1.5e-7 + ) def test_AlignTraj_custom_mass_weights(self, universe, reference, tmpdir): - outfile = str(tmpdir.join('align_test.dcd')) - x = align.AlignTraj(universe, reference, - filename=outfile, - weights=reference.atoms.masses).run() + outfile = str(tmpdir.join("align_test.dcd")) + x = align.AlignTraj( + universe, + reference, + filename=outfile, + weights=reference.atoms.masses, + ).run() fitted = mda.Universe(PSF, outfile) assert_allclose(x.results.rmsd[0], 0, rtol=0, atol=1.5e-3) assert_allclose(x.results.rmsd[-1], 6.9033, rtol=0, atol=1.5e-3) - self._assert_rmsd(reference, fitted, 0, 0.0, - weights=universe.atoms.masses) - self._assert_rmsd(reference, fitted, -1, 6.929083032629219, - weights=universe.atoms.masses) + self._assert_rmsd( + reference, fitted, 0, 0.0, weights=universe.atoms.masses + ) + self._assert_rmsd( + reference, + fitted, + -1, + 6.929083032629219, + weights=universe.atoms.masses, + ) def test_AlignTraj_partial_fit(self, universe, reference, tmpdir): - outfile = str(tmpdir.join('align_test.dcd')) + outfile = str(tmpdir.join("align_test.dcd")) # fitting on a partial selection should still write the whole topology - align.AlignTraj(universe, reference, select='resid 1-20', - filename=outfile, weights='mass').run() + align.AlignTraj( + universe, + reference, + select="resid 1-20", + filename=outfile, + weights="mass", + ).run() mda.Universe(PSF, outfile) def test_AlignTraj_in_memory(self, universe, reference, tmpdir): - outfile = str(tmpdir.join('align_test.dcd')) + outfile = str(tmpdir.join("align_test.dcd")) reference.trajectory[-1] - x = align.AlignTraj(universe, reference, filename=outfile, - in_memory=True).run() + x = align.AlignTraj( + universe, reference, filename=outfile, in_memory=True + ).run() assert x.filename is None assert_allclose(x.results.rmsd[0], 6.9290, rtol=0, atol=1.5e-3) assert_allclose(x.results.rmsd[-1], 5.2797e-07, rtol=0, atol=1.5e-3) @@ -357,20 +445,31 @@ def test_AlignTraj_writer_kwargs(self, universe, reference, tmpdir): # Issue 4564 writer_kwargs = dict(precision=2) with tmpdir.as_cwd(): - aligner = align.AlignTraj(universe, reference, - select='protein and name CA', - filename='aligned_traj.xtc', - writer_kwargs=writer_kwargs, - in_memory=False).run() + aligner = align.AlignTraj( + universe, + reference, + select="protein and name CA", + filename="aligned_traj.xtc", + writer_kwargs=writer_kwargs, + in_memory=False, + ).run() assert_equal(aligner._writer.precision, 2) def _assert_rmsd(self, reference, fitted, frame, desired, weights=None): fitted.trajectory[frame] - rmsd = rms.rmsd(reference.atoms.positions, fitted.atoms.positions, - superposition=True) - assert_allclose(rmsd, desired, rtol = 0, atol=1.5e-5, - err_msg="frame {0:d} of fit does not have " - "expected RMSD".format(frame)) + rmsd = rms.rmsd( + reference.atoms.positions, + fitted.atoms.positions, + superposition=True, + ) + assert_allclose( + rmsd, + desired, + rtol=0, + atol=1.5e-5, + err_msg="frame {0:d} of fit does not have " + "expected RMSD".format(frame), + ) def test_alignto_checks_selections(self, universe, reference): """Testing that alignto() fails if selections do not @@ -396,15 +495,16 @@ def different_atoms(): def test_alignto_partial_universe(self, universe, reference): u_bound = mda.Universe(ALIGN_BOUND) u_free = mda.Universe(ALIGN_UNBOUND) - selection = 'segid B' + selection = "segid B" segB_bound = u_bound.select_atoms(selection) segB_free = u_free.select_atoms(selection) segB_free.translate(segB_bound.centroid() - segB_free.centroid()) align.alignto(u_free, u_bound, select=selection) - assert_allclose(segB_bound.positions, segB_free.positions, - rtol=0, atol=1.5e-3) + assert_allclose( + segB_bound.positions, segB_free.positions, rtol=0, atol=1.5e-3 + ) def _get_aligned_average_positions(ref_files, ref, select="all", **kwargs): @@ -412,9 +512,10 @@ def _get_aligned_average_positions(ref_files, ref, select="all", **kwargs): prealigner = align.AlignTraj(u, ref, select=select, **kwargs).run() ag = u.select_atoms(select) reference_coordinates = u.trajectory.timeseries(asel=ag).mean(axis=1) - rmsd = sum(prealigner.results.rmsd/len(u.trajectory)) + rmsd = sum(prealigner.results.rmsd / len(u.trajectory)) return reference_coordinates, rmsd + class TestAverageStructure(object): ref_files = (PSF, DCD) @@ -433,8 +534,10 @@ def test_average_structure_deprecated_attrs(self, universe, reference): wmsg = "The `universe` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): - assert_equal(avg.universe.atoms.positions, - avg.results.universe.atoms.positions) + assert_equal( + avg.universe.atoms.positions, + avg.results.universe.atoms.positions, + ) wmsg = "The `positions` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): @@ -447,34 +550,43 @@ def test_average_structure_deprecated_attrs(self, universe, reference): def test_average_structure(self, universe, reference): ref, rmsd = _get_aligned_average_positions(self.ref_files, reference) avg = align.AverageStructure(universe, reference).run() - assert_allclose(avg.results.universe.atoms.positions, ref, rtol=0, atol=1.5e-4) + assert_allclose( + avg.results.universe.atoms.positions, ref, rtol=0, atol=1.5e-4 + ) assert_allclose(avg.results.rmsd, rmsd, rtol=0, atol=1.5e-7) def test_average_structure_mass_weighted(self, universe, reference): - ref, rmsd = _get_aligned_average_positions(self.ref_files, reference, weights='mass') - avg = align.AverageStructure(universe, reference, weights='mass').run() - assert_allclose(avg.results.universe.atoms.positions, ref, - rtol=0, atol=1.5e-4) + ref, rmsd = _get_aligned_average_positions( + self.ref_files, reference, weights="mass" + ) + avg = align.AverageStructure(universe, reference, weights="mass").run() + assert_allclose( + avg.results.universe.atoms.positions, ref, rtol=0, atol=1.5e-4 + ) assert_allclose(avg.results.rmsd, rmsd, rtol=0, atol=1.5e-7) def test_average_structure_select(self, universe, reference): - select = 'protein and name CA and resid 3-5' - ref, rmsd = _get_aligned_average_positions(self.ref_files, reference, select=select) + select = "protein and name CA and resid 3-5" + ref, rmsd = _get_aligned_average_positions( + self.ref_files, reference, select=select + ) avg = align.AverageStructure(universe, reference, select=select).run() - assert_allclose(avg.results.universe.atoms.positions, ref, - rtol=0, atol=1.5e-4) + assert_allclose( + avg.results.universe.atoms.positions, ref, rtol=0, atol=1.5e-4 + ) assert_allclose(avg.results.rmsd, rmsd, rtol=0, atol=1.5e-7) def test_average_structure_no_ref(self, universe): ref, rmsd = _get_aligned_average_positions(self.ref_files, universe) avg = align.AverageStructure(universe).run() - assert_allclose(avg.results.universe.atoms.positions, ref, - rtol=0, atol=1.5e-4) + assert_allclose( + avg.results.universe.atoms.positions, ref, rtol=0, atol=1.5e-4 + ) assert_allclose(avg.results.rmsd, rmsd, rtol=0, atol=1.5e-7) def test_average_structure_no_msf(self, universe): avg = align.AverageStructure(universe).run() - assert not hasattr(avg, 'msf') + assert not hasattr(avg, "msf") def test_mismatch_atoms(self, universe): u = mda.Merge(universe.atoms[:10]) @@ -493,15 +605,20 @@ def test_average_structure_ref_frame(self, universe): universe.trajectory[0] ref, rmsd = _get_aligned_average_positions(self.ref_files, u) avg = align.AverageStructure(universe, ref_frame=ref_frame).run() - assert_allclose(avg.results.universe.atoms.positions, ref, - rtol=0, atol=1.5e-4) + assert_allclose( + avg.results.universe.atoms.positions, ref, rtol=0, atol=1.5e-4 + ) assert_allclose(avg.results.rmsd, rmsd, rtol=0, atol=1.5e-7) def test_average_structure_in_memory(self, universe): avg = align.AverageStructure(universe, in_memory=True).run() reference_coordinates = universe.trajectory.timeseries().mean(axis=1) - assert_allclose(avg.results.universe.atoms.positions, - reference_coordinates, rtol=0, atol=1.5e-4) + assert_allclose( + avg.results.universe.atoms.positions, + reference_coordinates, + rtol=0, + atol=1.5e-4, + ) assert avg.filename is None @@ -509,57 +626,65 @@ class TestAlignmentProcessing: seq = FASTA error_msg = "selection string has unexpected length" - @pytest.mark.skipif(HAS_BIOPYTHON, reason='biopython is installed') + @pytest.mark.skipif(HAS_BIOPYTHON, reason="biopython is installed") def test_importerror_biopython(self): errmsg = "The `fasta2select` method requires an installation" with pytest.raises(ImportError, match=errmsg): _ = align.fasta2select(self.seq, is_aligned=True) - @pytest.mark.skipif(not HAS_BIOPYTHON, reason='requires biopython') + @pytest.mark.skipif(not HAS_BIOPYTHON, reason="requires biopython") def test_fasta2select_aligned(self): """test align.fasta2select() on aligned FASTA (Issue 112)""" sel = align.fasta2select(self.seq, is_aligned=True) # length of the output strings, not residues or anything real... - assert len(sel['reference']) == 30623, self.error_msg - assert len(sel['mobile']) == 30623, self.error_msg + assert len(sel["reference"]) == 30623, self.error_msg + assert len(sel["mobile"]) == 30623, self.error_msg @pytest.mark.skipif( executable_not_found("clustalw2") or not HAS_BIOPYTHON, - reason="Test skipped because clustalw2 executable not found") + reason="Test skipped because clustalw2 executable not found", + ) def test_fasta2select_file(self, tmpdir): """test align.fasta2select() on a non-aligned FASTA with default filenames""" with tmpdir.as_cwd(): - sel = align.fasta2select(self.seq, is_aligned=False, - alnfilename=None, treefilename=None) - assert len(sel['reference']) == 23080, self.error_msg - assert len(sel['mobile']) == 23090, self.error_msg + sel = align.fasta2select( + self.seq, is_aligned=False, alnfilename=None, treefilename=None + ) + assert len(sel["reference"]) == 23080, self.error_msg + assert len(sel["mobile"]) == 23090, self.error_msg @pytest.mark.skipif( executable_not_found("clustalw2") or not HAS_BIOPYTHON, - reason="Test skipped because clustalw2 executable not found") + reason="Test skipped because clustalw2 executable not found", + ) def test_fasta2select_ClustalW(self, tmpdir): """MDAnalysis.analysis.align: test fasta2select() with ClustalW (Issue 113)""" - alnfile = str(tmpdir.join('alignmentprocessing.aln')) - treefile = str(tmpdir.join('alignmentprocessing.dnd')) - sel = align.fasta2select(self.seq, is_aligned=False, - alnfilename=alnfile, treefilename=treefile) + alnfile = str(tmpdir.join("alignmentprocessing.aln")) + treefile = str(tmpdir.join("alignmentprocessing.dnd")) + sel = align.fasta2select( + self.seq, + is_aligned=False, + alnfilename=alnfile, + treefilename=treefile, + ) # numbers computed from alignment with clustalw 2.1 on Mac OS X # [orbeckst] length of the output strings, not residues or anything # real... - assert len(sel['reference']) == 23080, self.error_msg - assert len(sel['mobile']) == 23090, self.error_msg + assert len(sel["reference"]) == 23080, self.error_msg + assert len(sel["mobile"]) == 23090, self.error_msg - @pytest.mark.skipif(not HAS_BIOPYTHON, reason='requires biopython') + @pytest.mark.skipif(not HAS_BIOPYTHON, reason="requires biopython") def test_fasta2select_resids(self, tmpdir): """test align.fasta2select() when resids provided (Issue #3124)""" resids = [x for x in range(705)] - sel = align.fasta2select(self.seq, is_aligned=True, - ref_resids=resids, target_resids=resids) + sel = align.fasta2select( + self.seq, is_aligned=True, ref_resids=resids, target_resids=resids + ) # length of the output strings, not residues or anything real... - assert len(sel['reference']) == 30621, self.error_msg - assert len(sel['mobile']) == 30621, self.error_msg + assert len(sel["reference"]) == 30621, self.error_msg + assert len(sel["mobile"]) == 30621, self.error_msg class TestSequenceAlignmentFunction: @@ -573,14 +698,14 @@ def atomgroups(): mobile = universe.select_atoms("resid 122-159") return reference, mobile - @pytest.mark.skipif(HAS_BIOPYTHON, reason='biopython installed') + @pytest.mark.skipif(HAS_BIOPYTHON, reason="biopython installed") def test_biopython_import_error(self, atomgroups): ref, mob = atomgroups errmsg = "The `sequence_alignment` method requires an installation of" with pytest.raises(ImportError, match=errmsg): align.sequence_alignment(mob, ref) - @pytest.mark.skipif(not HAS_BIOPYTHON, reason='requires biopython') + @pytest.mark.skipif(not HAS_BIOPYTHON, reason="requires biopython") @pytest.mark.filterwarnings("ignore:`sequence_alignment` is deprecated!") def test_sequence_alignment(self, atomgroups): reference, mobile = atomgroups @@ -589,18 +714,24 @@ def test_sequence_alignment(self, atomgroups): assert len(aln) == 5, "return value has wrong tuple size" seqA, seqB, score, begin, end = aln - assert_equal(seqA, reference.residues.sequence(format="string"), - err_msg="reference sequence mismatch") - assert mobile.residues.sequence( - format="string") in seqB, "mobile sequence mismatch" - assert score == pytest.approx(54.6) + assert_equal( + seqA, + reference.residues.sequence(format="string"), + err_msg="reference sequence mismatch", + ) + assert ( + mobile.residues.sequence(format="string") in seqB + ), "mobile sequence mismatch" + assert score == pytest.approx(54.6) assert_array_equal([begin, end], [0, reference.n_residues]) - @pytest.mark.skipif(not HAS_BIOPYTHON, reason='requires biopython') + @pytest.mark.skipif(not HAS_BIOPYTHON, reason="requires biopython") def test_sequence_alignment_deprecation(self, atomgroups): reference, mobile = atomgroups - wmsg = ("`sequence_alignment` is deprecated!\n" - "`sequence_alignment` will be removed in release 3.0.") + wmsg = ( + "`sequence_alignment` is deprecated!\n" + "`sequence_alignment` will be removed in release 3.0." + ) with pytest.warns(DeprecationWarning, match=wmsg): align.sequence_alignment(mobile, reference) @@ -630,14 +761,13 @@ def test_iterative_average_default(self, mobile): [10.54679871, 9.49505306, -8.61215292], [9.99500556, 9.16624224, -7.75231192], [9.83897407, 9.93134598, -9.29541129], - [11.45760169, 10.5857071, -8.13037669] + [11.45760169, 10.5857071, -8.13037669], ], atol=1e-5, ) def test_iterative_average_eps_high(self, mobile): - res = align.iterative_average(mobile, select="bynum 1:10", - eps=1e-5) + res = align.iterative_average(mobile, select="bynum 1:10", eps=1e-5) assert_allclose( res.results.positions, [ @@ -650,15 +780,15 @@ def test_iterative_average_eps_high(self, mobile): [10.54679871, 9.49505306, -8.61215292], [9.99500556, 9.16624224, -7.75231192], [9.83897407, 9.93134598, -9.29541129], - [11.45760169, 10.5857071, -8.13037669] + [11.45760169, 10.5857071, -8.13037669], ], atol=1e-5, ) def test_iterative_average_weights_mass(self, mobile, reference): - res = align.iterative_average(mobile, reference, - select="bynum 1:10", - niter=10, weights="mass") + res = align.iterative_average( + mobile, reference, select="bynum 1:10", niter=10, weights="mass" + ) assert_allclose( res.results.positions, [ @@ -671,19 +801,17 @@ def test_iterative_average_weights_mass(self, mobile, reference): [10.37499697, 9.13535837, -8.3732586], [9.83883314, 8.57939098, -7.6195549], [9.64405257, 9.55924307, -9.04315991], - [11.0678934, 10.27798773, -7.64881842] + [11.0678934, 10.27798773, -7.64881842], ], atol=1e-5, ) def test_iterative_average_convergence_failure(self, mobile, reference): with pytest.raises(RuntimeError): - _ = align.iterative_average(mobile, reference, - niter=1, eps=0) + _ = align.iterative_average(mobile, reference, niter=1, eps=0) def test_iterative_average_convergence_verbose(self, mobile, reference): - _ = align.iterative_average(mobile, select="bynum 1:10", - verbose=True) + _ = align.iterative_average(mobile, select="bynum 1:10", verbose=True) def test_alignto_reorder_atomgroups(): @@ -691,5 +819,5 @@ def test_alignto_reorder_atomgroups(): u = mda.Universe(PDB_helix) mobile = u.atoms[:4] ref = u.atoms[[3, 2, 1, 0]] - rmsd = align.alignto(mobile, ref, select='bynum 1-4') + rmsd = align.alignto(mobile, ref, select="bynum 1-4") assert_allclose(rmsd, (0.0, 0.0)) diff --git a/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py b/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py index 443173cff70..4697485470b 100644 --- a/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py +++ b/testsuite/MDAnalysisTests/analysis/test_atomicdistances.py @@ -79,8 +79,9 @@ def ad_ag4(): @staticmethod @pytest.fixture() def expected_dist(ad_ag1, ad_ag2): - expected = np.zeros((len(ad_ag1.universe.trajectory), - ad_ag1.atoms.n_atoms)) + expected = np.zeros( + (len(ad_ag1.universe.trajectory), ad_ag1.atoms.n_atoms) + ) # calculate distances without PBCs using dist() for i, ts in enumerate(ad_ag1.universe.trajectory): @@ -90,8 +91,9 @@ def expected_dist(ad_ag1, ad_ag2): @staticmethod @pytest.fixture() def expected_pbc_dist(ad_ag1, ad_ag2): - expected = np.zeros((len(ad_ag1.universe.trajectory), - ad_ag1.atoms.n_atoms)) + expected = np.zeros( + (len(ad_ag1.universe.trajectory), ad_ag1.atoms.n_atoms) + ) # calculate distances with PBCs using dist() for i, ts in enumerate(ad_ag1.universe.trajectory): @@ -99,8 +101,8 @@ def expected_pbc_dist(ad_ag1, ad_ag2): return expected def test_ad_exceptions(self, ad_ag1, ad_ag3, ad_ag4): - '''Test exceptions raised when number of atoms do not - match or AtomGroups come from different trajectories.''' + """Test exceptions raised when number of atoms do not + match or AtomGroups come from different trajectories.""" # number of atoms do not match match_exp_atoms = "AtomGroups do not" @@ -114,22 +116,19 @@ def test_ad_exceptions(self, ad_ag1, ad_ag3, ad_ag4): # only need to test that this class correctly applies distance calcs # calc_bonds() is tested elsewhere - def test_ad_pairwise_dist(self, ad_ag1, ad_ag2, - expected_dist): - '''Ensure that pairwise distances between atoms are - correctly calculated without PBCs.''' - pairwise_no_pbc = (ad.AtomicDistances(ad_ag1, ad_ag2, - pbc=False).run()) + def test_ad_pairwise_dist(self, ad_ag1, ad_ag2, expected_dist): + """Ensure that pairwise distances between atoms are + correctly calculated without PBCs.""" + pairwise_no_pbc = ad.AtomicDistances(ad_ag1, ad_ag2, pbc=False).run() actual = pairwise_no_pbc.results # compare with expected values from dist() assert_allclose(actual, expected_dist) - def test_ad_pairwise_dist_pbc(self, ad_ag1, ad_ag2, - expected_pbc_dist): - '''Ensure that pairwise distances between atoms are - correctly calculated with PBCs.''' - pairwise_pbc = (ad.AtomicDistances(ad_ag1, ad_ag2).run()) + def test_ad_pairwise_dist_pbc(self, ad_ag1, ad_ag2, expected_pbc_dist): + """Ensure that pairwise distances between atoms are + correctly calculated with PBCs.""" + pairwise_pbc = ad.AtomicDistances(ad_ag1, ad_ag2).run() actual = pairwise_pbc.results # compare with expected values from dist() diff --git a/testsuite/MDAnalysisTests/analysis/test_backends.py b/testsuite/MDAnalysisTests/analysis/test_backends.py index a4c105e082a..471d367a603 100644 --- a/testsuite/MDAnalysisTests/analysis/test_backends.py +++ b/testsuite/MDAnalysisTests/analysis/test_backends.py @@ -51,22 +51,36 @@ def test_all_backends_give_correct_results(self, func, iterable, answer): for answ in backends_dict.values(): assert answ == answer - @pytest.mark.parametrize("backend_cls,params,warning_message", [ - (backends.BackendSerial, { - 'n_workers': 5 - }, "n_workers is ignored when executing with backend='serial'"), - ]) + @pytest.mark.parametrize( + "backend_cls,params,warning_message", + [ + ( + backends.BackendSerial, + {"n_workers": 5}, + "n_workers is ignored when executing with backend='serial'", + ), + ], + ) def test_get_warnings(self, backend_cls, params, warning_message): with pytest.warns(UserWarning, match=warning_message): backend_cls(**params) - @pytest.mark.parametrize("backend_cls,params,error_message", [ - pytest.param(backends.BackendDask, {'n_workers': 2}, - ("module 'dask' is missing. Please install 'dask': " - "https://docs.dask.org/en/stable/install.html"), - marks=pytest.mark.skipif(is_installed('dask'), - reason='dask is installed')) - ]) + @pytest.mark.parametrize( + "backend_cls,params,error_message", + [ + pytest.param( + backends.BackendDask, + {"n_workers": 2}, + ( + "module 'dask' is missing. Please install 'dask': " + "https://docs.dask.org/en/stable/install.html" + ), + marks=pytest.mark.skipif( + is_installed("dask"), reason="dask is installed" + ), + ) + ], + ) def test_get_errors(self, backend_cls, params, error_message): with pytest.raises(ValueError, match=error_message): backend_cls(**params) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 377d70602ba..e369c4c6021 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -20,29 +20,25 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -from collections import UserDict import pickle - -import pytest - -import numpy as np - -from numpy.testing import assert_equal, assert_allclose +from collections import UserDict import MDAnalysis as mda import numpy as np import pytest -from MDAnalysis.analysis import base, backends +from MDAnalysis.analysis import backends, base +from numpy.testing import assert_allclose, assert_almost_equal, assert_equal + from MDAnalysisTests.datafiles import DCD, PSF, TPR, XTC from MDAnalysisTests.util import no_deprecated_call -from numpy.testing import assert_almost_equal, assert_equal class FrameAnalysis(base.AnalysisBase): """Just grabs frame numbers of frames it goes over""" @classmethod - def get_supported_backends(cls): return ('serial', 'dask', 'multiprocessing') + def get_supported_backends(cls): + return ("serial", "dask", "multiprocessing") _analysis_algorithm_is_parallelizable = True @@ -60,7 +56,10 @@ def _conclude(self): self.found_frames = list(self.results.found_frames) def _get_aggregator(self): - return base.ResultsGroup({'found_frames': base.ResultsGroup.ndarray_hstack}) + return base.ResultsGroup( + {"found_frames": base.ResultsGroup.ndarray_hstack} + ) + class IncompleteAnalysis(base.AnalysisBase): def __init__(self, reader, **kwargs): @@ -80,42 +79,57 @@ def _prepare(self): self.results = base.Results() -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return mda.Universe(PSF, DCD) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u_xtc(): return mda.Universe(TPR, XTC) # dt = 100 -FRAMES_ERR = 'AnalysisBase.frames is incorrect' -TIMES_ERR = 'AnalysisBase.times is incorrect' +FRAMES_ERR = "AnalysisBase.frames is incorrect" +TIMES_ERR = "AnalysisBase.times is incorrect" + class Parallelizable(base.AnalysisBase): _analysis_algorithm_is_parallelizable = True + @classmethod - def get_supported_backends(cls): return ('multiprocessing', 'dask') - def _single_frame(self): pass + def get_supported_backends(cls): + return ("multiprocessing", "dask") + + def _single_frame(self): + pass + class SerialOnly(base.AnalysisBase): - def _single_frame(self): pass + def _single_frame(self): + pass + class ParallelizableWithDaskOnly(base.AnalysisBase): _analysis_algorithm_is_parallelizable = True + @classmethod - def get_supported_backends(cls): return ('dask',) - def _single_frame(self): pass + def get_supported_backends(cls): + return ("dask",) + + def _single_frame(self): + pass + class CustomSerialBackend(backends.BackendBase): def apply(self, func, computations): return [func(task) for task in computations] + class ManyWorkersBackend(backends.BackendBase): def apply(self, func, computations): return [func(task) for task in computations] + def test_incompatible_n_workers(u): backend = ManyWorkersBackend(n_workers=2) with pytest.raises(ValueError): @@ -126,91 +140,137 @@ 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"): + 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 + frames=frames, start=start, stop=stop, step=step ) + 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 + 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), - (SerialOnly, 'not-existing-backend', 2), - (SerialOnly, 'not-existing-backend', None), - (SerialOnly, 'multiprocessing', 2), - (SerialOnly, 'dask', None), - (ParallelizableWithDaskOnly, 'multiprocessing', None), - (ParallelizableWithDaskOnly, 'multiprocessing', 2), -]) + +@pytest.mark.parametrize( + "run_class,backend,n_workers", + [ + (Parallelizable, "not-existing-backend", 2), + (Parallelizable, "not-existing-backend", None), + (SerialOnly, "not-existing-backend", 2), + (SerialOnly, "not-existing-backend", None), + (SerialOnly, "multiprocessing", 2), + (SerialOnly, "dask", None), + (ParallelizableWithDaskOnly, "multiprocessing", None), + (ParallelizableWithDaskOnly, "multiprocessing", 2), + ], +) def test_backend_configuration_fails(u, run_class, backend, n_workers): u = mda.Universe(TPR, XTC) # dt = 100 with pytest.raises(ValueError): - _ = run_class(u.trajectory).run(backend=backend, n_workers=n_workers, stop=0) + _ = run_class(u.trajectory).run( + backend=backend, n_workers=n_workers, stop=0 + ) + -@pytest.mark.parametrize('run_class,backend,n_workers', [ - (Parallelizable, CustomSerialBackend, 2), - (ParallelizableWithDaskOnly, CustomSerialBackend, 2), -]) -def test_backend_configuration_works_when_unsupported_backend(u, run_class, backend, n_workers): +@pytest.mark.parametrize( + "run_class,backend,n_workers", + [ + (Parallelizable, CustomSerialBackend, 2), + (ParallelizableWithDaskOnly, CustomSerialBackend, 2), + ], +) +def test_backend_configuration_works_when_unsupported_backend( + u, run_class, backend, n_workers +): u = mda.Universe(TPR, XTC) # dt = 100 backend_instance = backend(n_workers=n_workers) - _ = run_class(u.trajectory).run(backend=backend_instance, n_workers=n_workers, stop=0, unsupported_backend=True) + _ = run_class(u.trajectory).run( + backend=backend_instance, + n_workers=n_workers, + stop=0, + unsupported_backend=True, + ) + -@pytest.mark.parametrize('run_class,backend,n_workers', [ - (Parallelizable, CustomSerialBackend, 1), - (ParallelizableWithDaskOnly, CustomSerialBackend, 1), -]) +@pytest.mark.parametrize( + "run_class,backend,n_workers", + [ + (Parallelizable, CustomSerialBackend, 1), + (ParallelizableWithDaskOnly, CustomSerialBackend, 1), + ], +) def test_custom_backend_works(u, run_class, backend, n_workers): backend_instance = backend(n_workers=n_workers) u = mda.Universe(TPR, XTC) # dt = 100 - _ = run_class(u.trajectory).run(backend=backend_instance, n_workers=n_workers, unsupported_backend=True) - -@pytest.mark.parametrize('run_class,backend_instance,n_workers', [ - (Parallelizable, map, 1), - (SerialOnly, list, 1), - (ParallelizableWithDaskOnly, object, 1), -]) -def test_fails_incorrect_custom_backend(u, run_class, backend_instance, n_workers): + _ = run_class(u.trajectory).run( + backend=backend_instance, n_workers=n_workers, unsupported_backend=True + ) + + +@pytest.mark.parametrize( + "run_class,backend_instance,n_workers", + [ + (Parallelizable, map, 1), + (SerialOnly, list, 1), + (ParallelizableWithDaskOnly, object, 1), + ], +) +def test_fails_incorrect_custom_backend( + u, run_class, backend_instance, n_workers +): u = mda.Universe(TPR, XTC) # dt = 100 with pytest.raises(ValueError): - _ = run_class(u.trajectory).run(backend=backend_instance, n_workers=n_workers, unsupported_backend=True) + _ = run_class(u.trajectory).run( + backend=backend_instance, + n_workers=n_workers, + unsupported_backend=True, + ) with pytest.raises(ValueError): - _ = run_class(u.trajectory).run(backend=backend_instance, n_workers=n_workers) + _ = run_class(u.trajectory).run( + backend=backend_instance, n_workers=n_workers + ) -@pytest.mark.parametrize('run_class,backend,n_workers', [ - (SerialOnly, CustomSerialBackend, 1), - (SerialOnly, 'multiprocessing', 1), - (SerialOnly, 'dask', 1), -]) + +@pytest.mark.parametrize( + "run_class,backend,n_workers", + [ + (SerialOnly, CustomSerialBackend, 1), + (SerialOnly, "multiprocessing", 1), + (SerialOnly, "dask", 1), + ], +) def test_fails_for_unparallelizable(u, run_class, backend, n_workers): u = mda.Universe(TPR, XTC) # dt = 100 with pytest.raises(ValueError): if not isinstance(backend, str): backend_instance = backend(n_workers=n_workers) - _ = run_class(u.trajectory).run(backend=backend_instance, n_workers=n_workers, unsupported_backend=True) + _ = run_class(u.trajectory).run( + backend=backend_instance, + n_workers=n_workers, + unsupported_backend=True, + ) else: - _ = run_class(u.trajectory).run(backend=backend, n_workers=n_workers, unsupported_backend=True) - -@pytest.mark.parametrize('run_kwargs,frames', [ - ({}, np.arange(98)), - ({'start': 20}, np.arange(20, 98)), - ({'stop': 30}, np.arange(30)), - ({'step': 10}, np.arange(0, 98, 10)) -]) + _ = run_class(u.trajectory).run( + backend=backend, n_workers=n_workers, unsupported_backend=True + ) + + +@pytest.mark.parametrize( + "run_kwargs,frames", + [ + ({}, np.arange(98)), + ({"start": 20}, np.arange(20, 98)), + ({"stop": 30}, np.arange(30)), + ({"step": 10}, np.arange(0, 98, 10)), + ], +) def test_start_stop_step_parallel(u, run_kwargs, frames, client_FrameAnalysis): # client_FrameAnalysis is defined [here](testsuite/MDAnalysisTests/analysis/conftest.py), # and determines a set of parameters ('backend', 'n_workers'), taking only backends @@ -219,7 +279,7 @@ def test_start_stop_step_parallel(u, run_kwargs, frames, client_FrameAnalysis): assert an.n_frames == len(frames) assert_equal(an.found_frames, frames) assert_equal(an.frames, frames, err_msg=FRAMES_ERR) - assert_almost_equal(an.times, frames+1, decimal=4, err_msg=TIMES_ERR) + assert_almost_equal(an.times, frames + 1, decimal=4, err_msg=TIMES_ERR) def test_reset_n_parts_to_n_frames(u): @@ -228,36 +288,57 @@ def test_reset_n_parts_to_n_frames(u): 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)), - ({'stop': 30}, np.arange(30)), - ({'step': 10}, np.arange(0, 98, 10)) -]) + 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)), + ({"stop": 30}, np.arange(30)), + ({"step": 10}, np.arange(0, 98, 10)), + ], +) def test_start_stop_step(u, run_kwargs, frames): an = FrameAnalysis(u.trajectory).run(**run_kwargs) assert an.n_frames == len(frames) assert_equal(an.found_frames, frames) assert_equal(an.frames, frames, err_msg=FRAMES_ERR) - assert_allclose(an.times, frames+1, rtol=0, atol=1.5e-4, err_msg=TIMES_ERR) + assert_allclose( + an.times, frames + 1, rtol=0, atol=1.5e-4, err_msg=TIMES_ERR + ) -@pytest.mark.parametrize('run_kwargs, frames', [ - ({'frames': [4, 5, 6, 7, 8, 9]}, np.arange(4, 10)), - ({'frames': [0, 2, 4, 6, 8]}, np.arange(0, 10, 2)), - ({'frames': [4, 6, 8]}, np.arange(4, 10, 2)), - ({'frames': [0, 3, 4, 3, 5]}, [0, 3, 4, 3, 5]), - ({'frames': [True, True, False, True, False, True, True, False, True, - False]}, (0, 1, 3, 5, 6, 8)), -]) +@pytest.mark.parametrize( + "run_kwargs, frames", + [ + ({"frames": [4, 5, 6, 7, 8, 9]}, np.arange(4, 10)), + ({"frames": [0, 2, 4, 6, 8]}, np.arange(0, 10, 2)), + ({"frames": [4, 6, 8]}, np.arange(4, 10, 2)), + ({"frames": [0, 3, 4, 3, 5]}, [0, 3, 4, 3, 5]), + ( + { + "frames": [ + True, + True, + False, + True, + False, + True, + True, + False, + True, + False, + ] + }, + (0, 1, 3, 5, 6, 8), + ), + ], +) def test_frame_slice(u_xtc, run_kwargs, frames): an = FrameAnalysis(u_xtc.trajectory).run(**run_kwargs) assert an.n_frames == len(frames) @@ -265,14 +346,32 @@ def test_frame_slice(u_xtc, run_kwargs, frames): assert_equal(an.frames, frames, err_msg=FRAMES_ERR) -@pytest.mark.parametrize('run_kwargs, frames', [ - ({'frames': [4, 5, 6, 7, 8, 9]}, np.arange(4, 10)), - ({'frames': [0, 2, 4, 6, 8]}, np.arange(0, 10, 2)), - ({'frames': [4, 6, 8]}, np.arange(4, 10, 2)), - ({'frames': [0, 3, 4, 3, 5]}, [0, 3, 4, 3, 5]), - ({'frames': [True, True, False, True, False, True, True, False, True, - False]}, (0, 1, 3, 5, 6, 8)), -]) +@pytest.mark.parametrize( + "run_kwargs, frames", + [ + ({"frames": [4, 5, 6, 7, 8, 9]}, np.arange(4, 10)), + ({"frames": [0, 2, 4, 6, 8]}, np.arange(0, 10, 2)), + ({"frames": [4, 6, 8]}, np.arange(4, 10, 2)), + ({"frames": [0, 3, 4, 3, 5]}, [0, 3, 4, 3, 5]), + ( + { + "frames": [ + True, + True, + False, + True, + False, + True, + True, + False, + True, + False, + ] + }, + (0, 1, 3, 5, 6, 8), + ), + ], +) def test_frame_slice_parallel(run_kwargs, frames, client_FrameAnalysis): u = mda.Universe(TPR, XTC) # dt = 100 an = FrameAnalysis(u.trajectory).run(**run_kwargs, **client_FrameAnalysis) @@ -281,25 +380,30 @@ def test_frame_slice_parallel(run_kwargs, frames, client_FrameAnalysis): assert_equal(an.frames, frames, err_msg=FRAMES_ERR) -@pytest.mark.parametrize('run_kwargs', [ - ({'start': 4, 'frames': [4, 5, 6, 7, 8, 9]}), - ({'stop': 6, 'frames': [0, 1, 2, 3, 4, 5]}), - ({'step': 2, 'frames': [0, 2, 4, 6, 8]}), - ({'start': 4, 'stop': 7, 'frames': [4, 5, 6]}), - ({'stop': 6, 'step': 2, 'frames': [0, 2, 4, 6]}), - ({'start': 4, 'step': 2, 'frames': [4, 6, 8]}), - ({'start': 0, 'stop': 0, 'step': 0, 'frames': [4, 6, 8]}), -]) +@pytest.mark.parametrize( + "run_kwargs", + [ + ({"start": 4, "frames": [4, 5, 6, 7, 8, 9]}), + ({"stop": 6, "frames": [0, 1, 2, 3, 4, 5]}), + ({"step": 2, "frames": [0, 2, 4, 6, 8]}), + ({"start": 4, "stop": 7, "frames": [4, 5, 6]}), + ({"stop": 6, "step": 2, "frames": [0, 2, 4, 6]}), + ({"start": 4, "step": 2, "frames": [4, 6, 8]}), + ({"start": 0, "stop": 0, "step": 0, "frames": [4, 6, 8]}), + ], +) def test_frame_fail(u, run_kwargs, client_FrameAnalysis): an = FrameAnalysis(u.trajectory) - msg = 'start/stop/step cannot be combined with frames' + msg = "start/stop/step cannot be combined with frames" with pytest.raises(ValueError, match=msg): an.run(**client_FrameAnalysis, **run_kwargs) + def test_parallelizable_transformations(): - # pick any transformation that would allow + # pick any transformation that would allow # for parallelizable attribute - from MDAnalysis.transformations import NoJump + from MDAnalysis.transformations import NoJump + u = mda.Universe(XTC, to_guess=()) u.trajectory.add_transformations(NoJump()) @@ -308,18 +412,18 @@ def test_parallelizable_transformations(): # test that parallel fails with pytest.raises(ValueError): - FrameAnalysis(u.trajectory).run(backend='multiprocessing') + 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' + 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 + unsupported_backend=True, ) @@ -327,25 +431,31 @@ def test_frame_bool_fail(client_FrameAnalysis): u = mda.Universe(TPR, XTC) # dt = 100 an = FrameAnalysis(u.trajectory) frames = [True, True, False] - msg = 'boolean index did not match indexed array along (axis|dimension) 0' + msg = "boolean index did not match indexed array along (axis|dimension) 0" with pytest.raises(IndexError, match=msg): an.run(**client_FrameAnalysis, frames=frames) def test_rewind(client_FrameAnalysis): u = mda.Universe(TPR, XTC) # dt = 100 - an = FrameAnalysis(u.trajectory).run(**client_FrameAnalysis, frames=[0, 2, 3, 5, 9]) + an = FrameAnalysis(u.trajectory).run( + **client_FrameAnalysis, frames=[0, 2, 3, 5, 9] + ) assert_equal(u.trajectory.ts.frame, 0) def test_frames_times(client_FrameAnalysis): u = mda.Universe(TPR, XTC) # dt = 100 - an = FrameAnalysis(u.trajectory).run(start=1, stop=8, step=2, **client_FrameAnalysis) + an = FrameAnalysis(u.trajectory).run( + start=1, stop=8, step=2, **client_FrameAnalysis + ) frames = np.array([1, 3, 5, 7]) assert an.n_frames == len(frames) assert_equal(an.found_frames, frames) assert_equal(an.frames, frames, err_msg=FRAMES_ERR) - assert_allclose(an.times, frames*100, rtol=0, atol=1.5e-4, err_msg=TIMES_ERR) + assert_allclose( + an.times, frames * 100, rtol=0, atol=1.5e-4, err_msg=TIMES_ERR + ) def test_verbose(u): @@ -356,7 +466,7 @@ def test_verbose(u): def test_warn_nparts_nworkers(u): a = FrameAnalysis(u.trajectory) with pytest.warns(UserWarning): - a.run(backend='multiprocessing', n_workers=3, n_parts=2) + a.run(backend="multiprocessing", n_workers=3, n_parts=2) @pytest.mark.parametrize( @@ -364,8 +474,8 @@ def test_warn_nparts_nworkers(u): [ (base.AnalysisBase, False), (base.AnalysisFromFunction, True), - (FrameAnalysis, True) - ] + (FrameAnalysis, True), + ], ) def test_not_parallelizable(u, classname, is_parallelizable): assert classname._analysis_algorithm_is_parallelizable == is_parallelizable @@ -374,30 +484,34 @@ def test_not_parallelizable(u, classname, is_parallelizable): def test_verbose_progressbar(u, capsys): FrameAnalysis(u.trajectory).run() _, err = capsys.readouterr() - expected = '' - actual = err.strip().split('\r')[-1] + expected = "" + actual = err.strip().split("\r")[-1] assert actual == expected def test_verbose_progressbar_run(u, capsys): FrameAnalysis(u.trajectory).run(verbose=True) _, 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_verbose_progressbar_run_with_kwargs(u, capsys): FrameAnalysis(u.trajectory).run( - verbose=True, progressbar_kwargs={'desc': 'custom'}) + verbose=True, progressbar_kwargs={"desc": "custom"} + ) _, err = capsys.readouterr() - expected = u'custom: 100%|██████████' - actual = err.strip().split('\r')[-1] + expected = "custom: 100%|██████████" + actual = err.strip().split("\r")[-1] assert actual[:23] == expected def test_progressbar_multiprocessing(u): with pytest.raises(ValueError): - FrameAnalysis(u.trajectory).run(backend='multiprocessing', verbose=True) + FrameAnalysis(u.trajectory).run( + backend="multiprocessing", verbose=True + ) def test_incomplete_defined_analysis(u): @@ -413,7 +527,7 @@ def test_filter_baseanalysis_kwargs_VE(): def bad_f(mobile, verbose=2): pass - kwargs = {'step': 3, 'foo': None} + kwargs = {"step": 3, "foo": None} with pytest.raises(ValueError): base._filter_baseanalysis_kwargs(bad_f, kwargs) @@ -423,15 +537,15 @@ def test_filter_baseanalysis_kwargs(): def good_f(mobile, ref): pass - kwargs = {'step': 3, 'foo': None} + kwargs = {"step": 3, "foo": None} base_kwargs, kwargs = base._filter_baseanalysis_kwargs(good_f, kwargs) assert 2 == len(kwargs) - assert kwargs['foo'] == None + assert kwargs["foo"] == None assert len(base_kwargs) == 1 - assert base_kwargs['verbose'] is False + assert base_kwargs["verbose"] is False def simple_function(mobile): @@ -443,13 +557,18 @@ def test_results_type(u): assert type(an.results) == base.Results -@pytest.mark.parametrize('start, stop, step, nframes', [ - (None, None, 2, 49), - (None, 50, 2, 25), - (20, 50, 2, 15), - (20, 50, None, 30) -]) -def test_AnalysisFromFunction(u, start, stop, step, nframes, client_AnalysisFromFunction): +@pytest.mark.parametrize( + "start, stop, step, nframes", + [ + (None, None, 2, 49), + (None, 50, 2, 25), + (20, 50, 2, 15), + (20, 50, None, 30), + ], +) +def test_AnalysisFromFunction( + u, start, stop, step, nframes, client_AnalysisFromFunction +): # client_AnalysisFromFunction is defined [here](testsuite/MDAnalysisTests/analysis/conftest.py), # and determines a set of parameters ('backend', 'n_workers'), taking only backends # that are implemented for a given subclass, to run the test against. @@ -488,7 +607,7 @@ def mass_xyz(atomgroup1, atomgroup2, masses): def test_AnalysisFromFunction_args_content(u, client_AnalysisFromFunction): - protein = u.select_atoms('protein') + protein = u.select_atoms("protein") masses = protein.masses.reshape(-1, 1) another = mda.Universe(TPR, XTC).select_atoms("protein") ans = base.AnalysisFromFunction(mass_xyz, protein, another, masses) @@ -507,7 +626,9 @@ def test_analysis_class(client_AnalysisFromFunctionAnalysisClass): u = mda.Universe(PSF, DCD) step = 2 - ana = ana_class(u.atoms).run(step=step, **client_AnalysisFromFunctionAnalysisClass) + ana = ana_class(u.atoms).run( + step=step, **client_AnalysisFromFunctionAnalysisClass + ) results = [] for ts in u.trajectory[::step]: diff --git a/testsuite/MDAnalysisTests/analysis/test_bat.py b/testsuite/MDAnalysisTests/analysis/test_bat.py index f6bf24a56a8..704cf616cb7 100644 --- a/testsuite/MDAnalysisTests/analysis/test_bat.py +++ b/testsuite/MDAnalysisTests/analysis/test_bat.py @@ -28,8 +28,15 @@ import copy import MDAnalysis as mda -from MDAnalysisTests.datafiles import (PSF, DCD, mol2_comments_header, XYZ_mini, - BATArray, TPR, XTC) +from MDAnalysisTests.datafiles import ( + PSF, + DCD, + mol2_comments_header, + XYZ_mini, + BATArray, + TPR, + XTC, +) from MDAnalysis.analysis.bat import BAT @@ -48,7 +55,7 @@ def bat(self, selected_residues, client_BAT): @pytest.fixture def bat_npz(self, tmpdir, selected_residues, client_BAT): - filename = str(tmpdir / 'test_bat_IO.npy') + filename = str(tmpdir / "test_bat_IO.npy") R = BAT(selected_residues) R.run(**client_BAT) R.save(filename) @@ -56,13 +63,16 @@ def bat_npz(self, tmpdir, selected_residues, client_BAT): def test_bat_root_selection(self, selected_residues): R = BAT(selected_residues) - assert_equal(R._root.indices, [8, 2, 1], - err_msg="error: incorrect root atoms selected") + assert_equal( + R._root.indices, + [8, 2, 1], + err_msg="error: incorrect root atoms selected", + ) def test_bat_number_of_frames(self, bat): - assert_equal(len(bat), - 2, - err_msg="error: list is not length of trajectory") + assert_equal( + len(bat), 2, err_msg="error: list is not length of trajectory" + ) def test_bat_coordinates(self, bat): test_bat = np.load(BATArray) @@ -71,24 +81,35 @@ def test_bat_coordinates(self, bat): test_bat, rtol=0, atol=1.5e-5, - err_msg="error: BAT coordinates should match test values") + err_msg="error: BAT coordinates should match test values", + ) def test_bat_coordinates_single_frame(self, selected_residues, client_BAT): - bat = BAT(selected_residues).run(start=1, stop=2, **client_BAT).results.bat + bat = ( + BAT(selected_residues) + .run(start=1, stop=2, **client_BAT) + .results.bat + ) test_bat = [np.load(BATArray)[1]] assert_allclose( bat, test_bat, rtol=0, atol=1.5e-5, - err_msg="error: BAT coordinates should match test values") + err_msg="error: BAT coordinates should match test values", + ) def test_bat_reconstruction(self, selected_residues, bat): R = BAT(selected_residues) XYZ = R.Cartesian(bat[0]) - assert_allclose(XYZ, selected_residues.positions, rtol=0, atol=1.5e-5, - err_msg="error: Reconstructed Cartesian coordinates " + \ - "don't match original") + assert_allclose( + XYZ, + selected_residues.positions, + rtol=0, + atol=1.5e-5, + err_msg="error: Reconstructed Cartesian coordinates " + + "don't match original", + ) def test_bat_IO(self, bat_npz, selected_residues, bat): R2 = BAT(selected_residues, filename=bat_npz) @@ -98,7 +119,8 @@ def test_bat_IO(self, bat_npz, selected_residues, bat): test_bat, rtol=0, atol=1.5e-5, - err_msg="error: Loaded BAT coordinates should match test values") + err_msg="error: Loaded BAT coordinates should match test values", + ) def test_bat_nobonds(self): u = mda.Universe(XYZ_mini) @@ -107,28 +129,29 @@ def test_bat_nobonds(self): Z = BAT(u.atoms) def test_bat_bad_initial_atom(self, selected_residues): - errmsg = 'Initial atom is not a terminal atom' + errmsg = "Initial atom is not a terminal atom" with pytest.raises(ValueError, match=errmsg): - R = BAT(selected_residues, initial_atom = selected_residues[0]) + R = BAT(selected_residues, initial_atom=selected_residues[0]) def test_bat_disconnected_atom_group(self): u = mda.Universe(PSF, DCD) - selected_residues = u.select_atoms("resid 1-3") + \ - u.select_atoms("resid 5-7") - errmsg = 'Additional torsions not found.' + selected_residues = u.select_atoms("resid 1-3") + u.select_atoms( + "resid 5-7" + ) + errmsg = "Additional torsions not found." with pytest.raises(ValueError, match=errmsg): R = BAT(selected_residues) def test_bat_multifragments_atomgroup(self): u = mda.Universe(TPR, XTC) - errmsg = 'AtomGroup has more than one molecule' + errmsg = "AtomGroup has more than one molecule" with pytest.raises(ValueError, match=errmsg): - BAT(u.select_atoms('resname SOL')) + BAT(u.select_atoms("resname SOL")) def test_bat_incorrect_dims(self, bat_npz): u = mda.Universe(PSF, DCD) selected_residues = u.select_atoms("resid 1-3") - errmsg = 'Dimensions of array in loaded file' + errmsg = "Dimensions of array in loaded file" with pytest.raises(ValueError, match=errmsg): R = BAT(selected_residues, filename=bat_npz) @@ -137,6 +160,9 @@ def test_Cartesian_does_not_modify_input(self, selected_residues, bat): pre_transformation = copy.deepcopy(bat[0]) R.Cartesian(bat[0]) assert_allclose( - pre_transformation, bat[0], rtol=0, atol=1.5e-7, - err_msg="BAT.Cartesian modified input data" + pre_transformation, + bat[0], + rtol=0, + atol=1.5e-7, + err_msg="BAT.Cartesian modified input data", ) diff --git a/testsuite/MDAnalysisTests/analysis/test_contacts.py b/testsuite/MDAnalysisTests/analysis/test_contacts.py index 6b416e27f8e..7f4a5b69ecc 100644 --- a/testsuite/MDAnalysisTests/analysis/test_contacts.py +++ b/testsuite/MDAnalysisTests/analysis/test_contacts.py @@ -21,32 +21,28 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # import warnings + import MDAnalysis as mda +import numpy as np import pytest from MDAnalysis.analysis import contacts from MDAnalysis.analysis.distances import distance_array - -from numpy.testing import ( - assert_equal, - assert_array_equal, - assert_allclose, -) -import numpy as np +from numpy.testing import assert_allclose, assert_array_equal, assert_equal from MDAnalysisTests.datafiles import ( - PSF, DCD, + PSF, TPR, XTC, + contacts_file, contacts_villin_folded, contacts_villin_unfolded, - contacts_file ) def test_soft_cut_q(): # just check some of the extremal points - assert contacts.soft_cut_q([0], [0]) == .5 + assert contacts.soft_cut_q([0], [0]) == 0.5 assert_allclose(contacts.soft_cut_q([100], [0]), 0, rtol=0, atol=1.5e-7) assert_allclose(contacts.soft_cut_q([-100], [0]), 1, rtol=0, atol=1.5e-7) @@ -58,8 +54,10 @@ def test_soft_cut_q_folded(): # indices have been stored 1 indexed indices = contacts_data[:, :2].astype(int) - 1 - r = np.linalg.norm(u.atoms.positions[indices[:, 0]] - - u.atoms.positions[indices[:, 1]], axis=1) + r = np.linalg.norm( + u.atoms.positions[indices[:, 0]] - u.atoms.positions[indices[:, 1]], + axis=1, + ) r0 = contacts_data[:, 2] beta = 5.0 @@ -76,8 +74,10 @@ def test_soft_cut_q_unfolded(): # indices have been stored 1 indexed indices = contacts_data[:, :2].astype(int) - 1 - r = np.linalg.norm(u.atoms.positions[indices[:, 0]] - - u.atoms.positions[indices[:, 1]], axis=1) + r = np.linalg.norm( + u.atoms.positions[indices[:, 0]] - u.atoms.positions[indices[:, 1]], + axis=1, + ) r0 = contacts_data[:, 2] beta = 5.0 @@ -87,25 +87,25 @@ def test_soft_cut_q_unfolded(): assert_allclose(Q.mean(), 0.0, rtol=0, atol=1.5e-1) -@pytest.mark.parametrize('r, cutoff, expected_value', [ - ([1], 2, 1), - ([2], 1, 0), - ([2, 0.5], 1, 0.5), - ([2, 3], [3, 4], 1), - ([4, 5], [3, 4], 0) - -]) +@pytest.mark.parametrize( + "r, cutoff, expected_value", + [ + ([1], 2, 1), + ([2], 1, 0), + ([2, 0.5], 1, 0.5), + ([2, 3], [3, 4], 1), + ([4, 5], [3, 4], 0), + ], +) def test_hard_cut_q(r, cutoff, expected_value): # just check some extremal points assert contacts.hard_cut_q(r, cutoff) == expected_value -@pytest.mark.parametrize('r, r0, radius, expected_value', [ - ([1], None, 2, 1), - ([2], None, 1, 0), - ([2, 0.5], None, 1, 0.5) - -]) +@pytest.mark.parametrize( + "r, r0, radius, expected_value", + [([1], None, 2, 1), ([2], None, 1, 0), ([2, 0.5], None, 1, 0.5)], +) def test_radius_cut_q(r, r0, radius, expected_value): # check some extremal points assert contacts.radius_cut_q(r, r0, radius) == expected_value @@ -126,7 +126,7 @@ def test_contact_matrix(): def test_new_selection(): u = mda.Universe(PSF, DCD) - selections = ('all', ) + selections = ("all",) sel = contacts._new_selections(u, selections, -1)[0] u.trajectory[-1] assert_array_equal(sel.positions, u.atoms.positions) @@ -134,7 +134,7 @@ def test_new_selection(): def soft_cut(ref, u, selA, selB, radius=4.5, beta=5.0, lambda_constant=1.8): """ - Reference implementation for testing + Reference implementation for testing """ # reference groups A and B from selection strings refA, refB = ref.select_atoms(selA), ref.select_atoms(selB) @@ -171,8 +171,13 @@ def universe(): return mda.Universe(PSF, DCD) def _run_Contacts( - self, universe, client_Contacts, 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,13 +186,13 @@ def _run_Contacts( select=(self.sel_acidic, self.sel_basic), refgroup=(acidic, basic), radius=6.0, - **kwargs + **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): """Test if Contacts._get_atomgroup() can take both string and AtomGroup - as selections. + as selections. """ ag = universe.select_atoms(seltxt) @@ -197,8 +202,7 @@ def test_select_valid_types(self, universe, seltxt): assert ag_from_string == ag_from_ag def test_contacts_selections(self, universe, client_Contacts): - """Test if Contacts can take both string and AtomGroup as selections. - """ + """Test if Contacts can take both string and AtomGroup as selections.""" aga = universe.select_atoms(self.sel_acidic) agb = universe.select_atoms(self.sel_basic) @@ -207,8 +211,9 @@ def test_contacts_selections(self, universe, client_Contacts): ) csel = contacts.Contacts( - universe, select=(self.sel_acidic, self.sel_basic), - refgroup=(aga, agb) + universe, + select=(self.sel_acidic, self.sel_basic), + refgroup=(aga, agb), ) cag.run(**client_Contacts) @@ -247,8 +252,11 @@ def test_end_zero(self, universe, client_Contacts): def test_slicing(self, universe, client_Contacts): start, stop, step = 10, 30, 5 CA1 = self._run_Contacts( - universe, client_Contacts=client_Contacts, - start=start, stop=stop, step=step + 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) @@ -261,14 +269,15 @@ def test_villin_folded(self, client_Contacts): grF = f.select_atoms(sel) - q = contacts.Contacts(u, - select=(sel, sel), - refgroup=(grF, grF), - method="soft_cut") + q = contacts.Contacts( + u, select=(sel, sel), refgroup=(grF, grF), method="soft_cut" + ) 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) + assert_allclose( + q.results.timeseries[:, 1], results[:, 1], rtol=0, atol=1.5e-7 + ) def test_villin_unfolded(self, client_Contacts): # both folded @@ -278,39 +287,46 @@ def test_villin_unfolded(self, client_Contacts): grF = f.select_atoms(sel) - q = contacts.Contacts(u, - select=(sel, sel), - refgroup=(grF, grF), - method="soft_cut") + q = contacts.Contacts( + u, select=(sel, sel), refgroup=(grF, grF), method="soft_cut" + ) 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) + assert_allclose( + q.results.timeseries[:, 1], results[:, 1], rtol=0, atol=1.5e-7 + ) 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, - 0.48543689, 0.46601942, 0.51456311, 0.49514563, 0.49514563, - 0.45631068, 0.47572816, 0.49514563, 0.50485437, 0.53398058, - 0.50485437, 0.51456311, 0.51456311, 0.49514563, 0.49514563, - 0.54368932, 0.50485437, 0.48543689, 0.55339806, 0.45631068, - 0.46601942, 0.53398058, 0.53398058, 0.46601942, 0.52427184, - 0.45631068, 0.46601942, 0.47572816, 0.46601942, 0.45631068, - 0.47572816, 0.45631068, 0.48543689, 0.4368932, 0.4368932, - 0.45631068, 0.50485437, 0.41747573, 0.4368932, 0.51456311, - 0.47572816, 0.46601942, 0.46601942, 0.47572816, 0.47572816, - 0.46601942, 0.45631068, 0.44660194, 0.47572816, 0.48543689, - 0.47572816, 0.42718447, 0.40776699, 0.37864078, 0.42718447, - 0.45631068, 0.4368932, 0.4368932, 0.45631068, 0.4368932, - 0.46601942, 0.45631068, 0.48543689, 0.44660194, 0.44660194, - 0.44660194, 0.42718447, 0.45631068, 0.44660194, 0.48543689, - 0.48543689, 0.44660194, 0.4368932, 0.40776699, 0.41747573, - 0.48543689, 0.45631068, 0.46601942, 0.47572816, 0.51456311, - 0.45631068, 0.37864078, 0.42718447] + # fmt: off + 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, + 0.48543689, 0.46601942, 0.51456311, 0.49514563, 0.49514563, + 0.45631068, 0.47572816, 0.49514563, 0.50485437, 0.53398058, + 0.50485437, 0.51456311, 0.51456311, 0.49514563, 0.49514563, + 0.54368932, 0.50485437, 0.48543689, 0.55339806, 0.45631068, + 0.46601942, 0.53398058, 0.53398058, 0.46601942, 0.52427184, + 0.45631068, 0.46601942, 0.47572816, 0.46601942, 0.45631068, + 0.47572816, 0.45631068, 0.48543689, 0.4368932, 0.4368932, + 0.45631068, 0.50485437, 0.41747573, 0.4368932, 0.51456311, + 0.47572816, 0.46601942, 0.46601942, 0.47572816, 0.47572816, + 0.46601942, 0.45631068, 0.44660194, 0.47572816, 0.48543689, + 0.47572816, 0.42718447, 0.40776699, 0.37864078, 0.42718447, + 0.45631068, 0.4368932, 0.4368932, 0.45631068, 0.4368932, + 0.46601942, 0.45631068, 0.48543689, 0.44660194, 0.44660194, + 0.44660194, 0.42718447, 0.45631068, 0.44660194, 0.48543689, + 0.48543689, 0.44660194, 0.4368932, 0.40776699, 0.41747573, + 0.48543689, 0.45631068, 0.46601942, 0.47572816, 0.51456311, + 0.45631068, 0.37864078, 0.42718447, + ] + # fmt: on assert len(ca.results.timeseries) == len(expected) - assert_allclose(ca.results.timeseries[:, 1], expected, rtol=0, atol=1.5e-7) + assert_allclose( + ca.results.timeseries[:, 1], expected, rtol=0, atol=1.5e-7 + ) def test_radius_cut_method(self, universe, client_Contacts): acidic = universe.select_atoms(self.sel_acidic) @@ -320,7 +336,9 @@ def test_radius_cut_method(self, universe, client_Contacts): expected = [] for ts in universe.trajectory: r = contacts.distance_array(acidic.positions, basic.positions) - expected.append(contacts.radius_cut_q(r[initial_contacts], None, radius=6.0)) + expected.append( + contacts.radius_cut_q(r[initial_contacts], None, radius=6.0) + ) ca = self._run_Contacts( universe, client_Contacts=client_Contacts, method="radius_cut" @@ -333,23 +351,27 @@ def _is_any_closer(r, r0, dist=2.5): def test_own_method(self, universe, client_Contacts): ca = self._run_Contacts( - universe, client_Contacts=client_Contacts, - method=self._is_any_closer + 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., - 0., 1., 1., 0., 1., 1., 1., 0., 0., 0., 0., 1., 0., - 0., 1., 0., 1., 1., 1., 0., 1., 0., 0., 1., 1., 1., - 0., 1., 0., 1., 1., 0., 0., 0., 1., 1., 1., 0., 0., - 1., 0., 1., 1., 1., 1., 1., 1., 0., 1., 1., 0., 1., - 0., 0., 1., 1., 0., 0., 1., 1., 1., 0., 1., 0., 0., - 1., 0., 1., 1., 1., 1., 1.] + # fmt: off + 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., 0., 1., 1., 0., 1., 1., + 1., 0., 0., 0., 0., 1., 0., 0., 1., 0., 1., 1., 1., 0., 1., 0., + 0., 1., 1., 1., 0., 1., 0., 1., 1., 0., 0., 0., 1., 1., 1., 0., + 0., 1., 0., 1., 1., 1., 1., 1., 1., 0., 1., 1., 0., 1., 0., 0., + 1., 1., 0., 0., 1., 1., 1., 0., 1., 0., 0., 1., 0., 1., 1., 1., + 1., 1., + ] + # fmt: on assert_array_equal(ca.results.timeseries[:, 1], bound_expected) @staticmethod def _weird_own_method(r, r0): - return 'aaa' + return "aaa" def test_own_method_no_array_cast(self, universe, client_Contacts): with pytest.raises(ValueError): @@ -366,23 +388,59 @@ def test_non_callable_method(self, universe, client_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, - 0.42223239, 0.41354071, 0.43641354, 0.41216834, 0.38334858]), - (False, [1., 0.42327791, 0.39192399, 0.40950119, 0.40902613, - 0.42470309, 0.41140143, 0.42897862, 0.41472684, 0.38574822]) - ]) + @pytest.mark.parametrize( + "pbc,expected", + [ + ( + True, + [ + 1.0, + 0.43138152, + 0.3989021, + 0.43824337, + 0.41948765, + 0.42223239, + 0.41354071, + 0.43641354, + 0.41216834, + 0.38334858, + ], + ), + ( + False, + [ + 1.0, + 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, client_Contacts): u = mda.Universe(TPR, XTC) sel_basic = "(resname ARG LYS)" sel_acidic = "(resname ASP GLU)" acidic = u.select_atoms(sel_acidic) basic = u.select_atoms(sel_basic) - - r = contacts.Contacts(u, select=(sel_acidic, sel_basic), - refgroup=(acidic, basic), radius=6.0, pbc=pbc) + + r = contacts.Contacts( + u, + select=(sel_acidic, sel_basic), + refgroup=(acidic, basic), + radius=6.0, + pbc=pbc, + ) r.run(**client_Contacts) - assert_allclose(r.results.timeseries[:, 1], expected,rtol=0, atol=1.5e-7) + assert_allclose( + r.results.timeseries[:, 1], expected, rtol=0, atol=1.5e-7 + ) def test_warn_deprecated_attr(self, universe, client_Contacts): """Test for warning message emitted on using deprecated `timeseries` @@ -394,13 +452,14 @@ def test_warn_deprecated_attr(self, universe, client_Contacts): with pytest.warns(DeprecationWarning, match=wmsg): assert_equal(CA1.timeseries, CA1.results.timeseries) - @pytest.mark.parametrize("datafiles, expected", [((PSF, DCD), 0), - ([TPR, XTC], 41814)]) + @pytest.mark.parametrize( + "datafiles, expected", [((PSF, DCD), 0), ([TPR, XTC], 41814)] + ) def test_n_initial_contacts(self, datafiles, expected): """Test for n_initial_contacts attribute""" u = mda.Universe(*datafiles) - select = ('protein', 'not protein') - refgroup = (u.select_atoms('protein'), u.select_atoms('not protein')) + select = ("protein", "not protein") + refgroup = (u.select_atoms("protein"), u.select_atoms("not protein")) r = contacts.Contacts(u, select=select, refgroup=refgroup) assert_equal(r.n_initial_contacts, expected) @@ -408,49 +467,61 @@ def test_n_initial_contacts(self, datafiles, expected): def test_q1q2(client_Contacts): u = mda.Universe(PSF, DCD) - q1q2 = contacts.q1q2(u, 'name CA', radius=8) + q1q2 = contacts.q1q2(u, "name CA", radius=8) 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, - 0.95367847, 0.96276113, 0.9582198, 0.95186194, 0.95367847, - 0.95095368, 0.94187103, 0.95186194, 0.94277929, 0.94187103, - 0.9373297, 0.93642144, 0.93097184, 0.93914623, 0.93278837, - 0.93188011, 0.9373297, 0.93097184, 0.93188011, 0.92643052, - 0.92824705, 0.92915531, 0.92643052, 0.92461399, 0.92279746, - 0.92643052, 0.93278837, 0.93188011, 0.93369664, 0.9346049, - 0.9373297, 0.94096276, 0.9400545, 0.93642144, 0.9373297, - 0.9373297, 0.9400545, 0.93006358, 0.9400545, 0.93823797, - 0.93914623, 0.93278837, 0.93097184, 0.93097184, 0.92733878, - 0.92824705, 0.92279746, 0.92824705, 0.91825613, 0.92733878, - 0.92643052, 0.92733878, 0.93278837, 0.92733878, 0.92824705, - 0.93097184, 0.93278837, 0.93914623, 0.93097184, 0.9373297, - 0.92915531, 0.93188011, 0.93551317, 0.94096276, 0.93642144, - 0.93642144, 0.9346049, 0.93369664, 0.93369664, 0.93278837, - 0.93006358, 0.93278837, 0.93006358, 0.9346049, 0.92824705, - 0.93097184, 0.93006358, 0.93188011, 0.93278837, 0.93006358, - 0.92915531, 0.92824705, 0.92733878, 0.92643052, 0.93188011, - 0.93006358, 0.9346049, 0.93188011] - assert_allclose(q1q2.results.timeseries[:, 1], q1_expected, rtol=0, atol=1.5e-7) - - q2_expected = [0.94649446, 0.94926199, 0.95295203, 0.95110701, 0.94833948, - 0.95479705, 0.94926199, 0.9501845, 0.94926199, 0.95387454, - 0.95202952, 0.95110701, 0.94649446, 0.94095941, 0.94649446, - 0.9400369, 0.94464945, 0.95202952, 0.94741697, 0.94649446, - 0.94188192, 0.94188192, 0.93911439, 0.94464945, 0.9400369, - 0.94095941, 0.94372694, 0.93726937, 0.93819188, 0.93357934, - 0.93726937, 0.93911439, 0.93911439, 0.93450185, 0.93357934, - 0.93265683, 0.93911439, 0.94372694, 0.93911439, 0.94649446, - 0.94833948, 0.95110701, 0.95110701, 0.95295203, 0.94926199, - 0.95110701, 0.94926199, 0.94741697, 0.95202952, 0.95202952, - 0.95202952, 0.94741697, 0.94741697, 0.94926199, 0.94280443, - 0.94741697, 0.94833948, 0.94833948, 0.9400369, 0.94649446, - 0.94741697, 0.94926199, 0.95295203, 0.94926199, 0.9501845, - 0.95664207, 0.95756458, 0.96309963, 0.95756458, 0.96217712, - 0.95756458, 0.96217712, 0.96586716, 0.96863469, 0.96494465, - 0.97232472, 0.97140221, 0.9695572, 0.97416974, 0.9695572, - 0.96217712, 0.96771218, 0.9704797, 0.96771218, 0.9695572, - 0.97140221, 0.97601476, 0.97693727, 0.98154982, 0.98431734, - 0.97601476, 0.9797048, 0.98154982, 0.98062731, 0.98431734, - 0.98616236, 0.9898524, 1.] - assert_allclose(q1q2.results.timeseries[:, 2], q2_expected, rtol=0, atol=1.5e-7) + # fmt: off + q1_expected = [ + 1.0, 0.98092643, 0.97366031, 0.97275204, 0.97002725, + 0.97275204, 0.96276113, 0.96730245, 0.9582198, 0.96185286, + 0.95367847, 0.96276113, 0.9582198, 0.95186194, 0.95367847, + 0.95095368, 0.94187103, 0.95186194, 0.94277929, 0.94187103, + 0.9373297, 0.93642144, 0.93097184, 0.93914623, 0.93278837, + 0.93188011, 0.9373297, 0.93097184, 0.93188011, 0.92643052, + 0.92824705, 0.92915531, 0.92643052, 0.92461399, 0.92279746, + 0.92643052, 0.93278837, 0.93188011, 0.93369664, 0.9346049, + 0.9373297, 0.94096276, 0.9400545, 0.93642144, 0.9373297, + 0.9373297, 0.9400545, 0.93006358, 0.9400545, 0.93823797, + 0.93914623, 0.93278837, 0.93097184, 0.93097184, 0.92733878, + 0.92824705, 0.92279746, 0.92824705, 0.91825613, 0.92733878, + 0.92643052, 0.92733878, 0.93278837, 0.92733878, 0.92824705, + 0.93097184, 0.93278837, 0.93914623, 0.93097184, 0.9373297, + 0.92915531, 0.93188011, 0.93551317, 0.94096276, 0.93642144, + 0.93642144, 0.9346049, 0.93369664, 0.93369664, 0.93278837, + 0.93006358, 0.93278837, 0.93006358, 0.9346049, 0.92824705, + 0.93097184, 0.93006358, 0.93188011, 0.93278837, 0.93006358, + 0.92915531, 0.92824705, 0.92733878, 0.92643052, 0.93188011, + 0.93006358, 0.9346049, 0.93188011, + ] + # fmt: on + assert_allclose( + q1q2.results.timeseries[:, 1], q1_expected, rtol=0, atol=1.5e-7 + ) + + # fmt: off + q2_expected = [ + 0.94649446, 0.94926199, 0.95295203, 0.95110701, 0.94833948, + 0.95479705, 0.94926199, 0.9501845, 0.94926199, 0.95387454, + 0.95202952, 0.95110701, 0.94649446, 0.94095941, 0.94649446, + 0.9400369, 0.94464945, 0.95202952, 0.94741697, 0.94649446, + 0.94188192, 0.94188192, 0.93911439, 0.94464945, 0.9400369, + 0.94095941, 0.94372694, 0.93726937, 0.93819188, 0.93357934, + 0.93726937, 0.93911439, 0.93911439, 0.93450185, 0.93357934, + 0.93265683, 0.93911439, 0.94372694, 0.93911439, 0.94649446, + 0.94833948, 0.95110701, 0.95110701, 0.95295203, 0.94926199, + 0.95110701, 0.94926199, 0.94741697, 0.95202952, 0.95202952, + 0.95202952, 0.94741697, 0.94741697, 0.94926199, 0.94280443, + 0.94741697, 0.94833948, 0.94833948, 0.9400369, 0.94649446, + 0.94741697, 0.94926199, 0.95295203, 0.94926199, 0.9501845, + 0.95664207, 0.95756458, 0.96309963, 0.95756458, 0.96217712, + 0.95756458, 0.96217712, 0.96586716, 0.96863469, 0.96494465, + 0.97232472, 0.97140221, 0.9695572, 0.97416974, 0.9695572, + 0.96217712, 0.96771218, 0.9704797, 0.96771218, 0.9695572, + 0.97140221, 0.97601476, 0.97693727, 0.98154982, 0.98431734, + 0.97601476, 0.9797048, 0.98154982, 0.98062731, 0.98431734, + 0.98616236, 0.9898524, 1.0, + ] + # fmt: on + assert_allclose( + q1q2.results.timeseries[:, 2], q2_expected, rtol=0, atol=1.5e-7 + ) diff --git a/testsuite/MDAnalysisTests/analysis/test_data.py b/testsuite/MDAnalysisTests/analysis/test_data.py index 44853346c85..d3d570d6c46 100644 --- a/testsuite/MDAnalysisTests/analysis/test_data.py +++ b/testsuite/MDAnalysisTests/analysis/test_data.py @@ -23,9 +23,15 @@ from numpy.testing import assert_equal import pytest + def test_all_exports(): from MDAnalysis.analysis.data import filenames - missing = [name for name in dir(filenames) - if - not name.startswith('_') and name not in filenames.__all__ and name != 'absolute_import'] + + missing = [ + name + for name in dir(filenames) + if not name.startswith("_") + and name not in filenames.__all__ + and name != "absolute_import" + ] assert_equal(missing, [], err_msg="Variables need to be added to __all__.") diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index 68dbed6839d..3547ff9fe91 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -41,32 +41,35 @@ class TestDensity(object): nbins = 3, 4, 5 counts = 100 - Lmax = 10. + Lmax = 10.0 - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def bins(self): return [np.linspace(0, self.Lmax, n + 1) for n in self.nbins] @pytest.fixture() def h_and_edges(self, bins): return np.histogramdd( - self.Lmax * np.sin( - np.linspace(0, 1, self.counts * 3)).reshape(self.counts, 3), - bins=bins) + self.Lmax + * np.sin(np.linspace(0, 1, self.counts * 3)).reshape( + self.counts, 3 + ), + bins=bins, + ) @pytest.fixture() def D(self, h_and_edges): h, edges = h_and_edges - d = density.Density(h, edges, parameters={'isDensity': False}, - units={'length': 'A'}) + d = density.Density( + h, edges, parameters={"isDensity": False}, units={"length": "A"} + ) d.make_density() return d @pytest.fixture() def D1(self, h_and_edges): h, edges = h_and_edges - d = density.Density(h, edges, parameters={}, - units={}) + d = density.Density(h, edges, parameters={}, units={}) return d def test_shape(self, D): @@ -74,17 +77,19 @@ def test_shape(self, D): def test_edges(self, bins, D): for dim, (edges, fixture) in enumerate(zip(D.edges, bins)): - assert_almost_equal(edges, fixture, - err_msg="edges[{0}] mismatch".format(dim)) + assert_almost_equal( + edges, fixture, err_msg="edges[{0}] mismatch".format(dim) + ) def test_midpoints(self, bins, D): - midpoints = [0.5*(b[:-1] + b[1:]) for b in bins] + midpoints = [0.5 * (b[:-1] + b[1:]) for b in bins] for dim, (mp, fixture) in enumerate(zip(D.midpoints, midpoints)): - assert_almost_equal(mp, fixture, - err_msg="midpoints[{0}] mismatch".format(dim)) + assert_almost_equal( + mp, fixture, err_msg="midpoints[{0}] mismatch".format(dim) + ) def test_delta(self, D): - deltas = np.array([self.Lmax])/np.array(self.nbins) + deltas = np.array([self.Lmax]) / np.array(self.nbins) assert_almost_equal(D.delta, deltas) def test_grid(self, D): @@ -93,12 +98,12 @@ def test_grid(self, D): assert_almost_equal(D.grid.sum() * dV, self.counts) def test_origin(self, bins, D): - midpoints = [0.5*(b[:-1] + b[1:]) for b in bins] + midpoints = [0.5 * (b[:-1] + b[1:]) for b in bins] origin = [m[0] for m in midpoints] assert_almost_equal(D.origin, origin) def test_check_set_unit_keyerror(self, D): - units = {'weight': 'A'} + units = {"weight": "A"} with pytest.raises(ValueError): D._check_set_unit(units) @@ -108,23 +113,23 @@ def test_check_set_unit_attributeError(self, D): D._check_set_unit(units) def test_check_set_unit_nolength(self, D): - del D.units['length'] - units = {'density': 'A^{-3}'} + del D.units["length"] + units = {"density": "A^{-3}"} with pytest.raises(ValueError): D._check_set_unit(units) def test_check_set_density_none(self, D1): - units = {'density': None} + units = {"density": None} D1._check_set_unit(units) - assert D1.units['density'] is None + assert D1.units["density"] is None def test_check_set_density_not_in_units(self, D1): - del D1.units['density'] + del D1.units["density"] D1._check_set_unit({}) - assert D1.units['density'] is None + assert D1.units["density"] is None def test_parameters_isdensity(self, D): - with pytest.warns(UserWarning, match='Running make_density()'): + with pytest.warns(UserWarning, match="Running make_density()"): D.make_density() def test_check_convert_density_grid_not_density(self, D1): @@ -132,65 +137,64 @@ def test_check_convert_density_grid_not_density(self, D1): D1.convert_density() def test_check_convert_density_value_error(self, D): - unit = 'A^{-2}' + unit = "A^{-2}" with pytest.raises(ValueError, match="The name of the unit"): D.convert_density(unit) def test_check_convert_density_units_same_density_units(self, D): - unit = 'A^{-3}' + unit = "A^{-3}" D_orig = copy.deepcopy(D) D.convert_density(unit) - assert D.units['density'] == D_orig.units['density'] == unit + assert D.units["density"] == D_orig.units["density"] == unit assert_almost_equal(D.grid, D_orig.grid) def test_check_convert_density_units_density(self, D): - unit = 'nm^{-3}' + unit = "nm^{-3}" D_orig = copy.deepcopy(D) D.convert_density(unit) - assert D.units['density'] == 'nm^{-3}' + assert D.units["density"] == "nm^{-3}" assert_almost_equal(D.grid, 10**3 * D_orig.grid) def test_convert_length_same_length_units(self, D): - unit = 'A' + unit = "A" D_orig = copy.deepcopy(D) D.convert_length(unit) - assert D.units['length'] == D_orig.units['length'] == unit + assert D.units["length"] == D_orig.units["length"] == unit assert_almost_equal(D.grid, D_orig.grid) def test_convert_length_other_length_units(self, D): - unit = 'nm' + unit = "nm" D_orig = copy.deepcopy(D) D.convert_length(unit) - assert D.units['length'] == unit + assert D.units["length"] == unit assert_almost_equal(D.grid, D_orig.grid) def test_repr(self, D, D1): - assert str(D) == '' - assert str(D1) == '' + assert str(D) == "" + assert str(D1) == "" def test_check_convert_length_edges(self, D): D1 = copy.deepcopy(D) - unit = 'nm' + unit = "nm" D.convert_length(unit) for prev_edge, conv_edge in zip(D1.edges, D.edges): - assert_almost_equal(prev_edge, 10*conv_edge) + assert_almost_equal(prev_edge, 10 * conv_edge) def test_check_convert_density_edges(self, D): - unit = 'nm^{-3}' + unit = "nm^{-3}" D_orig = copy.deepcopy(D) D.convert_density(unit) for new_den, orig_den in zip(D.edges, D_orig.edges): assert_almost_equal(new_den, orig_den) - @pytest.mark.parametrize('dxtype', - ("float", "double", "int", "byte")) + @pytest.mark.parametrize("dxtype", ("float", "double", "int", "byte")) def test_export_types(self, D, dxtype, tmpdir, outfile="density.dx"): with tmpdir.as_cwd(): D.export(outfile, type=dxtype) dx = gridData.OpenDX.field(0) dx.read(outfile) - data = dx.components['data'] + data = dx.components["data"] assert data.type == dxtype @@ -198,35 +202,48 @@ class DensityParameters(object): topology = TPR trajectory = XTC delta = 2.0 - selections = {'none': "resname None", - 'static': "name OW", - 'dynamic': "name OW and around 4 (protein and resid 1-10)", - 'solute': "protein and not name H*", - } - references = {'static': - {'meandensity': 0.016764271713091212, }, - 'static_sliced': - {'meandensity': 0.016764270747693617, }, - 'static_defined': - {'meandensity': 0.0025000000000000005, }, - 'static_defined_unequal': - {'meandensity': 0.006125, }, - 'dynamic': - {'meandensity': 0.0012063418843728784, }, - 'notwithin': - {'meandensity': 0.015535385132107926, }, - } - cutoffs = {'notwithin': 4.0, } - gridcenters = {'static_defined': np.array([56.0, 45.0, 35.0]), - 'error1': np.array([56.0, 45.0]), - 'error2': [56.0, 45.0, "MDAnalysis"], - } + selections = { + "none": "resname None", + "static": "name OW", + "dynamic": "name OW and around 4 (protein and resid 1-10)", + "solute": "protein and not name H*", + } + references = { + "static": { + "meandensity": 0.016764271713091212, + }, + "static_sliced": { + "meandensity": 0.016764270747693617, + }, + "static_defined": { + "meandensity": 0.0025000000000000005, + }, + "static_defined_unequal": { + "meandensity": 0.006125, + }, + "dynamic": { + "meandensity": 0.0012063418843728784, + }, + "notwithin": { + "meandensity": 0.015535385132107926, + }, + } + cutoffs = { + "notwithin": 4.0, + } + gridcenters = { + "static_defined": np.array([56.0, 45.0, 35.0]), + "error1": np.array([56.0, 45.0]), + "error2": [56.0, 45.0, "MDAnalysis"], + } precision = 5 - outfile = 'density.dx' + outfile = "density.dx" @pytest.fixture() def universe(self): - return mda.Universe(self.topology, self.trajectory, tpr_resid_from_one=False) + return mda.Universe( + self.topology, self.trajectory, tpr_resid_from_one=False + ) class TestDensityAnalysis(DensityParameters): @@ -237,37 +254,42 @@ def check_DensityAnalysis( tmpdir, client_DensityAnalysis, runargs=None, - **kwargs + **kwargs, ): runargs = runargs if runargs else {} with tmpdir.as_cwd(): D = density.DensityAnalysis(ag, delta=self.delta, **kwargs).run( **runargs, **client_DensityAnalysis ) - assert_almost_equal(D.results.density.grid.mean(), ref_meandensity, - err_msg="mean density does not match") + assert_almost_equal( + D.results.density.grid.mean(), + ref_meandensity, + err_msg="mean density does not match", + ) D.results.density.export(self.outfile) D2 = density.Density(self.outfile) assert_almost_equal( - D.results.density.grid, D2.grid, decimal=self.precision, - err_msg="DX export failed: different grid sizes" + D.results.density.grid, + D2.grid, + decimal=self.precision, + err_msg="DX export failed: different grid sizes", ) @pytest.mark.parametrize("mode", ("static", "dynamic")) def test_run(self, mode, universe, tmpdir, client_DensityAnalysis): - updating = (mode == "dynamic") + updating = mode == "dynamic" self.check_DensityAnalysis( universe.select_atoms(self.selections[mode], updating=updating), - self.references[mode]['meandensity'], + self.references[mode]["meandensity"], tmpdir=tmpdir, client_DensityAnalysis=client_DensityAnalysis, ) def test_sliced(self, universe, tmpdir, client_DensityAnalysis): self.check_DensityAnalysis( - universe.select_atoms(self.selections['static']), - self.references['static_sliced']['meandensity'], + universe.select_atoms(self.selections["static"]), + self.references["static_sliced"]["meandensity"], tmpdir=tmpdir, client_DensityAnalysis=client_DensityAnalysis, runargs=dict(start=1, stop=-1, step=2), @@ -278,11 +300,11 @@ def test_userdefn_eqbox(self, universe, tmpdir, client_DensityAnalysis): # Do not need to see UserWarning that box is too small warnings.simplefilter("ignore") self.check_DensityAnalysis( - universe.select_atoms(self.selections['static']), - self.references['static_defined']['meandensity'], + universe.select_atoms(self.selections["static"]), + self.references["static_defined"]["meandensity"], tmpdir=tmpdir, client_DensityAnalysis=client_DensityAnalysis, - gridcenter=self.gridcenters['static_defined'], + gridcenter=self.gridcenters["static_defined"], xdim=10.0, ydim=10.0, zdim=10.0, @@ -290,11 +312,11 @@ def test_userdefn_eqbox(self, universe, tmpdir, client_DensityAnalysis): def test_userdefn_neqbox(self, universe, tmpdir, client_DensityAnalysis): self.check_DensityAnalysis( - universe.select_atoms(self.selections['static']), - self.references['static_defined_unequal']['meandensity'], + universe.select_atoms(self.selections["static"]), + self.references["static_defined_unequal"]["meandensity"], tmpdir=tmpdir, client_DensityAnalysis=client_DensityAnalysis, - gridcenter=self.gridcenters['static_defined'], + gridcenter=self.gridcenters["static_defined"], xdim=10.0, ydim=15.0, zdim=20.0, @@ -312,8 +334,10 @@ def test_userdefn_boxshape(self, universe, client_DensityAnalysis): assert D.results.density.grid.shape == (8, 12, 17) def test_warn_userdefn_padding(self, universe, client_DensityAnalysis): - regex = (r"Box padding \(currently set at 1\.0\) is not used " - r"in user defined grids\.") + regex = ( + r"Box padding \(currently set at 1\.0\) is not used " + r"in user defined grids\." + ) with pytest.warns(UserWarning, match=regex): D = density.DensityAnalysis( universe.select_atoms(self.selections["static"]), @@ -326,8 +350,10 @@ def test_warn_userdefn_padding(self, universe, client_DensityAnalysis): ).run(step=5, **client_DensityAnalysis) def test_warn_userdefn_smallgrid(self, universe, client_DensityAnalysis): - regex = ("Atom selection does not fit grid --- " - "you may want to define a larger box") + regex = ( + "Atom selection does not fit grid --- " + "you may want to define a larger box" + ) with pytest.warns(UserWarning, match=regex): D = density.DensityAnalysis( universe.select_atoms(self.selections["static"]), @@ -343,7 +369,9 @@ def test_ValueError_userdefn_gridcenter_shape( self, universe, client_DensityAnalysis ): # Test len(gridcenter) != 3 - with pytest.raises(ValueError, match="Gridcenter must be a 3D coordinate"): + with pytest.raises( + ValueError, match="Gridcenter must be a 3D coordinate" + ): D = density.DensityAnalysis( universe.select_atoms(self.selections["static"]), delta=self.delta, @@ -357,7 +385,9 @@ def test_ValueError_userdefn_gridcenter_type( self, universe, client_DensityAnalysis ): # Test gridcenter includes non-numeric strings - with pytest.raises(ValueError, match="Gridcenter must be a 3D coordinate"): + with pytest.raises( + ValueError, match="Gridcenter must be a 3D coordinate" + ): D = density.DensityAnalysis( universe.select_atoms(self.selections["static"]), delta=self.delta, @@ -371,7 +401,7 @@ def test_ValueError_userdefn_gridcenter_missing( self, universe, client_DensityAnalysis ): # Test no gridcenter provided when grid dimensions are given - regex = ("Gridcenter or grid dimensions are not provided") + regex = "Gridcenter or grid dimensions are not provided" with pytest.raises(ValueError, match=regex): D = density.DensityAnalysis( universe.select_atoms(self.selections["static"]), @@ -381,10 +411,13 @@ def test_ValueError_userdefn_gridcenter_missing( zdim=10.0, ).run(step=5, **client_DensityAnalysis) - def test_ValueError_userdefn_xdim_type(self, universe, - client_DensityAnalysis): + def test_ValueError_userdefn_xdim_type( + self, universe, client_DensityAnalysis + ): # Test xdim != int or float - with pytest.raises(ValueError, match="xdim, ydim, and zdim must be numbers"): + with pytest.raises( + ValueError, match="xdim, ydim, and zdim must be numbers" + ): D = density.DensityAnalysis( universe.select_atoms(self.selections["static"]), delta=self.delta, @@ -394,10 +427,11 @@ def test_ValueError_userdefn_xdim_type(self, universe, gridcenter=self.gridcenters["static_defined"], ).run(step=5, **client_DensityAnalysis) - def test_ValueError_userdefn_xdim_nanvalue(self, universe, - client_DensityAnalysis): + def test_ValueError_userdefn_xdim_nanvalue( + self, universe, client_DensityAnalysis + ): # Test xdim set to NaN value - regex = ("Gridcenter or grid dimensions have NaN element") + regex = "Gridcenter or grid dimensions have NaN element" with pytest.raises(ValueError, match=regex): D = density.DensityAnalysis( universe.select_atoms(self.selections["static"]), @@ -409,10 +443,12 @@ def test_ValueError_userdefn_xdim_nanvalue(self, universe, ).run(step=5, **client_DensityAnalysis) def test_warn_noatomgroup(self, universe, client_DensityAnalysis): - regex = ("No atoms in AtomGroup at input time frame. " - "This may be intended; please ensure that " - "your grid selection covers the atomic " - "positions you wish to capture.") + regex = ( + "No atoms in AtomGroup at input time frame. " + "This may be intended; please ensure that " + "your grid selection covers the atomic " + "positions you wish to capture." + ) with pytest.warns(UserWarning, match=regex): D = density.DensityAnalysis( universe.select_atoms(self.selections["none"]), @@ -425,20 +461,24 @@ def test_warn_noatomgroup(self, universe, client_DensityAnalysis): ).run(step=5, **client_DensityAnalysis) def test_ValueError_noatomgroup(self, universe, client_DensityAnalysis): - with pytest.raises(ValueError, match="No atoms in AtomGroup at input" - " time frame. Grid for density" - " could not be automatically" - " generated. If this is" - " expected, a user" - " defined grid will " - "need to be provided instead."): + with pytest.raises( + ValueError, + match="No atoms in AtomGroup at input" + " time frame. Grid for density" + " could not be automatically" + " generated. If this is" + " expected, a user" + " defined grid will " + "need to be provided instead.", + ): D = density.DensityAnalysis( universe.select_atoms(self.selections["none"]) ).run(step=5, **client_DensityAnalysis) def test_warn_results_deprecated(self, universe, client_DensityAnalysis): D = density.DensityAnalysis( - universe.select_atoms(self.selections['static'])) + universe.select_atoms(self.selections["static"]) + ) D.run(stop=1, **client_DensityAnalysis) wmsg = "The `density` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): @@ -451,27 +491,30 @@ def test_density_analysis_conversion_default_unit(self): D.run() D.results.density.convert_density() + class TestGridImport(object): - @block_import('gridData') + @block_import("gridData") def test_absence_griddata(self): - sys.modules.pop('MDAnalysis.analysis.density', None) + sys.modules.pop("MDAnalysis.analysis.density", None) # if gridData package is missing an ImportError should be raised # at the module level of MDAnalysis.analysis.density with pytest.raises(ImportError): import MDAnalysis.analysis.density def test_presence_griddata(self): - sys.modules.pop('MDAnalysis.analysis.density', None) + sys.modules.pop("MDAnalysis.analysis.density", None) # no ImportError exception is raised when gridData is properly # imported by MDAnalysis.analysis.density # mock gridData in case there are testing scenarios where # it is not available mock = Mock() - with patch.dict('sys.modules', {'gridData': mock}): + with patch.dict("sys.modules", {"gridData": mock}): try: import MDAnalysis.analysis.density except ImportError: - pytest.fail(msg='''MDAnalysis.analysis.density should not raise - an ImportError if gridData is available.''') + pytest.fail( + msg="""MDAnalysis.analysis.density should not raise + an ImportError if gridData is available.""" + ) diff --git a/testsuite/MDAnalysisTests/analysis/test_dielectric.py b/testsuite/MDAnalysisTests/analysis/test_dielectric.py index a1a5ccc5062..45d8e722cdb 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dielectric.py +++ b/testsuite/MDAnalysisTests/analysis/test_dielectric.py @@ -41,7 +41,7 @@ def test_broken_molecules(self, ag): ag.wrap() eps = DielectricConstant(ag, make_whole=False).run() - assert_allclose(eps.results['eps_mean'], 721.711, rtol=1e-03) + assert_allclose(eps.results["eps_mean"], 721.711, rtol=1e-03) def test_broken_repaired_molecules(self, ag): # cut molecules apart @@ -50,21 +50,23 @@ def test_broken_repaired_molecules(self, ag): ag.wrap() eps = DielectricConstant(ag, make_whole=True).run() - assert_allclose(eps.results['eps_mean'], 5.088, rtol=1e-03) + assert_allclose(eps.results["eps_mean"], 5.088, rtol=1e-03) def test_temperature(self, ag): eps = DielectricConstant(ag, temperature=100).run() - assert_allclose(eps.results['eps_mean'], 9.621, rtol=1e-03) + assert_allclose(eps.results["eps_mean"], 9.621, rtol=1e-03) def test_non_charges(self): u = mda.Universe(DCD_TRICLINIC, to_guess=()) - with pytest.raises(NoDataError, - match="No charges defined given atomgroup."): + with pytest.raises( + NoDataError, match="No charges defined given atomgroup." + ): DielectricConstant(u.atoms).run() def test_non_neutral(self, ag): - with pytest.raises(NotImplementedError, - match="Analysis for non-neutral systems or"): + with pytest.raises( + NotImplementedError, match="Analysis for non-neutral systems or" + ): DielectricConstant(ag[:-1]).run() def test_free_charges(self, ag): diff --git a/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py b/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py index 11271fd8f4c..99e978b3ff8 100644 --- a/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py +++ b/testsuite/MDAnalysisTests/analysis/test_diffusionmap.py @@ -28,17 +28,17 @@ from numpy.testing import assert_array_almost_equal, assert_allclose -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return MDAnalysis.Universe(PDB, XTC) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dist(u): - return diffusionmap.DistanceMatrix(u, select='backbone') + return diffusionmap.DistanceMatrix(u, select="backbone") -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def dmap(dist): d_map = diffusionmap.DiffusionMap(dist) d_map.run() @@ -53,57 +53,74 @@ def test_eg(dist, dmap): def test_dist_weights(u): - backbone = u.select_atoms('backbone') + backbone = u.select_atoms("backbone") weights_atoms = np.ones(len(backbone.atoms)) - dist = diffusionmap.DistanceMatrix(u, - select='backbone', - weights=weights_atoms) + dist = diffusionmap.DistanceMatrix( + u, select="backbone", weights=weights_atoms + ) dist.run(step=3) dmap = diffusionmap.DiffusionMap(dist) dmap.run() assert_array_almost_equal(dmap.eigenvalues, [1, 1, 1, 1], 4) - assert_array_almost_equal(dmap._eigenvectors, - ([[0, 0, 1, 0], - [0, 0, 0, 1], - [-.707, -.707, 0, 0], - [.707, -.707, 0, 0]]), 2) + assert_array_almost_equal( + dmap._eigenvectors, + ( + [ + [0, 0, 1, 0], + [0, 0, 0, 1], + [-0.707, -0.707, 0, 0], + [0.707, -0.707, 0, 0], + ] + ), + 2, + ) def test_dist_weights_frames(u): - backbone = u.select_atoms('backbone') + backbone = u.select_atoms("backbone") weights_atoms = np.ones(len(backbone.atoms)) - dist = diffusionmap.DistanceMatrix(u, - select='backbone', - weights=weights_atoms) + dist = diffusionmap.DistanceMatrix( + u, select="backbone", weights=weights_atoms + ) frames = np.arange(len(u.trajectory)) dist.run(frames=frames[::3]) dmap = diffusionmap.DiffusionMap(dist) dmap.run() assert_array_almost_equal(dmap.eigenvalues, [1, 1, 1, 1], 4) - assert_array_almost_equal(dmap._eigenvectors, - ([[0, 0, 1, 0], - [0, 0, 0, 1], - [-.707, -.707, 0, 0], - [.707, -.707, 0, 0]]), 2) + assert_array_almost_equal( + dmap._eigenvectors, + ( + [ + [0, 0, 1, 0], + [0, 0, 0, 1], + [-0.707, -0.707, 0, 0], + [0.707, -0.707, 0, 0], + ] + ), + 2, + ) + def test_distvalues_ag_universe(u): - dist_universe = diffusionmap.DistanceMatrix(u, select='backbone').run() - ag = u.select_atoms('backbone') + dist_universe = diffusionmap.DistanceMatrix(u, select="backbone").run() + ag = u.select_atoms("backbone") dist_ag = diffusionmap.DistanceMatrix(ag).run() - assert_allclose(dist_universe.results.dist_matrix, - dist_ag.results.dist_matrix) + assert_allclose( + dist_universe.results.dist_matrix, dist_ag.results.dist_matrix + ) def test_distvalues_ag_select(u): - dist_universe = diffusionmap.DistanceMatrix(u, select='backbone').run() - ag = u.select_atoms('protein') - dist_ag = diffusionmap.DistanceMatrix(ag, select='backbone').run() - assert_allclose(dist_universe.results.dist_matrix, - dist_ag.results.dist_matrix) - + dist_universe = diffusionmap.DistanceMatrix(u, select="backbone").run() + ag = u.select_atoms("protein") + dist_ag = diffusionmap.DistanceMatrix(ag, select="backbone").run() + assert_allclose( + dist_universe.results.dist_matrix, dist_ag.results.dist_matrix + ) + def test_different_steps(u): - dmap = diffusionmap.DiffusionMap(u, select='backbone') + dmap = diffusionmap.DiffusionMap(u, select="backbone") dmap.run(step=3) assert dmap._eigenvectors.shape == (4, 4) @@ -118,7 +135,7 @@ def test_transform(u, dmap): def test_long_traj(u): - with pytest.warns(UserWarning, match='The distance matrix is very large'): + with pytest.warns(UserWarning, match="The distance matrix is very large"): dmap = diffusionmap.DiffusionMap(u) dmap._dist_matrix.run(stop=1) dmap._dist_matrix.n_frames = 5001 @@ -126,20 +143,21 @@ def test_long_traj(u): def test_updating_atomgroup(u): - with pytest.warns(UserWarning, match='U must be a static AtomGroup'): - resid_select = 'around 5 resname ALA' + with pytest.warns(UserWarning, match="U must be a static AtomGroup"): + resid_select = "around 5 resname ALA" ag = u.select_atoms(resid_select, updating=True) dmap = diffusionmap.DiffusionMap(ag) dmap.run() + def test_not_universe_atomgroup_error(u): trj_only = u.trajectory - with pytest.raises(ValueError, match='U is not a Universe or AtomGroup'): + with pytest.raises(ValueError, match="U is not a Universe or AtomGroup"): diffusionmap.DiffusionMap(trj_only) def test_DistanceMatrix_attr_warning(u): - dist = diffusionmap.DistanceMatrix(u, select='backbone').run(step=3) + dist = diffusionmap.DistanceMatrix(u, select="backbone").run(step=3) wmsg = f"The `dist_matrix` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): assert getattr(dist, "dist_matrix") is dist.results.dist_matrix diff --git a/testsuite/MDAnalysisTests/analysis/test_dihedrals.py b/testsuite/MDAnalysisTests/analysis/test_dihedrals.py index 767345bda7f..82625dfb934 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dihedrals.py +++ b/testsuite/MDAnalysisTests/analysis/test_dihedrals.py @@ -26,10 +26,19 @@ import pytest import MDAnalysis as mda -from MDAnalysisTests.datafiles import (GRO, XTC, TPR, DihedralArray, - DihedralsArray, RamaArray, GLYRamaArray, - JaninArray, LYSJaninArray, PDB_rama, - PDB_janin) +from MDAnalysisTests.datafiles import ( + GRO, + XTC, + TPR, + DihedralArray, + DihedralsArray, + RamaArray, + GLYRamaArray, + JaninArray, + LYSJaninArray, + PDB_rama, + PDB_janin, +) import MDAnalysis.analysis.dihedrals from MDAnalysis.analysis.dihedrals import Dihedral, Ramachandran, Janin @@ -39,10 +48,11 @@ class TestDihedral(object): @pytest.fixture() def atomgroup(self): u = mda.Universe(GRO, XTC) - ag = u.select_atoms("(resid 4 and name N CA C) or (resid 5 and name N)") + ag = u.select_atoms( + "(resid 4 and name N CA C) or (resid 5 and name N)" + ) return ag - 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 @@ -53,25 +63,39 @@ def test_dihedral(self, atomgroup, client_Dihedral): 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") + 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, client_Dihedral): - dihedral = Dihedral([atomgroup]).run(start=5, stop=6, **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") + 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, 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") + 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, client_Dihedral): with pytest.raises(ValueError): @@ -97,42 +121,64 @@ def rama_ref_array(self): 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, client_Ramachandran): + **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, client_Ramachandran + ): rama = Ramachandran(universe.select_atoms("protein")).run( - 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, client_Ramachandran): + 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, client_Ramachandran + ): rama = Ramachandran(universe.select_atoms("resname GLY")).run( - **client_Ramachandran) + **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") + 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, client_Ramachandran): with pytest.raises(ValueError): - rama = Ramachandran(universe.select_atoms("resid 220"), - check_protein=True).run(**client_Ramachandran) + rama = Ramachandran( + universe.select_atoms("resid 220"), check_protein=True + ).run(**client_Ramachandran) def test_outside_protein_unchecked(self, universe, client_Ramachandran): - rama = Ramachandran(universe.select_atoms("resid 220"), - check_protein=False).run(**client_Ramachandran) + rama = Ramachandran( + universe.select_atoms("resid 220"), check_protein=False + ).run(**client_Ramachandran) def test_protein_ends(self, universe): with pytest.warns(UserWarning) as record: - rama = Ramachandran(universe.select_atoms("protein"), - check_protein=True).run() + rama = Ramachandran( + universe.select_atoms("protein"), check_protein=True + ).run() assert len(record) == 1 def test_None_removal(self): @@ -141,9 +187,14 @@ def test_None_removal(self): rama = Ramachandran(u.select_atoms("protein").residues[1:-1]) def test_plot(self, universe): - ax = Ramachandran(universe.select_atoms("resid 5-10")).run().plot(ref=True) - assert isinstance(ax, matplotlib.axes.Axes), \ - "Ramachandran.plot() did not return and Axes instance" + ax = ( + Ramachandran(universe.select_atoms("resid 5-10")) + .run() + .plot(ref=True) + ) + assert isinstance( + ax, matplotlib.axes.Axes + ), "Ramachandran.plot() did not return and Axes instance" def test_ramachandran_attr_warning(self, universe): rama = Ramachandran(universe.select_atoms("protein")).run(stop=2) @@ -178,24 +229,36 @@ 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, - err_msg="error: dihedral angles should " - "match test values") + assert_allclose( + janin.results.angles, + ref_array, + rtol=0, + atol=1.5e-3, + err_msg="error: dihedral angles should " "match test values", + ) def test_janin_single_frame(self, universe, janin_ref_array): janin = Janin(universe.select_atoms("protein")).run(start=5, stop=6) - assert_allclose(janin.results.angles[0], janin_ref_array[5], rtol=0, atol=1.5e-3, - err_msg="error: dihedral angles should " - "match test values") + assert_allclose( + janin.results.angles[0], + janin_ref_array[5], + rtol=0, + atol=1.5e-3, + err_msg="error: dihedral angles should " "match test values", + ) 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, - err_msg="error: dihedral angles should " - "match test values") + assert_allclose( + janin.results.angles, + test_janin, + rtol=0, + atol=1.5e-3, + err_msg="error: dihedral angles should " "match test values", + ) def test_outside_protein_length(self, universe): with pytest.raises(ValueError): @@ -208,13 +271,17 @@ def test_remove_residues(self, universe): def test_atom_selection(self): with pytest.raises(ValueError): u = mda.Universe(PDB_janin) - janin = Janin(u.select_atoms("protein and not resname ALA CYS GLY " - "PRO SER THR VAL")) + janin = Janin( + u.select_atoms( + "protein and not resname ALA CYS GLY " "PRO SER THR VAL" + ) + ) def test_plot(self, universe): ax = Janin(universe.select_atoms("resid 5-10")).run().plot(ref=True) - assert isinstance(ax, matplotlib.axes.Axes), \ - "Ramachandran.plot() did not return and Axes instance" + assert isinstance( + ax, matplotlib.axes.Axes + ), "Ramachandran.plot() did not return and Axes instance" def test_janin_attr_warning(self, universe): janin = Janin(universe.select_atoms("protein")).run(stop=2) @@ -226,13 +293,14 @@ def test_janin_attr_warning(self, universe): # 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 @@ -241,13 +309,31 @@ def test_class_is_parallelizable(classname, 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',)), - ] + ( + 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_distances.py b/testsuite/MDAnalysisTests/analysis/test_distances.py index 8e3a14f8224..90fe8d75e8e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_distances.py +++ b/testsuite/MDAnalysisTests/analysis/test_distances.py @@ -29,8 +29,13 @@ import MDAnalysis.analysis.distances -from numpy.testing import (assert_equal, assert_array_equal, assert_almost_equal, - assert_array_almost_equal,assert_allclose) +from numpy.testing import ( + assert_equal, + assert_array_equal, + assert_almost_equal, + assert_array_almost_equal, + assert_allclose, +) import numpy as np @@ -38,75 +43,92 @@ class TestContactMatrix(object): @staticmethod @pytest.fixture() def coord(): - return np.array([[1, 1, 1], - [5, 5, 5], - [1.1, 1.1, 1.1], - [11, 11, 11], # neighboring image with pbc - [21, 21, 21]], # non neighboring image with pbc - dtype=np.float32) - + return np.array( + [ + [1, 1, 1], + [5, 5, 5], + [1.1, 1.1, 1.1], + [11, 11, 11], # neighboring image with pbc + [21, 21, 21], + ], # non neighboring image with pbc + dtype=np.float32, + ) + @staticmethod @pytest.fixture() def box(): return np.array([10, 10, 10, 90, 90, 90], dtype=np.float32) - + @staticmethod @pytest.fixture() def shape(): return 5, 5 - + @staticmethod @pytest.fixture() def res_no_pbc(): - return np.array([[1, 0, 1, 0, 0], - [0, 1, 0, 0, 0], - [1, 0, 1, 0, 0], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 1]], dtype=bool) - + return np.array( + [ + [1, 0, 1, 0, 0], + [0, 1, 0, 0, 0], + [1, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1], + ], + dtype=bool, + ) + @staticmethod @pytest.fixture() def res_pbc(): - return np.array([[1, 0, 1, 1, 1], - [0, 1, 0, 0, 0], - [1, 0, 1, 1, 1], - [1, 0, 1, 1, 1], - [1, 0, 1, 1, 1]], dtype=bool) + return np.array( + [ + [1, 0, 1, 1, 1], + [0, 1, 0, 0, 0], + [1, 0, 1, 1, 1], + [1, 0, 1, 1, 1], + [1, 0, 1, 1, 1], + ], + dtype=bool, + ) def test_np(self, coord, shape, res_no_pbc): contacts = MDAnalysis.analysis.distances.contact_matrix( coord, cutoff=1, returntype="numpy" ) - assert contacts.shape == shape, \ - "wrong shape (should be {0})".format(shape) + assert contacts.shape == shape, "wrong shape (should be {0})".format( + shape + ) assert_equal(contacts, res_no_pbc) def test_sparse(self, coord, shape, res_no_pbc): contacts = MDAnalysis.analysis.distances.contact_matrix( coord, cutoff=1.5, returntype="sparse" ) - assert contacts.shape == shape, \ - "wrong shape (should be {0})".format(shape) + assert contacts.shape == shape, "wrong shape (should be {0})".format( + shape + ) assert_equal(contacts.toarray(), res_no_pbc) def test_box_numpy(self, coord, box, shape, res_pbc): contacts = MDAnalysis.analysis.distances.contact_matrix( coord, box=box, cutoff=1 ) - assert contacts.shape == shape, \ - "wrong shape (should be {0})".format(shape) + assert contacts.shape == shape, "wrong shape (should be {0})".format( + shape + ) assert_equal(contacts, res_pbc) def test_box_sparse(self, coord, box, shape, res_pbc): contacts = MDAnalysis.analysis.distances.contact_matrix( - coord, box=box, cutoff=1, returntype='sparse' + coord, box=box, cutoff=1, returntype="sparse" + ) + assert contacts.shape == shape, "wrong shape (should be {0})".format( + shape ) - assert contacts.shape == shape, \ - "wrong shape (should be {0})".format(shape) assert_equal(contacts.toarray(), res_pbc) - class TestDist(object): @staticmethod @@ -126,13 +148,13 @@ def ag2(): @pytest.fixture() def box(): return np.array([8, 8, 8, 90, 90, 90], dtype=np.float32) - + @staticmethod @pytest.fixture() def expected(ag, ag2): - - return np.diag(scipy.spatial.distance.cdist( - ag.positions, ag2.positions) + + return np.diag( + scipy.spatial.distance.cdist(ag.positions, ag2.positions) ) @staticmethod @@ -141,38 +163,37 @@ def expected_box(ag, ag2, box): rp = np.abs(ag.positions - ag2.positions) box_2d = box[np.newaxis, 0:3] - rp = np.where(rp > box_2d / 2, box_2d - rp, rp) + rp = np.where(rp > box_2d / 2, box_2d - rp, rp) return np.sqrt(np.square(rp).sum(axis=1)) def test_pairwise_dist(self, ag, ag2, expected): - '''Ensure that pairwise distances between atoms are - correctly calculated.''' + """Ensure that pairwise distances between atoms are + correctly calculated.""" actual = MDAnalysis.analysis.distances.dist(ag, ag2)[2] assert_allclose(actual, expected) def test_pairwise_dist_box(self, ag, ag2, expected_box, box): - '''Ensure that pairwise distances between atoms are - correctly calculated.''' + """Ensure that pairwise distances between atoms are + correctly calculated.""" actual = MDAnalysis.analysis.distances.dist(ag, ag2, 0, box)[2] assert_allclose(actual, expected_box, rtol=1e-05, atol=10) def test_pairwise_dist_offset_effect(self, ag, ag2, expected): - '''Test that feeding in offsets to dist() doesn't alter - pairwise distance matrix.''' - actual = MDAnalysis.analysis.distances.dist( - ag, ag2, offset=229)[2] + """Test that feeding in offsets to dist() doesn't alter + pairwise distance matrix.""" + actual = MDAnalysis.analysis.distances.dist(ag, ag2, offset=229)[2] assert_allclose(actual, expected) def test_offset_calculation(self, ag, ag2): - '''Test that offsets fed to dist() are correctly calculated.''' - actual = MDAnalysis.analysis.distances.dist(ag, ag2, - offset=33)[:2] - assert_equal(actual, np.array([ag.atoms.resids + 33, - ag2.atoms.resids + 33])) + """Test that offsets fed to dist() are correctly calculated.""" + actual = MDAnalysis.analysis.distances.dist(ag, ag2, offset=33)[:2] + assert_equal( + actual, np.array([ag.atoms.resids + 33, ag2.atoms.resids + 33]) + ) def test_mismatch_exception(self, ag, ag2, expected): - '''A ValueError should be raised if the two atomgroups - don't have the same number of atoms.''' + """A ValueError should be raised if the two atomgroups + don't have the same number of atoms.""" with pytest.raises(ValueError): MDAnalysis.analysis.distances.dist(ag[:8], ag2) @@ -202,36 +223,30 @@ def group(u): @pytest.fixture() def expected(self, group, ag, ag2): - distance_matrix_1 = scipy.spatial.distance.cdist(group.positions, - ag.positions) + distance_matrix_1 = scipy.spatial.distance.cdist( + group.positions, ag.positions + ) mask_1 = np.unique(np.where(distance_matrix_1 <= self.distance)[0]) group_filtered = group[mask_1] - distance_matrix_2 = scipy.spatial.distance.cdist(group_filtered.positions, - ag2.positions) + distance_matrix_2 = scipy.spatial.distance.cdist( + group_filtered.positions, ag2.positions + ) mask_2 = np.unique(np.where(distance_matrix_2 <= self.distance)[0]) return group_filtered[mask_2].indices def test_between_simple_case_indices_only(self, group, ag, ag2, expected): - '''Test MDAnalysis.analysis.distances.between() for + """Test MDAnalysis.analysis.distances.between() for a simple input case. Checks atom indices of returned AtomGroup against sorted expected index - values.''' + values.""" actual = MDAnalysis.analysis.distances.between( - group, - ag, - ag2, - self.distance + group, ag, ag2, self.distance ).indices assert_equal(actual, expected) - @pytest.mark.parametrize('dists', [5.9, 0.0]) + @pytest.mark.parametrize("dists", [5.9, 0.0]) def test_between_return_type(self, dists, group, ag, ag2): - '''Test that MDAnalysis.analysis.distances.between() - returns an AtomGroup even when the returned group is empty.''' - actual = MDAnalysis.analysis.distances.between( - group, - ag, - ag2, - dists - ) + """Test that MDAnalysis.analysis.distances.between() + returns an AtomGroup even when the returned group is empty.""" + actual = MDAnalysis.analysis.distances.between(group, ag, ag2, dists) assert isinstance(actual, MDAnalysis.core.groups.AtomGroup) diff --git a/testsuite/MDAnalysisTests/analysis/test_dssp.py b/testsuite/MDAnalysisTests/analysis/test_dssp.py index f7a1e118931..ee43a400dff 100644 --- a/testsuite/MDAnalysisTests/analysis/test_dssp.py +++ b/testsuite/MDAnalysisTests/analysis/test_dssp.py @@ -9,7 +9,9 @@ # 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")) +@pytest.mark.parametrize( + "pdb_filename", glob.glob(f"{DSSP_FOLDER}/?????.pdb.gz") +) 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: @@ -27,7 +29,9 @@ def test_trajectory(client_DSSP): 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---" + assert ( + first_frame[:10] != last_frame[:10] == avg_frame[:10] == "-EEEEEE---" + ) protein = mda.Universe(TPR, XTC).select_atoms("protein") run = DSSP(protein).run(**client_DSSP, stop=10) @@ -39,7 +43,9 @@ def test_atomgroup(client_DSSP): 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---" + assert ( + first_frame[:10] != last_frame[:10] == avg_frame[:10] == "-EEEEEE---" + ) def test_trajectory_with_hydrogens(client_DSSP): @@ -49,10 +55,14 @@ def test_trajectory_with_hydrogens(client_DSSP): 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---" + assert ( + first_frame[:10] == last_frame[:10] == avg_frame[:10] == "-EEEEEE---" + ) -@pytest.mark.parametrize("pdb_filename", glob.glob(f"{DSSP_FOLDER}/2xdgA.pdb.gz")) +@pytest.mark.parametrize( + "pdb_filename", glob.glob(f"{DSSP_FOLDER}/2xdgA.pdb.gz") +) def test_trajectory_without_hydrogen_fails(pdb_filename, client_DSSP): u = mda.Universe(pdb_filename) with pytest.raises(ValueError): @@ -62,8 +72,9 @@ def test_trajectory_without_hydrogen_fails(pdb_filename, 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, - client_DSSP): +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(**client_DSSP) diff --git a/testsuite/MDAnalysisTests/analysis/test_encore.py b/testsuite/MDAnalysisTests/analysis/test_encore.py index 948575adfff..9204352f480 100644 --- a/testsuite/MDAnalysisTests/analysis/test_encore.py +++ b/testsuite/MDAnalysisTests/analysis/test_encore.py @@ -20,27 +20,25 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -import MDAnalysis as mda -import MDAnalysis.analysis.encore as encore - import importlib -import tempfile -import numpy as np -import sys import os -import warnings import platform +import sys +import tempfile +import warnings from importlib import reload +import MDAnalysis as mda +import MDAnalysis.analysis.align as align +import MDAnalysis.analysis.encore as encore +import MDAnalysis.analysis.encore.confdistmatrix as confdistmatrix +import MDAnalysis.analysis.rms as rms +import numpy as np import pytest -from numpy.testing import assert_equal, assert_allclose +from numpy.testing import assert_allclose, assert_equal -from MDAnalysisTests.datafiles import DCD, DCD2, PSF, TPR, XTC from MDAnalysisTests import block_import - -import MDAnalysis.analysis.rms as rms -import MDAnalysis.analysis.align as align -import MDAnalysis.analysis.encore.confdistmatrix as confdistmatrix +from MDAnalysisTests.datafiles import DCD, DCD2, PSF, TPR, XTC def function(x): @@ -52,14 +50,15 @@ def test_moved_to_mdakit_warning(): with pytest.warns(DeprecationWarning, match=wmsg): reload(encore) + class TestEncore(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ens1_template(self): template = mda.Universe(PSF, DCD) template.transfer_to_memory(step=5) return template - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ens2_template(self): template = mda.Universe(PSF, DCD2) template.transfer_to_memory(step=5) @@ -69,21 +68,23 @@ def ens2_template(self): def ens1(self, ens1_template): return mda.Universe( ens1_template.filename, - ens1_template.trajectory.timeseries(order='fac'), - format=mda.coordinates.memory.MemoryReader) + ens1_template.trajectory.timeseries(order="fac"), + format=mda.coordinates.memory.MemoryReader, + ) @pytest.fixture() def ens2(self, ens2_template): return mda.Universe( ens2_template.filename, - ens2_template.trajectory.timeseries(order='fac'), - format=mda.coordinates.memory.MemoryReader) + ens2_template.trajectory.timeseries(order="fac"), + format=mda.coordinates.memory.MemoryReader, + ) def test_triangular_matrix(self): scalar = 2 size = 3 expected_value = 1.984 - filename = tempfile.mktemp()+".npz" + filename = tempfile.mktemp() + ".npz" triangular_matrix = encore.utils.TriangularMatrix(size=size) @@ -91,130 +92,191 @@ def test_triangular_matrix(self): err_msg = ( "Data error in TriangularMatrix: read/write are not consistent" - ) + ) assert_equal(triangular_matrix[0, 1], expected_value, err_msg) - assert_equal(triangular_matrix[0,1], triangular_matrix[1,0], - err_msg="Data error in TriangularMatrix: matrix non symmetrical") + assert_equal( + triangular_matrix[0, 1], + triangular_matrix[1, 0], + err_msg="Data error in TriangularMatrix: matrix non symmetrical", + ) triangular_matrix.savez(filename) - triangular_matrix_2 = encore.utils.TriangularMatrix(size = size, loadfile = filename) - assert_equal(triangular_matrix_2[0,1], expected_value, - err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical") + triangular_matrix_2 = encore.utils.TriangularMatrix( + size=size, loadfile=filename + ) + assert_equal( + triangular_matrix_2[0, 1], + expected_value, + err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical", + ) - triangular_matrix_3 = encore.utils.TriangularMatrix(size = size) + triangular_matrix_3 = encore.utils.TriangularMatrix(size=size) triangular_matrix_3.loadz(filename) - assert_equal(triangular_matrix_3[0,1], expected_value, - err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical") + assert_equal( + triangular_matrix_3[0, 1], + expected_value, + err_msg="Data error in TriangularMatrix: loaded matrix non symmetrical", + ) incremented_triangular_matrix = triangular_matrix + scalar - assert_equal(incremented_triangular_matrix[0,1], expected_value + scalar, - err_msg="Error in TriangularMatrix: addition of scalar gave" - "inconsistent results") + assert_equal( + incremented_triangular_matrix[0, 1], + expected_value + scalar, + err_msg="Error in TriangularMatrix: addition of scalar gave" + "inconsistent results", + ) triangular_matrix += scalar - assert_equal(triangular_matrix[0,1], expected_value + scalar, - err_msg="Error in TriangularMatrix: addition of scalar gave" - "inconsistent results") + assert_equal( + triangular_matrix[0, 1], + expected_value + scalar, + err_msg="Error in TriangularMatrix: addition of scalar gave" + "inconsistent results", + ) multiplied_triangular_matrix_2 = triangular_matrix_2 * scalar - assert_equal(multiplied_triangular_matrix_2[0,1], expected_value * scalar, - err_msg="Error in TriangularMatrix: multiplication by scalar gave" - "inconsistent results") + assert_equal( + multiplied_triangular_matrix_2[0, 1], + expected_value * scalar, + err_msg="Error in TriangularMatrix: multiplication by scalar gave" + "inconsistent results", + ) triangular_matrix_2 *= scalar - assert_equal(triangular_matrix_2[0,1], expected_value * scalar, - err_msg="Error in TriangularMatrix: multiplication by scalar gave\ -inconsistent results") + assert_equal( + triangular_matrix_2[0, 1], + expected_value * scalar, + err_msg="Error in TriangularMatrix: multiplication by scalar gave\ +inconsistent results", + ) - @pytest.mark.xfail(os.name == 'nt', - reason="Not yet supported on Windows.") + @pytest.mark.xfail(os.name == "nt", reason="Not yet supported on Windows.") def test_parallel_calculation(self): - arguments = [tuple([i]) for i in np.arange(0,100)] + arguments = [tuple([i]) for i in np.arange(0, 100)] - parallel_calculation = encore.utils.ParallelCalculation(function=function, - n_jobs=4, - args=arguments) + parallel_calculation = encore.utils.ParallelCalculation( + function=function, n_jobs=4, args=arguments + ) results = parallel_calculation.run() for i, r in enumerate(results): assert_equal( r[1], - arguments[i][0]**2, - err_msg="Unexpected results from ParallelCalculation") + arguments[i][0] ** 2, + err_msg="Unexpected results from ParallelCalculation", + ) def test_rmsd_matrix_with_superimposition(self, ens1): - conf_dist_matrix = encore.confdistmatrix.conformational_distance_matrix( - ens1, - encore.confdistmatrix.set_rmsd_matrix_elements, - select="name CA", - pairwise_align=True, - weights='mass', - n_jobs=1) + conf_dist_matrix = ( + encore.confdistmatrix.conformational_distance_matrix( + ens1, + encore.confdistmatrix.set_rmsd_matrix_elements, + select="name CA", + pairwise_align=True, + weights="mass", + n_jobs=1, + ) + ) reference = rms.RMSD(ens1, select="name CA") reference.run() err_msg = ( "Calculated RMSD values differ from " - "the reference implementation") + "the reference implementation" + ) for i, rmsd in enumerate(reference.results.rmsd): - assert_allclose(conf_dist_matrix[0, i], rmsd[2], rtol=0, atol=1.5e-3, err_msg=err_msg) + assert_allclose( + conf_dist_matrix[0, i], + rmsd[2], + rtol=0, + atol=1.5e-3, + err_msg=err_msg, + ) def test_rmsd_matrix_with_superimposition_custom_weights(self, ens1): - conf_dist_matrix = encore.confdistmatrix.conformational_distance_matrix( - ens1, - encore.confdistmatrix.set_rmsd_matrix_elements, - select="name CA", - pairwise_align=True, - weights='mass', - n_jobs=1) + conf_dist_matrix = ( + encore.confdistmatrix.conformational_distance_matrix( + ens1, + encore.confdistmatrix.set_rmsd_matrix_elements, + select="name CA", + pairwise_align=True, + weights="mass", + n_jobs=1, + ) + ) - conf_dist_matrix_custom = encore.confdistmatrix.conformational_distance_matrix( - ens1, - encore.confdistmatrix.set_rmsd_matrix_elements, - select="name CA", - pairwise_align=True, - weights=(ens1.select_atoms('name CA').masses, ens1.select_atoms('name CA').masses), - n_jobs=1) + conf_dist_matrix_custom = ( + encore.confdistmatrix.conformational_distance_matrix( + ens1, + encore.confdistmatrix.set_rmsd_matrix_elements, + select="name CA", + pairwise_align=True, + weights=( + ens1.select_atoms("name CA").masses, + ens1.select_atoms("name CA").masses, + ), + n_jobs=1, + ) + ) for i in range(conf_dist_matrix_custom.size): - assert_allclose(conf_dist_matrix_custom[0, i], conf_dist_matrix[0, i], rtol=0, atol=1.5e-7) + assert_allclose( + conf_dist_matrix_custom[0, i], + conf_dist_matrix[0, i], + rtol=0, + atol=1.5e-7, + ) def test_rmsd_matrix_without_superimposition(self, ens1): selection_string = "name CA" selection = ens1.select_atoms(selection_string) reference_rmsd = [] - coordinates = ens1.trajectory.timeseries(selection, order='fac') + coordinates = ens1.trajectory.timeseries(selection, order="fac") for coord in coordinates: - reference_rmsd.append(rms.rmsd(coordinates[0], coord, superposition=False)) + reference_rmsd.append( + rms.rmsd(coordinates[0], coord, superposition=False) + ) confdist_matrix = encore.confdistmatrix.conformational_distance_matrix( ens1, encore.confdistmatrix.set_rmsd_matrix_elements, select=selection_string, pairwise_align=False, - weights='mass', - n_jobs=1) + weights="mass", + n_jobs=1, + ) print(repr(confdist_matrix.as_array()[0, :])) - assert_allclose(confdist_matrix.as_array()[0,:], reference_rmsd, rtol=0, atol=1.5e-3, - err_msg="calculated RMSD values differ from reference") + assert_allclose( + confdist_matrix.as_array()[0, :], + reference_rmsd, + rtol=0, + atol=1.5e-3, + err_msg="calculated RMSD values differ from reference", + ) def test_ensemble_superimposition(self): aligned_ensemble1 = mda.Universe(PSF, DCD) - align.AlignTraj(aligned_ensemble1, aligned_ensemble1, - select="name CA", - in_memory=True).run() + align.AlignTraj( + aligned_ensemble1, + aligned_ensemble1, + select="name CA", + in_memory=True, + ).run() aligned_ensemble2 = mda.Universe(PSF, DCD) - align.AlignTraj(aligned_ensemble2, aligned_ensemble2, - select="name *", - in_memory=True).run() - - rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms('name *')) + align.AlignTraj( + aligned_ensemble2, + aligned_ensemble2, + select="name *", + in_memory=True, + ).run() + + rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms("name *")) rmsfs1.run() - rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms('name *')) + rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms("name *")) rmsfs2.run() assert sum(rmsfs1.results.rmsf) > sum(rmsfs2.results.rmsf), ( @@ -224,18 +286,24 @@ def test_ensemble_superimposition(self): def test_ensemble_superimposition_to_reference_non_weighted(self): aligned_ensemble1 = mda.Universe(PSF, DCD) - align.AlignTraj(aligned_ensemble1, aligned_ensemble1, - select="name CA", - in_memory=True).run() + align.AlignTraj( + aligned_ensemble1, + aligned_ensemble1, + select="name CA", + in_memory=True, + ).run() aligned_ensemble2 = mda.Universe(PSF, DCD) - align.AlignTraj(aligned_ensemble2, aligned_ensemble2, - select="name *", - in_memory=True).run() - - rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms('name *')) + align.AlignTraj( + aligned_ensemble2, + aligned_ensemble2, + select="name *", + in_memory=True, + ).run() + + rmsfs1 = rms.RMSF(aligned_ensemble1.select_atoms("name *")) rmsfs1.run() - rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms('name *')) + rmsfs2 = rms.RMSF(aligned_ensemble2.select_atoms("name *")) rmsfs2.run() assert sum(rmsfs1.results.rmsf) > sum(rmsfs2.results.rmsf), ( @@ -244,64 +312,96 @@ def test_ensemble_superimposition_to_reference_non_weighted(self): ) def test_covariance_matrix(self, ens1): + # fmt: off reference_cov = np.array([ - [12.9122,-5.2692,3.9016,10.0663,-5.3309,3.8923,8.5037,-5.2017,2.6941], - [-5.2692,4.1087,-2.4101,-4.5485,3.3954,-2.3245,-3.7343,2.8415,-1.6223], - [3.9016,-2.4101,3.1800,3.4453,-2.6860,2.2438,2.7751,-2.2523,1.6084], - [10.0663,-4.5485,3.4453,8.8608,-4.6727,3.3641,7.0106,-4.4986,2.2604], - [-5.3309,3.3954,-2.6860,-4.6727,4.4627,-2.4233,-3.8304,3.0367,-1.6942], - [3.8923,-2.3245,2.2438,3.3641,-2.4233,2.6193,2.6908,-2.0252,1.5775], - [8.5037,-3.7343,2.7751,7.0106,-3.8304,2.6908,6.2861,-3.7138,1.8701], - [-5.2017,2.8415,-2.2523,-4.4986,3.0367,-2.0252,-3.7138,3.3999,-1.4166], - [2.6941,-1.6223,1.6084,2.2604,-1.6942,1.5775,1.8701,-1.4166,1.4664]]) - - covariance = encore.covariance.covariance_matrix(ens1, - select="name CA and resnum 1:3", - estimator=encore.covariance.shrinkage_covariance_estimator) - assert_allclose(covariance, reference_cov, rtol=0, atol=1.5e-4, - err_msg="Covariance matrix from covariance estimation not as expected") + [12.9122,-5.2692,3.9016,10.0663,-5.3309,3.8923,8.5037,-5.2017,2.6941], + [-5.2692,4.1087,-2.4101,-4.5485,3.3954,-2.3245,-3.7343,2.8415,-1.6223], + [3.9016,-2.4101,3.1800,3.4453,-2.6860,2.2438,2.7751,-2.2523,1.6084], + [10.0663,-4.5485,3.4453,8.8608,-4.6727,3.3641,7.0106,-4.4986,2.2604], + [-5.3309,3.3954,-2.6860,-4.6727,4.4627,-2.4233,-3.8304,3.0367,-1.6942], + [3.8923,-2.3245,2.2438,3.3641,-2.4233,2.6193,2.6908,-2.0252,1.5775], + [8.5037,-3.7343,2.7751,7.0106,-3.8304,2.6908,6.2861,-3.7138,1.8701], + [-5.2017,2.8415,-2.2523,-4.4986,3.0367,-2.0252,-3.7138,3.3999,-1.4166], + [2.6941,-1.6223,1.6084,2.2604,-1.6942,1.5775,1.8701,-1.4166,1.4664] + ]) + # fmt: on + + covariance = encore.covariance.covariance_matrix( + ens1, + select="name CA and resnum 1:3", + estimator=encore.covariance.shrinkage_covariance_estimator, + ) + assert_allclose( + covariance, + reference_cov, + rtol=0, + atol=1.5e-4, + err_msg="Covariance matrix from covariance estimation not as expected", + ) def test_covariance_matrix_with_reference(self, ens1): + # fmt: off reference_cov = np.array([ - [39.0760,-28.5383,29.7761,37.9330,-35.5251,18.9421,30.4334,-31.4829,12.8712], - [-28.5383,24.1827,-25.5676,-29.0183,30.3511,-15.9598,-22.9298,26.1086,-10.8693], - [29.7761,-25.5676,28.9796,30.7607,-32.8739,17.7072,24.1689,-28.3557,12.1190], - [37.9330,-29.0183,30.7607,37.6532,-36.4537,19.2865,29.9841,-32.1404,12.9998], - [-35.5251,30.3511,-32.8739,-36.4537,38.5711,-20.1190,-28.7652,33.2857,-13.6963], - [18.9421,-15.9598,17.7072,19.2865,-20.1190,11.4059,15.1244,-17.2695,7.8205], - [30.4334,-22.9298,24.1689,29.9841,-28.7652,15.1244,24.0514,-25.4106,10.2863], - [-31.4829,26.1086,-28.3557,-32.1404,33.2857,-17.2695,-25.4106,29.1773,-11.7530], - [12.8712,-10.8693,12.1190,12.9998,-13.6963,7.8205,10.2863,-11.7530,5.5058]]) - - covariance = encore.covariance.covariance_matrix(ens1, - select="name CA and resnum 1:3", - estimator=encore.covariance.shrinkage_covariance_estimator, - reference=ens1) + [39.0760,-28.5383,29.7761,37.9330,-35.5251,18.9421,30.4334,-31.4829,12.8712], + [-28.5383,24.1827,-25.5676,-29.0183,30.3511,-15.9598,-22.9298,26.1086,-10.8693], + [29.7761,-25.5676,28.9796,30.7607,-32.8739,17.7072,24.1689,-28.3557,12.1190], + [37.9330,-29.0183,30.7607,37.6532,-36.4537,19.2865,29.9841,-32.1404,12.9998], + [-35.5251,30.3511,-32.8739,-36.4537,38.5711,-20.1190,-28.7652,33.2857,-13.6963], + [18.9421,-15.9598,17.7072,19.2865,-20.1190,11.4059,15.1244,-17.2695,7.8205], + [30.4334,-22.9298,24.1689,29.9841,-28.7652,15.1244,24.0514,-25.4106,10.2863], + [-31.4829,26.1086,-28.3557,-32.1404,33.2857,-17.2695,-25.4106,29.1773,-11.7530], + [12.8712,-10.8693,12.1190,12.9998,-13.6963,7.8205,10.2863,-11.7530,5.5058] + ]) + # fmt: on + + covariance = encore.covariance.covariance_matrix( + ens1, + select="name CA and resnum 1:3", + estimator=encore.covariance.shrinkage_covariance_estimator, + reference=ens1, + ) err_msg = ( - "Covariance matrix from covariance estimation not as expected" - ) - assert_allclose(covariance, reference_cov, rtol=0, atol=1.5e-4, err_msg=err_msg) + "Covariance matrix from covariance estimation not as expected" + ) + assert_allclose( + covariance, reference_cov, rtol=0, atol=1.5e-4, err_msg=err_msg + ) def test_hes_to_self(self, ens1): results, details = encore.hes([ens1, ens1]) result_value = results[0, 1] - expected_value = 0. - assert_allclose(result_value, expected_value, rtol=0, atol=1.5e-7, - err_msg="Harmonic Ensemble Similarity to itself\ - not zero:{0:f}".format(result_value)) + expected_value = 0.0 + assert_allclose( + result_value, + expected_value, + rtol=0, + atol=1.5e-7, + err_msg="Harmonic Ensemble Similarity to itself\ + not zero:{0:f}".format( + result_value + ), + ) def test_hes(self, ens1, ens2): - results, details = encore.hes([ens1, ens2], weights='mass') + results, details = encore.hes([ens1, ens2], weights="mass") result_value = results[0, 1] - min_bound = 1E5 - assert result_value > min_bound, "Unexpected value for Harmonic " \ - "Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, min_bound) + min_bound = 1e5 + assert result_value > min_bound, ( + "Unexpected value for Harmonic " + "Ensemble Similarity: {0:f}. Expected {1:f}.".format( + result_value, min_bound + ) + ) def test_hes_custom_weights(self, ens1, ens2): - results, details = encore.hes([ens1, ens2], weights='mass') - results_custom, details_custom = encore.hes([ens1, ens2], - weights=(ens1.select_atoms('name CA').masses, - ens2.select_atoms('name CA').masses)) + results, details = encore.hes([ens1, ens2], weights="mass") + results_custom, details_custom = encore.hes( + [ens1, ens2], + weights=( + ens1.select_atoms("name CA").masses, + ens2.select_atoms("name CA").masses, + ), + ) result_value = results[0, 1] result_value_custom = results_custom[0, 1] assert_allclose(result_value, result_value_custom, rtol=0, atol=1.5e-7) @@ -310,90 +410,154 @@ def test_hes_align(self, ens1, ens2): # This test is massively sensitive! # Get 5260 when masses were float32? results, details = encore.hes([ens1, ens2], align=True) - result_value = results[0,1] + result_value = results[0, 1] expected_value = 2047.05 - assert_allclose(result_value, expected_value, rtol=0, atol=1.5e3, - err_msg="Unexpected value for Harmonic Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) + assert_allclose( + result_value, + expected_value, + rtol=0, + atol=1.5e3, + err_msg="Unexpected value for Harmonic Ensemble Similarity: {0:f}. Expected {1:f}.".format( + result_value, expected_value + ), + ) def test_ces_to_self(self, ens1): - results, details = \ - encore.ces([ens1, ens1], - clustering_method=encore.AffinityPropagationNative(preference = -3.0)) - result_value = results[0,1] - expected_value = 0. - assert_allclose(result_value, expected_value, rtol=0, atol=1.5e-7, - err_msg="ClusteringEnsemble Similarity to itself not zero: {0:f}".format(result_value)) + results, details = encore.ces( + [ens1, ens1], + clustering_method=encore.AffinityPropagationNative( + preference=-3.0 + ), + ) + result_value = results[0, 1] + expected_value = 0.0 + assert_allclose( + result_value, + expected_value, + rtol=0, + atol=1.5e-7, + err_msg="ClusteringEnsemble Similarity to itself not zero: {0:f}".format( + result_value + ), + ) def test_ces(self, ens1, ens2): results, details = encore.ces([ens1, ens2]) - result_value = results[0,1] + result_value = results[0, 1] expected_value = 0.51 - assert_allclose(result_value, expected_value, rtol=0, atol=1.5e-2, - err_msg="Unexpected value for Cluster Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) + assert_allclose( + result_value, + expected_value, + rtol=0, + atol=1.5e-2, + err_msg="Unexpected value for Cluster Ensemble Similarity: {0:f}. Expected {1:f}.".format( + result_value, expected_value + ), + ) def test_dres_to_self(self, ens1): results, details = encore.dres([ens1, ens1]) - result_value = results[0,1] - expected_value = 0. - assert_allclose(result_value, expected_value, rtol=0, atol=1.5e-2, - err_msg="Dim. Reduction Ensemble Similarity to itself not zero: {0:f}".format(result_value)) + result_value = results[0, 1] + expected_value = 0.0 + assert_allclose( + result_value, + expected_value, + rtol=0, + atol=1.5e-2, + err_msg="Dim. Reduction Ensemble Similarity to itself not zero: {0:f}".format( + result_value + ), + ) def test_dres(self, ens1, ens2): - results, details = encore.dres([ens1, ens2], select="name CA and resnum 1-10") - result_value = results[0,1] + results, details = encore.dres( + [ens1, ens2], select="name CA and resnum 1-10" + ) + result_value = results[0, 1] upper_bound = 0.6 - assert result_value < upper_bound, "Unexpected value for Dim. " \ - "reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, upper_bound) + assert result_value < upper_bound, ( + "Unexpected value for Dim. " + "reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format( + result_value, upper_bound + ) + ) @pytest.mark.xfail # sporadically fails, see Issue #2158 def test_dres_without_superimposition(self, ens1, ens2): distance_matrix = encore.get_distance_matrix( - encore.merge_universes([ens1, ens2]), - superimpose=False) - results, details = encore.dres([ens1, ens2], - distance_matrix = distance_matrix) - result_value = results[0,1] + encore.merge_universes([ens1, ens2]), superimpose=False + ) + results, details = encore.dres( + [ens1, ens2], distance_matrix=distance_matrix + ) + result_value = results[0, 1] expected_value = 0.68 - assert_allclose(result_value, expected_value, rtol=0, atol=1.5e-1, - err_msg="Unexpected value for Dim. reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) + assert_allclose( + result_value, + expected_value, + rtol=0, + atol=1.5e-1, + err_msg="Unexpected value for Dim. reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format( + result_value, expected_value + ), + ) def test_ces_convergence(self, ens1): - expected_values = [0.3443593, 0.1941854, 0.06857104, 0.] + expected_values = [0.3443593, 0.1941854, 0.06857104, 0.0] results = encore.ces_convergence(ens1, 5) - for i,ev in enumerate(expected_values): - assert_allclose(ev, results[i], rtol=0, atol=1.5e-2, - err_msg="Unexpected value for Clustering Ensemble similarity in convergence estimation") + for i, ev in enumerate(expected_values): + assert_allclose( + ev, + results[i], + rtol=0, + atol=1.5e-2, + err_msg="Unexpected value for Clustering Ensemble similarity in convergence estimation", + ) def test_dres_convergence(self, ens1): # Due to encore.dres_convergence() involving random numbers, the # following assertion is allowed to fail once. This significantly # reduces the probability of a random test failure. - expected_values = [0.3, 0.] + expected_values = [0.3, 0.0] results = encore.dres_convergence(ens1, 10) try: - assert_allclose(results[:,0], expected_values, rtol=0, atol=1.5e-1) + assert_allclose( + results[:, 0], expected_values, rtol=0, atol=1.5e-1 + ) except AssertionError: # Random test failure is very rare, but repeating the failed test # just once would only assert that the test passes with 50% # probability. To be a little safer, we raise a warning and repeat # the test 10 times: - warnings.warn(message="Test 'test_dres_convergence' failed, " - "repeating test 10 times.", - category=RuntimeWarning) + warnings.warn( + message="Test 'test_dres_convergence' failed, " + "repeating test 10 times.", + category=RuntimeWarning, + ) for i in range(10): results = encore.dres_convergence(ens1, 10) - assert_allclose(results[:,0], expected_values, rtol=0, atol=1.5e-1, - err_msg="Unexpected value for Dim. " - "reduction Ensemble similarity in " - "convergence estimation") + assert_allclose( + results[:, 0], + expected_values, + rtol=0, + atol=1.5e-1, + err_msg="Unexpected value for Dim. " + "reduction Ensemble similarity in " + "convergence estimation", + ) @pytest.mark.xfail # sporadically fails, see Issue #2158 def test_hes_error_estimation(self, ens1): expected_average = 10 expected_stdev = 12 - averages, stdevs = encore.hes([ens1, ens1], estimate_error = True, bootstrapping_samples=10, select="name CA and resnum 1-10") - average = averages[0,1] - stdev = stdevs[0,1] + averages, stdevs = encore.hes( + [ens1, ens1], + estimate_error=True, + bootstrapping_samples=10, + select="name CA and resnum 1-10", + ) + average = averages[0, 1] + stdev = stdevs[0, 1] err_msg = ( "Unexpected average value for bootstrapped samples in Harmonic" " Ensemble similarity" @@ -402,69 +566,87 @@ def test_hes_error_estimation(self, ens1): "Unexpected standard deviation for bootstrapped samples in" " Harmonic Ensemble similarity" ) - assert_allclose(average, expected_average, rtol=0, atol=1.5e2, err_msg=err_msg) - assert_allclose(stdev, expected_stdev, rtol=0, atol=1.5e2, err_msg=error_msg) + assert_allclose( + average, expected_average, rtol=0, atol=1.5e2, err_msg=err_msg + ) + assert_allclose( + stdev, expected_stdev, rtol=0, atol=1.5e2, err_msg=error_msg + ) def test_ces_error_estimation(self, ens1): expected_average = 0.03 expected_stdev = 0.31 - averages, stdevs = encore.ces([ens1, ens1], - estimate_error = True, - bootstrapping_samples=10, - clustering_method=encore.AffinityPropagationNative(preference=-2.0), - select="name CA and resnum 1-10") - average = averages[0,1] - stdev = stdevs[0,1] - - assert_allclose(average, expected_average, rtol=0, atol=1.5e-1, - err_msg="Unexpected average value for bootstrapped samples in Clustering Ensemble similarity") - assert_allclose(stdev, expected_stdev, rtol=0, atol=1.5, - err_msg="Unexpected standard deviation for bootstrapped samples in Clustering Ensemble similarity") + averages, stdevs = encore.ces( + [ens1, ens1], + estimate_error=True, + bootstrapping_samples=10, + clustering_method=encore.AffinityPropagationNative( + preference=-2.0 + ), + select="name CA and resnum 1-10", + ) + average = averages[0, 1] + stdev = stdevs[0, 1] + + assert_allclose( + average, + expected_average, + rtol=0, + atol=1.5e-1, + err_msg="Unexpected average value for bootstrapped samples in Clustering Ensemble similarity", + ) + assert_allclose( + stdev, + expected_stdev, + rtol=0, + atol=1.5, + err_msg="Unexpected standard deviation for bootstrapped samples in Clustering Ensemble similarity", + ) def test_ces_error_estimation_ensemble_bootstrap(self, ens1): # Error estimation using a method that does not take a distance # matrix as input, and therefore relies on bootstrapping the ensembles # instead - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") expected_average = 0.03 expected_stdev = 0.02 - averages, stdevs = encore.ces([ens1, ens1], - estimate_error = True, - bootstrapping_samples=10, - clustering_method=encore.KMeans(n_clusters=2), - select="name CA and resnum 1-10") + averages, stdevs = encore.ces( + [ens1, ens1], + estimate_error=True, + bootstrapping_samples=10, + clustering_method=encore.KMeans(n_clusters=2), + select="name CA and resnum 1-10", + ) average = averages[0, 1] stdev = stdevs[0, 1] err_msg = ( "Unexpected average value for bootstrapped samples in" - " Clustering Ensemble similarity") + " Clustering Ensemble similarity" + ) assert_allclose( - average, - expected_average, - rtol = 0, - atol = 1.5e-1, - err_msg=err_msg) + average, expected_average, rtol=0, atol=1.5e-1, err_msg=err_msg + ) error_msg = ( "Unexpected standard deviation for bootstrapped samples in" " Clustering Ensemble similarity" - ) + ) assert_allclose( - stdev, - expected_stdev, - rtol=0, - atol=1.5e-1, - err_msg=error_msg) + stdev, expected_stdev, rtol=0, atol=1.5e-1, err_msg=error_msg + ) def test_dres_error_estimation(self, ens1): average_upper_bound = 0.3 stdev_upper_bound = 0.2 - averages, stdevs = encore.dres([ens1, ens1], estimate_error = True, - bootstrapping_samples=10, - select="name CA and resnum 1-10") - average = averages[0,1] - stdev = stdevs[0,1] + averages, stdevs = encore.dres( + [ens1, ens1], + estimate_error=True, + bootstrapping_samples=10, + select="name CA and resnum 1-10", + ) + average = averages[0, 1] + stdev = stdevs[0, 1] err_msg = ( "Unexpected average value for bootstrapped samples in Dim. " "reduction Ensemble similarity" @@ -478,141 +660,160 @@ def test_dres_error_estimation(self, ens1): class TestEncoreClustering(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ens1_template(self): template = mda.Universe(PSF, DCD) template.transfer_to_memory(step=5) return template - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ens2_template(self): template = mda.Universe(PSF, DCD2) template.transfer_to_memory(step=5) return template - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def cc(self): return encore.ClusterCollection([1, 1, 1, 3, 3, 5, 5, 5]) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def cluster(self): return encore.Cluster(elem_list=np.array([0, 1, 2]), centroid=1) @pytest.fixture() def ens1(self, ens1_template): return mda.Universe( - ens1_template.filename, - ens1_template.trajectory.timeseries(order='fac'), - format=mda.coordinates.memory.MemoryReader) + ens1_template.filename, + ens1_template.trajectory.timeseries(order="fac"), + format=mda.coordinates.memory.MemoryReader, + ) @pytest.fixture() def ens2(self, ens2_template): return mda.Universe( ens2_template.filename, - ens2_template.trajectory.timeseries(order='fac'), - format=mda.coordinates.memory.MemoryReader) + ens2_template.trajectory.timeseries(order="fac"), + format=mda.coordinates.memory.MemoryReader, + ) def test_clustering_one_ensemble(self, ens1): cluster_collection = encore.cluster(ens1) expected_value = 7 - assert len(cluster_collection) == expected_value, "Unexpected " \ - "results: {0}".format(cluster_collection) + assert ( + len(cluster_collection) == expected_value + ), "Unexpected " "results: {0}".format(cluster_collection) def test_clustering_two_ensembles(self, ens1, ens2): cluster_collection = encore.cluster([ens1, ens2]) expected_value = 14 - assert len(cluster_collection) == expected_value, "Unexpected " \ - "results: {0}".format(cluster_collection) - - @pytest.mark.xfail(platform.machine() == "arm64" and platform.system() == "Darwin", - reason="see gh-3599") + assert ( + len(cluster_collection) == expected_value + ), "Unexpected " "results: {0}".format(cluster_collection) + + @pytest.mark.xfail( + platform.machine() == "arm64" and platform.system() == "Darwin", + reason="see gh-3599", + ) def test_clustering_three_ensembles_two_identical(self, ens1, ens2): cluster_collection = encore.cluster([ens1, ens2, ens1]) expected_value = 40 - assert len(cluster_collection) == expected_value, "Unexpected result:" \ - " {0}".format(cluster_collection) + assert ( + len(cluster_collection) == expected_value + ), "Unexpected result:" " {0}".format(cluster_collection) def test_clustering_two_methods(self, ens1): cluster_collection = encore.cluster( [ens1], - method=[encore.AffinityPropagationNative(), - encore.AffinityPropagationNative()]) - assert len(cluster_collection[0]) == len(cluster_collection[1]), \ - "Unexpected result: {0}".format(cluster_collection) + method=[ + encore.AffinityPropagationNative(), + encore.AffinityPropagationNative(), + ], + ) + assert len(cluster_collection[0]) == len( + cluster_collection[1] + ), "Unexpected result: {0}".format(cluster_collection) def test_clustering_AffinityPropagationNative_direct(self, ens1): method = encore.AffinityPropagationNative() distance_matrix = encore.get_distance_matrix(ens1) cluster_assignment = method(distance_matrix) expected_value = 7 - assert len(set(cluster_assignment)) == expected_value, \ - "Unexpected result: {0}".format(cluster_assignment) + assert ( + len(set(cluster_assignment)) == expected_value + ), "Unexpected result: {0}".format(cluster_assignment) def test_clustering_AffinityPropagation_direct(self, ens1): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") method = encore.AffinityPropagation(random_state=0) distance_matrix = encore.get_distance_matrix(ens1) cluster_assignment = method(distance_matrix) expected_value = 7 - assert len(set(cluster_assignment)) == expected_value, \ - "Unexpected result: {0}".format(cluster_assignment) + assert ( + len(set(cluster_assignment)) == expected_value + ), "Unexpected result: {0}".format(cluster_assignment) def test_clustering_KMeans_direct(self, ens1): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") clusters = 10 method = encore.KMeans(clusters) - coordinates = ens1.trajectory.timeseries(order='fac') - coordinates = np.reshape(coordinates, - (coordinates.shape[0], -1)) + coordinates = ens1.trajectory.timeseries(order="fac") + coordinates = np.reshape(coordinates, (coordinates.shape[0], -1)) cluster_assignment = method(coordinates) - assert len(set(cluster_assignment)) == clusters, \ - "Unexpected result: {0}".format(cluster_assignment) + assert ( + len(set(cluster_assignment)) == clusters + ), "Unexpected result: {0}".format(cluster_assignment) def test_clustering_DBSCAN_direct(self, ens1): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") method = encore.DBSCAN(eps=0.5, min_samples=2) distance_matrix = encore.get_distance_matrix(ens1) cluster_assignment = method(distance_matrix) expected_value = 2 - assert len(set(cluster_assignment)) == expected_value, \ - "Unexpected result: {0}".format(cluster_assignment) + assert ( + len(set(cluster_assignment)) == expected_value + ), "Unexpected result: {0}".format(cluster_assignment) def test_clustering_two_different_methods(self, ens1): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") cluster_collection = encore.cluster( [ens1], - method=[encore.AffinityPropagation(preference=-7.5, - random_state=0), - encore.DBSCAN(min_samples=2)]) - assert len(cluster_collection[0]) == len(cluster_collection[1]), \ - "Unexpected result: {0}".format(cluster_collection) + method=[ + encore.AffinityPropagation(preference=-7.5, random_state=0), + encore.DBSCAN(min_samples=2), + ], + ) + assert len(cluster_collection[0]) == len( + cluster_collection[1] + ), "Unexpected result: {0}".format(cluster_collection) def test_clustering_method_w_no_distance_matrix(self, ens1): - pytest.importorskip('sklearn') - cluster_collection = encore.cluster( - [ens1], - method=encore.KMeans(10)) - assert len(cluster_collection) == 10, \ - "Unexpected result: {0}".format(cluster_collection) + pytest.importorskip("sklearn") + cluster_collection = encore.cluster([ens1], method=encore.KMeans(10)) + assert len(cluster_collection) == 10, "Unexpected result: {0}".format( + cluster_collection + ) def test_clustering_two_methods_one_w_no_distance_matrix(self, ens1): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") cluster_collection = encore.cluster( [ens1], - method=[encore.KMeans(17), - encore.AffinityPropagationNative()]) - assert len(cluster_collection[0]) == len(cluster_collection[0]), \ - "Unexpected result: {0}".format(cluster_collection) + method=[encore.KMeans(17), encore.AffinityPropagationNative()], + ) + assert len(cluster_collection[0]) == len( + cluster_collection[0] + ), "Unexpected result: {0}".format(cluster_collection) def test_sklearn_affinity_propagation(self, ens1): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") cc1 = encore.cluster([ens1]) - cc2 = encore.cluster([ens1], - method=encore.AffinityPropagation(random_state=0)) - assert len(cc1) == len(cc2), \ - "Native and sklearn implementations of affinity "\ - "propagation don't agree: mismatch in number of "\ - "clusters: {0} {1}".format(len(cc1), len(cc2)) + cc2 = encore.cluster( + [ens1], method=encore.AffinityPropagation(random_state=0) + ) + assert len(cc1) == len(cc2), ( + "Native and sklearn implementations of affinity " + "propagation don't agree: mismatch in number of " + "clusters: {0} {1}".format(len(cc1), len(cc2)) + ) def test_ClusterCollection_init(self, cc): err_msg = "ClusterCollection was not constructed correctly" @@ -631,24 +832,31 @@ def test_ClusterCollection_get_ids(self, cc): assert_equal( cc.get_ids(), [0, 1, 2], - err_msg="ClusterCollection ids aren't as expected") + err_msg="ClusterCollection ids aren't as expected", + ) def test_ClusterCollection_get_centroids(self, cc): assert_equal( - cc.get_centroids(), [1, 3, 5], - err_msg="ClusterCollection centroids aren't as expected") + cc.get_centroids(), + [1, 3, 5], + err_msg="ClusterCollection centroids aren't as expected", + ) + def test_cluster_add_metadata(self, cluster): - metadata = cluster.elements*10 - cluster.add_metadata('test', metadata) + metadata = cluster.elements * 10 + cluster.add_metadata("test", metadata) assert_equal( - cluster.metadata['test'], + cluster.metadata["test"], metadata, - err_msg="Cluster metadata isn't as expected") + err_msg="Cluster metadata isn't as expected", + ) metadata = np.append(metadata, 9) - error_message = ("Size of metadata is not equal to the " - "number of cluster elements") + error_message = ( + "Size of metadata is not equal to the " + "number of cluster elements" + ) with pytest.raises(TypeError, match=error_message): - cluster.add_metadata('test2', metadata) + cluster.add_metadata("test2", metadata) def test_empty_cluster(self): empty_cluster = encore.Cluster() @@ -663,11 +871,14 @@ def test_centroid_not_in_elements(self): encore.Cluster([38, 39, 40, 41, 42, 43], 99) def test_metadata_size_error(self): - error_message = ('Size of metadata having label "label" is ' - 'not equal to the number of cluster elements') + error_message = ( + 'Size of metadata having label "label" is ' + "not equal to the number of cluster elements" + ) with pytest.raises(TypeError, match=error_message): - encore.Cluster(np.array([1, 1, 1]), 1, None, - {"label": [1, 1, 1, 1]}) + encore.Cluster( + np.array([1, 1, 1]), 1, None, {"label": [1, 1, 1, 1]} + ) def test_cluster_iteration(self, cluster): test = [] @@ -676,7 +887,7 @@ def test_cluster_iteration(self, cluster): assert_equal(cluster.elements, test) def test_cluster_len(self, cluster): - assert(cluster.size == len(cluster)) + assert cluster.size == len(cluster) def test_cluster_repr(self): repr_message = "" @@ -685,6 +896,7 @@ def test_cluster_repr(self): repr_message = "" assert_equal(repr(cluster), repr_message) + class TestEncoreClusteringSklearn(object): """The tests in this class were duplicated from the affinity propagation tests in scikit-learn""" @@ -693,75 +905,79 @@ class TestEncoreClusteringSklearn(object): @pytest.fixture() def distance_matrix(self): - X = np.array([[8.73101582, 8.85617874], - [11.61311169, 11.58774351], - [10.86083514, 11.06253959], - [9.45576027, 8.50606967], - [11.30441509, 11.04867001], - [8.63708065, 9.02077816], - [8.34792066, 9.1851129], - [11.06197897, 11.15126501], - [11.24563175, 9.36888267], - [10.83455241, 8.70101808], - [11.49211627, 11.48095194], - [10.6448857, 10.20768141], - [10.491806, 9.38775868], - [11.08330999, 9.39065561], - [10.83872922, 9.48897803], - [11.37890079, 8.93799596], - [11.70562094, 11.16006288], - [10.95871246, 11.1642394], - [11.59763163, 10.91793669], - [11.05761743, 11.5817094], - [8.35444086, 8.91490389], - [8.79613913, 8.82477028], - [11.00420001, 9.7143482], - [11.90790185, 10.41825373], - [11.39149519, 11.89635728], - [8.31749192, 9.78031016], - [11.59530088, 9.75835567], - [11.17754529, 11.13346973], - [11.01830341, 10.92512646], - [11.75326028, 8.46089638], - [11.74702358, 9.36241786], - [10.53075064, 9.77744847], - [8.67474149, 8.30948696], - [11.05076484, 9.16079575], - [8.79567794, 8.52774713], - [11.18626498, 8.38550253], - [10.57169895, 9.42178069], - [8.65168114, 8.76846013], - [11.12522708, 10.6583617], - [8.87537899, 9.02246614], - [9.29163622, 9.05159316], - [11.38003537, 10.93945712], - [8.74627116, 8.85490353], - [10.65550973, 9.76402598], - [8.49888186, 9.31099614], - [8.64181338, 9.154761], - [10.84506927, 10.8790789], - [8.98872711, 9.17133275], - [11.7470232, 10.60908885], - [10.89279865, 9.32098256], - [11.14254656, 9.28262927], - [9.02660689, 9.12098876], - [9.16093666, 8.72607596], - [11.47151183, 8.92803007], - [11.76917681, 9.59220592], - [9.97880407, 11.26144744], - [8.58057881, 8.43199283], - [10.53394006, 9.36033059], - [11.34577448, 10.70313399], - [9.07097046, 8.83928763]]) - - XX = np.einsum('ij,ij->i', X, X)[:, np.newaxis] + X = np.array( + [ + [8.73101582, 8.85617874], + [11.61311169, 11.58774351], + [10.86083514, 11.06253959], + [9.45576027, 8.50606967], + [11.30441509, 11.04867001], + [8.63708065, 9.02077816], + [8.34792066, 9.1851129], + [11.06197897, 11.15126501], + [11.24563175, 9.36888267], + [10.83455241, 8.70101808], + [11.49211627, 11.48095194], + [10.6448857, 10.20768141], + [10.491806, 9.38775868], + [11.08330999, 9.39065561], + [10.83872922, 9.48897803], + [11.37890079, 8.93799596], + [11.70562094, 11.16006288], + [10.95871246, 11.1642394], + [11.59763163, 10.91793669], + [11.05761743, 11.5817094], + [8.35444086, 8.91490389], + [8.79613913, 8.82477028], + [11.00420001, 9.7143482], + [11.90790185, 10.41825373], + [11.39149519, 11.89635728], + [8.31749192, 9.78031016], + [11.59530088, 9.75835567], + [11.17754529, 11.13346973], + [11.01830341, 10.92512646], + [11.75326028, 8.46089638], + [11.74702358, 9.36241786], + [10.53075064, 9.77744847], + [8.67474149, 8.30948696], + [11.05076484, 9.16079575], + [8.79567794, 8.52774713], + [11.18626498, 8.38550253], + [10.57169895, 9.42178069], + [8.65168114, 8.76846013], + [11.12522708, 10.6583617], + [8.87537899, 9.02246614], + [9.29163622, 9.05159316], + [11.38003537, 10.93945712], + [8.74627116, 8.85490353], + [10.65550973, 9.76402598], + [8.49888186, 9.31099614], + [8.64181338, 9.154761], + [10.84506927, 10.8790789], + [8.98872711, 9.17133275], + [11.7470232, 10.60908885], + [10.89279865, 9.32098256], + [11.14254656, 9.28262927], + [9.02660689, 9.12098876], + [9.16093666, 8.72607596], + [11.47151183, 8.92803007], + [11.76917681, 9.59220592], + [9.97880407, 11.26144744], + [8.58057881, 8.43199283], + [10.53394006, 9.36033059], + [11.34577448, 10.70313399], + [9.07097046, 8.83928763], + ] + ) + + XX = np.einsum("ij,ij->i", X, X)[:, np.newaxis] YY = XX.T distances = np.dot(X, X.T) distances *= -2 distances += XX distances += YY np.maximum(distances, 0, out=distances) - distances.flat[::distances.shape[0] + 1] = 0.0 + distances.flat[:: distances.shape[0] + 1] = 0.0 dimension = len(distances) distance_matrix = encore.utils.TriangularMatrix(len(distances)) @@ -771,24 +987,27 @@ def distance_matrix(self): return distance_matrix def test_one(self, distance_matrix): - preference = -float(np.median(distance_matrix.as_array()) * 10.) - clustering_method = encore.AffinityPropagationNative(preference=preference) - ccs = encore.cluster(None, - distance_matrix=distance_matrix, - method=clustering_method) - assert self.n_clusters == len(ccs), \ - "Basic clustering test failed to give the right"\ - "number of clusters: {0} vs {1}".format(self.n_clusters, len(ccs)) + preference = -float(np.median(distance_matrix.as_array()) * 10.0) + clustering_method = encore.AffinityPropagationNative( + preference=preference + ) + ccs = encore.cluster( + None, distance_matrix=distance_matrix, method=clustering_method + ) + assert self.n_clusters == len(ccs), ( + "Basic clustering test failed to give the right" + "number of clusters: {0} vs {1}".format(self.n_clusters, len(ccs)) + ) class TestEncoreDimensionalityReduction(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ens1_template(self): template = mda.Universe(PSF, DCD) template.transfer_to_memory(step=5) return template - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def ens2_template(self): template = mda.Universe(PSF, DCD2) template.transfer_to_memory(step=5) @@ -798,126 +1017,168 @@ def ens2_template(self): def ens1(self, ens1_template): return mda.Universe( ens1_template.filename, - ens1_template.trajectory.timeseries(order='fac'), - format=mda.coordinates.memory.MemoryReader) + ens1_template.trajectory.timeseries(order="fac"), + format=mda.coordinates.memory.MemoryReader, + ) @pytest.fixture() def ens2(self, ens2_template): return mda.Universe( ens2_template.filename, - ens2_template.trajectory.timeseries(order='fac'), - format=mda.coordinates.memory.MemoryReader) + ens2_template.trajectory.timeseries(order="fac"), + format=mda.coordinates.memory.MemoryReader, + ) def test_dimensionality_reduction_one_ensemble(self, ens1): dimension = 2 coordinates, details = encore.reduce_dimensionality(ens1) - assert_equal(coordinates.shape[0], dimension, - err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) + assert_equal( + coordinates.shape[0], + dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates + ), + ) def test_dimensionality_reduction_two_ensembles(self, ens1, ens2): dimension = 2 - coordinates, details = \ - encore.reduce_dimensionality([ens1, ens2]) - assert_equal(coordinates.shape[0], dimension, - err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - - - def test_dimensionality_reduction_three_ensembles_two_identical(self, - ens1, ens2): - coordinates, details = \ - encore.reduce_dimensionality([ens1, ens2, ens1]) - coordinates_ens1 = coordinates[:,np.where(details["ensemble_membership"]==1)] - coordinates_ens3 = coordinates[:,np.where(details["ensemble_membership"]==3)] - assert_allclose(coordinates_ens1, coordinates_ens3, rtol=0, atol=1.5, - err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) + coordinates, details = encore.reduce_dimensionality([ens1, ens2]) + assert_equal( + coordinates.shape[0], + dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates + ), + ) + def test_dimensionality_reduction_three_ensembles_two_identical( + self, ens1, ens2 + ): + coordinates, details = encore.reduce_dimensionality([ens1, ens2, ens1]) + coordinates_ens1 = coordinates[ + :, np.where(details["ensemble_membership"] == 1) + ] + coordinates_ens3 = coordinates[ + :, np.where(details["ensemble_membership"] == 3) + ] + assert_allclose( + coordinates_ens1, + coordinates_ens3, + rtol=0, + atol=1.5, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates + ), + ) def test_dimensionality_reduction_specified_dimension(self, ens1, ens2): dimension = 3 coordinates, details = encore.reduce_dimensionality( [ens1, ens2], - method=encore.StochasticProximityEmbeddingNative(dimension=dimension)) - assert_equal(coordinates.shape[0], dimension, - err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - + method=encore.StochasticProximityEmbeddingNative( + dimension=dimension + ), + ) + assert_equal( + coordinates.shape[0], + dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates + ), + ) def test_dimensionality_reduction_SPENative_direct(self, ens1): dimension = 2 method = encore.StochasticProximityEmbeddingNative(dimension=dimension) distance_matrix = encore.get_distance_matrix(ens1) coordinates, details = method(distance_matrix) - assert_equal(coordinates.shape[0], dimension, - err_msg="Unexpected result in dimensionality reduction: {0}".format( - coordinates)) + assert_equal( + coordinates.shape[0], + dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates + ), + ) def test_dimensionality_reduction_PCA_direct(self, ens1): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") dimension = 2 method = encore.PrincipalComponentAnalysis(dimension=dimension) - coordinates = ens1.trajectory.timeseries(order='fac') - coordinates = np.reshape(coordinates, - (coordinates.shape[0], -1)) + coordinates = ens1.trajectory.timeseries(order="fac") + coordinates = np.reshape(coordinates, (coordinates.shape[0], -1)) coordinates, details = method(coordinates) - assert_equal(coordinates.shape[0], dimension, - err_msg="Unexpected result in dimensionality reduction: {0}".format( - coordinates)) - + assert_equal( + coordinates.shape[0], + dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates + ), + ) def test_dimensionality_reduction_different_method(self, ens1, ens2): - pytest.importorskip('sklearn') + pytest.importorskip("sklearn") dimension = 3 - coordinates, details = \ - encore.reduce_dimensionality( - [ens1, ens2], - method=encore.PrincipalComponentAnalysis(dimension=dimension)) - assert_equal(coordinates.shape[0], dimension, - err_msg="Unexpected result in dimensionality reduction: {0}".format(coordinates)) - + coordinates, details = encore.reduce_dimensionality( + [ens1, ens2], + method=encore.PrincipalComponentAnalysis(dimension=dimension), + ) + assert_equal( + coordinates.shape[0], + dimension, + err_msg="Unexpected result in dimensionality reduction: {0}".format( + coordinates + ), + ) def test_dimensionality_reduction_two_methods(self, ens1, ens2): - dims = [2,3] - coordinates, details = \ - encore.reduce_dimensionality( - [ens1, ens2], - method=[encore.StochasticProximityEmbeddingNative(dims[0]), - encore.StochasticProximityEmbeddingNative(dims[1])]) + dims = [2, 3] + coordinates, details = encore.reduce_dimensionality( + [ens1, ens2], + method=[ + encore.StochasticProximityEmbeddingNative(dims[0]), + encore.StochasticProximityEmbeddingNative(dims[1]), + ], + ) assert_equal(coordinates[1].shape[0], dims[1]) def test_dimensionality_reduction_two_different_methods(self, ens1, ens2): - pytest.importorskip('sklearn') - dims = [2,3] - coordinates, details = \ - encore.reduce_dimensionality( - [ens1, ens2], - method=[encore.StochasticProximityEmbeddingNative(dims[0]), - encore.PrincipalComponentAnalysis(dims[1])]) + pytest.importorskip("sklearn") + dims = [2, 3] + coordinates, details = encore.reduce_dimensionality( + [ens1, ens2], + method=[ + encore.StochasticProximityEmbeddingNative(dims[0]), + encore.PrincipalComponentAnalysis(dims[1]), + ], + ) assert_equal(coordinates[1].shape[0], dims[1]) class TestEncoreConfDistMatrix(object): def test_get_distance_matrix(self): # Issue #1324 - u = mda.Universe(TPR,XTC) + u = mda.Universe(TPR, XTC) dm = confdistmatrix.get_distance_matrix(u) + class TestEncoreImportWarnings(object): - @block_import('sklearn') + @block_import("sklearn") def _check_sklearn_import_warns(self, package, recwarn): for mod in list(sys.modules): # list as we're changing as we iterate - if 'encore' in mod: + if "encore" in mod: sys.modules.pop(mod, None) - warnings.simplefilter('always') + warnings.simplefilter("always") # assert_warns(ImportWarning, importlib.import_module, package) importlib.import_module(package) assert recwarn.pop(ImportWarning) def test_import_warnings(self, recwarn): for mod in list(sys.modules): # list as we're changing as we iterate - if 'encore' in mod: + if "encore" in mod: sys.modules.pop(mod, None) for pkg in ( - 'MDAnalysis.analysis.encore.dimensionality_reduction.DimensionalityReductionMethod', - 'MDAnalysis.analysis.encore.clustering.ClusteringMethod', + "MDAnalysis.analysis.encore.dimensionality_reduction.DimensionalityReductionMethod", + "MDAnalysis.analysis.encore.clustering.ClusteringMethod", ): self._check_sklearn_import_warns(pkg, recwarn) # This is a quickfix! Convert this to a parametrize call in future. diff --git a/testsuite/MDAnalysisTests/analysis/test_gnm.py b/testsuite/MDAnalysisTests/analysis/test_gnm.py index e69ac7056fe..0283ca66b6b 100644 --- a/testsuite/MDAnalysisTests/analysis/test_gnm.py +++ b/testsuite/MDAnalysisTests/analysis/test_gnm.py @@ -25,10 +25,9 @@ import MDAnalysis as mda import MDAnalysis.analysis.gnm - -from numpy.testing import assert_almost_equal import numpy as np import pytest +from numpy.testing import assert_almost_equal from MDAnalysisTests.datafiles import GRO, XTC @@ -39,16 +38,27 @@ def universe(): def test_gnm(universe, tmpdir, client_GNMAnalysis): - output = os.path.join(str(tmpdir), 'output.txt') + output = os.path.join(str(tmpdir), "output.txt") gnm = mda.analysis.gnm.GNMAnalysis(universe, ReportVector=output) 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) - assert_almost_equal(gnm.results.eigenvalues, - [2.0287113e-15, 4.1471575e-15, 1.8539533e-15, 4.3810359e-15, - 3.9607304e-15, 4.1289113e-15, 2.5501084e-15, 4.0498182e-15, - 4.2058769e-15, 3.9839431e-15]) + assert_almost_equal( + gnm.results.eigenvalues, + [ + 2.0287113e-15, + 4.1471575e-15, + 1.8539533e-15, + 4.3810359e-15, + 3.9607304e-15, + 4.1289113e-15, + 2.5501084e-15, + 4.0498182e-15, + 4.2058769e-15, + 3.9839431e-15, + ], + ) def test_gnm_run_step(universe, client_GNMAnalysis): @@ -57,26 +67,34 @@ def test_gnm_run_step(universe, client_GNMAnalysis): result = gnm.results assert len(result.times) == 4 assert_almost_equal(gnm.results.times, np.arange(0, 1200, 300), decimal=4) - assert_almost_equal(gnm.results.eigenvalues, - [2.0287113e-15, 4.3810359e-15, 2.5501084e-15, 3.9839431e-15]) + assert_almost_equal( + gnm.results.eigenvalues, + [2.0287113e-15, 4.3810359e-15, 2.5501084e-15, 3.9839431e-15], + ) def test_generate_kirchoff(universe): gnm = mda.analysis.gnm.GNMAnalysis(universe) gen = gnm.generate_kirchoff() - assert_almost_equal(gen[0], - [7,-1,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0,-1,-1,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,-1, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + # fmt: off + assert_almost_equal( + gen[0], + [ + 7,-1,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0,-1,-1,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,-1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ) + # fmt: on def test_gnm_SVD_fail(universe): @@ -93,25 +111,36 @@ def test_closeContactGNMAnalysis(universe, client_GNMAnalysis): gnm.run(stop=2, **client_GNMAnalysis) result = gnm.results assert len(result.times) == 2 - assert_almost_equal(gnm.results.times, (0, 100), decimal=4) - assert_almost_equal(gnm.results.eigenvalues, [0.1502614, 0.1426407]) + assert_almost_equal(gnm.results.times, (0, 100), decimal=4) + assert_almost_equal(gnm.results.eigenvalues, [0.1502614, 0.1426407]) gen = gnm.generate_kirchoff() - assert_almost_equal(gen[0], - [16.326744128018923, -2.716098853586913, -1.94736842105263, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, -0.05263157894736842, 0.0, 0.0, 0.0, -3.3541953679557905, 0.0, -1.4210526315789465, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, -1.0423368771244421, -1.3006649542861801, -0.30779350562554625, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.927172649945531, -0.7509392614826383, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -2.263157894736841, -0.24333213169614382]) + # fmt: off + assert_almost_equal( + gen[0], + [ + 16.326744128018923, -2.716098853586913, -1.94736842105263, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -0.05263157894736842, 0.0, 0.0, 0.0, -3.3541953679557905, + 0.0, -1.4210526315789465, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, -1.0423368771244421, -1.3006649542861801, + -0.30779350562554625, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + -0.927172649945531, -0.7509392614826383, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + -2.263157894736841, -0.24333213169614382 + ] + ) + # fmt: on def test_closeContactGNMAnalysis_weights_None(universe, client_GNMAnalysis): @@ -120,17 +149,29 @@ def test_closeContactGNMAnalysis_weights_None(universe, client_GNMAnalysis): result = gnm.results assert len(result.times) == 2 assert_almost_equal(gnm.results.times, (0, 100), decimal=4) - assert_almost_equal(gnm.results.eigenvalues, [2.4328739, 2.2967251]) + assert_almost_equal(gnm.results.eigenvalues, [2.4328739, 2.2967251]) gen = gnm.generate_kirchoff() - assert_almost_equal(gen[0], - [303.0, -58.0, -37.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, - 0.0, 0.0, 0.0, -67.0, 0.0, -27.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -17.0, -15.0, - -6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, -14.0, -15.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -43.0, -3.0]) + # fmt: off + assert_almost_equal( + gen[0], + [ + 303.0, -58.0, -37.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0,0.0, 0.0, 0.0, -67.0, 0.0, + -27.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, -17.0, -15.0, -6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, -14.0, -15.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -43.0, -3.0 + ] + ) + # fmt: on diff --git a/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py b/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py index dbde56fde4e..3a291050d46 100644 --- a/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py +++ b/testsuite/MDAnalysisTests/analysis/test_helix_analysis.py @@ -28,10 +28,16 @@ import MDAnalysis as mda from MDAnalysis.analysis import helix_analysis as hel -from MDAnalysisTests.datafiles import (GRO, XTC, PSF, DCD, PDB_small, - HELANAL_BENDING_MATRIX, - HELANAL_BENDING_MATRIX_SUBSET, - XYZ) +from MDAnalysisTests.datafiles import ( + GRO, + XTC, + PSF, + DCD, + PDB_small, + HELANAL_BENDING_MATRIX, + HELANAL_BENDING_MATRIX_SUBSET, + XYZ, +) # reference data from old helix analysis of a single PDB file: # data = MDAnalysis.analysis.helanal.helanal_main(PDB_small, @@ -39,28 +45,69 @@ # keys are renamed and local screw angles now use a different # algorithm HELANAL_SINGLE_DATA = { - 'global_tilts': np.rad2deg(1.3309656332019535), - 'local_heights summary': np.array([1.5286051, 0.19648294, 0.11384312], - dtype=np.float32), - 'local_bends': - np.array([3.44526005, 4.85425806, 4.69548464, 2.39473653, - 3.56172442, 3.97128344, 3.41563916, 1.86140978, - 5.22997046, 5.41381264, 27.49601364, 39.69839478, - 35.05921936, 21.78928566, 9.8632431, 8.80066967, - 5.5344553, 6.14356709, 10.15450764, 11.07686138, - 9.23541832], dtype=np.float32), - 'local_nres_per_turn summary': np.array([3.64864163, 0.152694, - 0.1131402]), - 'local_twists summary': np.array([98.83011627, 4.08171701, 3.07253003], - dtype=np.float32), - 'local_twists': - np.array([97.23709869, 99.09676361, 97.25350952, 101.76019287, - 100.42689514, 97.08784485, 97.07430267, 98.33553314, - 97.86578369, 95.45792389, 97.10089111, 95.26415253, - 87.93136597, 108.38458252, 95.27167511, 104.01931763, - 100.7199707, 101.48034668, 99.64170074, 94.78946686, - 102.50147247, 97.25154877, 104.54204559, 101.42829895], - dtype=np.float32), + "global_tilts": np.rad2deg(1.3309656332019535), + "local_heights summary": np.array( + [1.5286051, 0.19648294, 0.11384312], dtype=np.float32 + ), + "local_bends": np.array( + [ + 3.44526005, + 4.85425806, + 4.69548464, + 2.39473653, + 3.56172442, + 3.97128344, + 3.41563916, + 1.86140978, + 5.22997046, + 5.41381264, + 27.49601364, + 39.69839478, + 35.05921936, + 21.78928566, + 9.8632431, + 8.80066967, + 5.5344553, + 6.14356709, + 10.15450764, + 11.07686138, + 9.23541832, + ], + dtype=np.float32, + ), + "local_nres_per_turn summary": np.array([3.64864163, 0.152694, 0.1131402]), + "local_twists summary": np.array( + [98.83011627, 4.08171701, 3.07253003], dtype=np.float32 + ), + "local_twists": np.array( + [ + 97.23709869, + 99.09676361, + 97.25350952, + 101.76019287, + 100.42689514, + 97.08784485, + 97.07430267, + 98.33553314, + 97.86578369, + 95.45792389, + 97.10089111, + 95.26415253, + 87.93136597, + 108.38458252, + 95.27167511, + 104.01931763, + 100.7199707, + 101.48034668, + 99.64170074, + 94.78946686, + 102.50147247, + 97.25154877, + 104.54204559, + 101.42829895, + ], + dtype=np.float32, + ), } @@ -113,8 +160,9 @@ def test_local_screw_angles_plane_circle(): """ angdeg = np.arange(0, 360, 12, dtype=np.int32) angrad = np.deg2rad(angdeg, dtype=np.float64) - xyz = np.array([[np.cos(a), np.sin(a), 0] for a in angrad], - dtype=np.float64) + xyz = np.array( + [[np.cos(a), np.sin(a), 0] for a in angrad], dtype=np.float64 + ) xyz[15, 1] = 0 # because np.sin(np.deg2rad(180)) = 1e-16 ?! screw = hel.local_screw_angles([1, 0, 0], [0, 1, 0], xyz) correct = np.zeros_like(angdeg) @@ -130,8 +178,9 @@ def test_local_screw_angles_ortho_circle(): """ angdeg = np.arange(0, 360, 12, dtype=np.int32) angrad = np.deg2rad(angdeg, dtype=np.float64) - xyz = np.array([[np.cos(a), np.sin(a), 0] for a in angrad], - dtype=np.float64) + xyz = np.array( + [[np.cos(a), np.sin(a), 0] for a in angrad], dtype=np.float64 + ) xyz[15, 1] = 0 # because np.sin(np.deg2rad(180)) = 1e-16 ?! screw = hel.local_screw_angles([1, 0, 0], [0, 0, 1], xyz) correct = np.zeros_like(angdeg) @@ -197,57 +246,70 @@ def zigzag(): x x x x x """ n_atoms = 100 - u = mda.Universe.empty(100, atom_resindex=np.arange(n_atoms), - trajectory=True) - xyz = np.array(list(zip([1, -1]*(n_atoms//2), # x \in {0, 1} - [0]*n_atoms, # y == 0 - range(n_atoms)))) # z rises continuously + u = mda.Universe.empty( + 100, atom_resindex=np.arange(n_atoms), trajectory=True + ) + xyz = np.array( + list( + zip( + [1, -1] * (n_atoms // 2), # x \in {0, 1} + [0] * n_atoms, # y == 0 + range(n_atoms), + ) + ) + ) # z rises continuously u.load_new(xyz) return u -@pytest.mark.parametrize('ref_axis,screw_angles', [ - # input vectors zigzag between [-1, 0, 0] and [1, 0, 0] - # global axis is z-axis - ([0, 0, 1], [180, 0]), # calculated to x-z plane - ([1, 0, 0], [180, 0]), # calculated to x-z plane - ([-1, 0, 0], [0, 180]), # calculated to x-z plane upside down - ([0, 1, 0], [90, -90]), # calculated to y-z plane - ([0, -1, 0], [-90, 90]), # calculated to x-z plane upside down - # calculated to diagonal xy-z plane rotating around - ([1, 1, 0], [135, -45]), - ([-1, 1, 0], [45, -135]), - ([1, -1, 0], [-135, 45]), - ([-1, -1, 0], [-45, 135]), - # calculated to diagonal xyz-z plane w/o z contribution - ([1, 1, 1], [135, -45]), - ([1, 1, -1], [135, -45]), - ([1, -1, 1], [-135, 45]), - ([-1, 1, 1], [45, -135]), - ([-1, -1, 1], [-45, 135]), - ([-1, -1, -1], [-45, 135]), -]) +@pytest.mark.parametrize( + "ref_axis,screw_angles", + [ + # input vectors zigzag between [-1, 0, 0] and [1, 0, 0] + # global axis is z-axis + ([0, 0, 1], [180, 0]), # calculated to x-z plane + ([1, 0, 0], [180, 0]), # calculated to x-z plane + ([-1, 0, 0], [0, 180]), # calculated to x-z plane upside down + ([0, 1, 0], [90, -90]), # calculated to y-z plane + ([0, -1, 0], [-90, 90]), # calculated to x-z plane upside down + # calculated to diagonal xy-z plane rotating around + ([1, 1, 0], [135, -45]), + ([-1, 1, 0], [45, -135]), + ([1, -1, 0], [-135, 45]), + ([-1, -1, 0], [-45, 135]), + # calculated to diagonal xyz-z plane w/o z contribution + ([1, 1, 1], [135, -45]), + ([1, 1, -1], [135, -45]), + ([1, -1, 1], [-135, 45]), + ([-1, 1, 1], [45, -135]), + ([-1, -1, 1], [-45, 135]), + ([-1, -1, -1], [-45, 135]), + ], +) def test_helix_analysis_zigzag(zigzag, ref_axis, screw_angles): - properties = hel.helix_analysis(zigzag.atoms.positions, - ref_axis=ref_axis) - assert_almost_equal(properties['local_twists'], 180, decimal=4) - assert_almost_equal(properties['local_nres_per_turn'], 2, decimal=4) - assert_almost_equal(properties['global_axis'], - [0, 0, -1], decimal=4) + properties = hel.helix_analysis(zigzag.atoms.positions, ref_axis=ref_axis) + assert_almost_equal(properties["local_twists"], 180, decimal=4) + assert_almost_equal(properties["local_nres_per_turn"], 2, decimal=4) + assert_almost_equal(properties["global_axis"], [0, 0, -1], decimal=4) # all 0 vectors - assert_almost_equal(properties['local_axes'], 0, decimal=4) - assert_almost_equal(properties['local_bends'], 0, decimal=4) - assert_almost_equal(properties['all_bends'], 0, decimal=4) - assert_almost_equal(properties['local_heights'], 0, decimal=4) - assert_almost_equal(properties['local_helix_directions'][0::2], - [[-1, 0, 0]]*49, decimal=4) - assert_almost_equal(properties['local_helix_directions'][1::2], - [[1, 0, 0]]*49, decimal=4) + assert_almost_equal(properties["local_axes"], 0, decimal=4) + assert_almost_equal(properties["local_bends"], 0, decimal=4) + assert_almost_equal(properties["all_bends"], 0, decimal=4) + assert_almost_equal(properties["local_heights"], 0, decimal=4) + assert_almost_equal( + properties["local_helix_directions"][0::2], + [[-1, 0, 0]] * 49, + decimal=4, + ) + assert_almost_equal( + properties["local_helix_directions"][1::2], [[1, 0, 0]] * 49, decimal=4 + ) origins = zigzag.atoms.positions[1:-1].copy() origins[:, 0] = 0 - assert_almost_equal(properties['local_origins'], origins, decimal=4) - assert_almost_equal(properties['local_screw_angles'], - screw_angles*49, decimal=4) + assert_almost_equal(properties["local_origins"], origins, decimal=4) + assert_almost_equal( + properties["local_screw_angles"], screw_angles * 49, decimal=4 + ) def square_oct(n_rep=10): @@ -258,13 +320,20 @@ def square_oct(n_rep=10): x-coordinates increase continuously. """ # square-octagon-square-octagon - sq2 = 0.5 ** 0.5 + sq2 = 0.5**0.5 square = [(1, 0), (0, 1), (-1, 0), (0, -1)] - octagon = [(1, 0), (sq2, sq2), (0, 1), (-sq2, sq2), - (-1, 0), (-sq2, -sq2), (0, -1), (sq2, -sq2)] - shapes = (square+octagon)*n_rep - xyz = np.array(list(zip(np.arange(len(shapes)), - *zip(*shapes)))) + octagon = [ + (1, 0), + (sq2, sq2), + (0, 1), + (-sq2, sq2), + (-1, 0), + (-sq2, -sq2), + (0, -1), + (sq2, -sq2), + ] + shapes = (square + octagon) * n_rep + xyz = np.array(list(zip(np.arange(len(shapes)), *zip(*shapes)))) n_atoms = len(xyz) u = mda.Universe.empty(n_atoms, trajectory=True) u.load_new(xyz) @@ -276,64 +345,82 @@ def test_helix_analysis_square_oct(): u = square_oct(n_rep=n_rep) n_atoms = len(u.atoms) - properties = hel.helix_analysis(u.atoms.positions, - ref_axis=[0, 0, 1]) + properties = hel.helix_analysis(u.atoms.positions, ref_axis=[0, 0, 1]) twist_trans = [102.76438, 32.235607] - twists = ([90]*2 + twist_trans + [45]*6 + twist_trans[::-1]) * n_rep - assert_almost_equal(properties['local_twists'], twists[:n_atoms-3], - decimal=4) + twists = ([90] * 2 + twist_trans + [45] * 6 + twist_trans[::-1]) * n_rep + assert_almost_equal( + properties["local_twists"], twists[: n_atoms - 3], decimal=4 + ) res_trans = [3.503159, 11.167775] - res = ([4]*2 + res_trans + [8]*6 + res_trans[::-1]) * n_rep - assert_almost_equal(properties['local_nres_per_turn'], res[:n_atoms-3], - decimal=4) - assert_almost_equal(properties['global_axis'], - [-1, 0, 0], decimal=3) - assert_almost_equal(properties['local_axes']-[1, 0, 0], 0, decimal=4) - assert_almost_equal(properties['local_bends'], 0, decimal=4) - assert_almost_equal(properties['all_bends'], 0, decimal=4) - assert_almost_equal(properties['local_heights'], 1, decimal=4) - - loc_rot = [[0., 0., 1.], - [0., -1., 0.], - [0., 0., -1.], - [0., 0.97528684, 0.2209424], # the transition square->oct - [0., 0.70710677, 0.70710677], # 0.5 ** 0.5 - [0., 0., 1.], - [0., -0.70710677, 0.70710677], - [0., -1., 0.], - [0., -0.70710677, -0.70710677], - [0., 0., -1.], - [0., 0.70710677, -0.70710677], - [0., 0.97528684, -0.2209424]] * n_rep - assert_almost_equal(properties['local_helix_directions'], - loc_rot[:n_atoms-2], decimal=4) + res = ([4] * 2 + res_trans + [8] * 6 + res_trans[::-1]) * n_rep + assert_almost_equal( + properties["local_nres_per_turn"], res[: n_atoms - 3], decimal=4 + ) + assert_almost_equal(properties["global_axis"], [-1, 0, 0], decimal=3) + assert_almost_equal(properties["local_axes"] - [1, 0, 0], 0, decimal=4) + assert_almost_equal(properties["local_bends"], 0, decimal=4) + assert_almost_equal(properties["all_bends"], 0, decimal=4) + assert_almost_equal(properties["local_heights"], 1, decimal=4) + + loc_rot = [ + [0.0, 0.0, 1.0], + [0.0, -1.0, 0.0], + [0.0, 0.0, -1.0], + [0.0, 0.97528684, 0.2209424], # the transition square->oct + [0.0, 0.70710677, 0.70710677], # 0.5 ** 0.5 + [0.0, 0.0, 1.0], + [0.0, -0.70710677, 0.70710677], + [0.0, -1.0, 0.0], + [0.0, -0.70710677, -0.70710677], + [0.0, 0.0, -1.0], + [0.0, 0.70710677, -0.70710677], + [0.0, 0.97528684, -0.2209424], + ] * n_rep + assert_almost_equal( + properties["local_helix_directions"], loc_rot[: n_atoms - 2], decimal=4 + ) origins = u.atoms.positions.copy()[1:-1] - origins[:, 1:] = ([[0., 0.], # square - [0., 0.], - [0., -0.33318555], # square -> octagon - [-1.7878988, -0.6315732], # square -> octagon - [0., 0.], # octagon - [0., 0.], - [0., 0.], - [0., 0.], - [0., 0.], - [0., 0.], - [-1.3141878, 1.3141878], # octagon -> square - [0.34966463, 0.14732757]]*n_rep)[:len(origins)] - assert_almost_equal(properties['local_origins'], origins, - decimal=4) + origins[:, 1:] = ( + [ + [0.0, 0.0], # square + [0.0, 0.0], + [0.0, -0.33318555], # square -> octagon + [-1.7878988, -0.6315732], # square -> octagon + [0.0, 0.0], # octagon + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + [0.0, 0.0], + [-1.3141878, 1.3141878], # octagon -> square + [0.34966463, 0.14732757], + ] + * n_rep + )[: len(origins)] + assert_almost_equal(properties["local_origins"], origins, decimal=4) # calculated to the x-y plane # all input vectors (loc_rot) are in y-z plane - screw = [0, 90, 180, # square - -77.236, # transition - -45, 0, 45, 90, 135, 180, -135, # octagon - -102.764]*n_rep + screw = [ + 0, + 90, + 180, # square + -77.236, # transition + -45, + 0, + 45, + 90, + 135, + 180, + -135, # octagon + -102.764, + ] * n_rep # not quite 0, comes out as 1.32e-06 - assert_almost_equal(properties['local_screw_angles'], screw[:-2], - decimal=3) + assert_almost_equal( + properties["local_screw_angles"], screw[:-2], decimal=3 + ) class TestHELANAL(object): @@ -341,47 +428,58 @@ class TestHELANAL(object): @pytest.fixture() def psf_ca(self): u = mda.Universe(PSF, DCD) - ag = u.select_atoms('name CA') + ag = u.select_atoms("name CA") return ag @pytest.fixture() def helanal(self, psf_ca): - ha = hel.HELANAL(psf_ca, select='resnum 161-187', - flatten_single_helix=True) + ha = hel.HELANAL( + psf_ca, select="resnum 161-187", flatten_single_helix=True + ) return ha.run(start=10, stop=80) def test_regression_summary(self, helanal): - bends = helanal.results.summary['all_bends'] + bends = helanal.results.summary["all_bends"] old_helanal = read_bending_matrix(HELANAL_BENDING_MATRIX_SUBSET) - assert_almost_equal(np.triu(bends['mean'], 1), old_helanal['Mean'], - decimal=1) - assert_almost_equal(np.triu(bends['sample_sd'], 1), old_helanal['SD'], - decimal=1) - assert_almost_equal(np.triu(bends['abs_dev'], 1), old_helanal['ABDEV'], - decimal=1) + assert_almost_equal( + np.triu(bends["mean"], 1), old_helanal["Mean"], decimal=1 + ) + assert_almost_equal( + np.triu(bends["sample_sd"], 1), old_helanal["SD"], decimal=1 + ) + assert_almost_equal( + np.triu(bends["abs_dev"], 1), old_helanal["ABDEV"], decimal=1 + ) def test_regression_values(self): u = mda.Universe(PDB_small) - ha = hel.HELANAL(u, select='name CA and resnum 161-187', - flatten_single_helix=True) + ha = hel.HELANAL( + u, select="name CA and resnum 161-187", flatten_single_helix=True + ) ha.run() for key, value in HELANAL_SINGLE_DATA.items(): - if 'summary' in key: + if "summary" in key: data = ha.results[key.split()[0]] - calculated = [data.mean(), data.std(ddof=1), - np.fabs(data-data.mean()).mean()] + calculated = [ + data.mean(), + data.std(ddof=1), + np.fabs(data - data.mean()).mean(), + ] else: calculated = ha.results[key][0] - msg = 'Mismatch between calculated and reference {}' - assert_almost_equal(calculated, value, - decimal=4, - err_msg=msg.format(key)) + msg = "Mismatch between calculated and reference {}" + assert_almost_equal( + calculated, value, decimal=4, err_msg=msg.format(key) + ) def test_multiple_selections(self, psf_ca): - ha = hel.HELANAL(psf_ca, flatten_single_helix=True, - select=('resnum 30-40', 'resnum 60-80')) + ha = hel.HELANAL( + psf_ca, + flatten_single_helix=True, + select=("resnum 30-40", "resnum 60-80"), + ) ha.run() n_frames = len(psf_ca.universe.trajectory) assert len(ha.atomgroups) == 2 @@ -393,23 +491,23 @@ def test_multiple_selections(self, psf_ca): def test_universe_from_origins(self, helanal): u = helanal.universe_from_origins() assert isinstance(u, mda.Universe) - assert len(u.atoms) == len(helanal.atomgroups[0])-2 + assert len(u.atoms) == len(helanal.atomgroups[0]) - 2 assert len(u.trajectory) == 70 def test_universe_from_origins_except(self, psf_ca): - ha = hel.HELANAL(psf_ca, select='resnum 161-187') - with pytest.raises(ValueError, match=r'before universe_from_origins'): + ha = hel.HELANAL(psf_ca, select="resnum 161-187") + with pytest.raises(ValueError, match=r"before universe_from_origins"): u = ha.universe_from_origins() def test_multiple_atoms_per_residues(self): u = mda.Universe(XYZ) with pytest.warns(UserWarning) as rec: - ha = hel.HELANAL(u, select='name H') + ha = hel.HELANAL(u, select="name H") assert len(rec) == 1 - assert 'multiple atoms' in rec[0].message.args[0] + assert "multiple atoms" in rec[0].message.args[0] def test_residue_gaps_split(self, psf_ca): - sel = 'resid 6:50 or resid 100:130 or resid 132:148' + sel = "resid 6:50 or resid 100:130 or resid 132:148" with pytest.warns(UserWarning) as rec: ha = hel.HELANAL(psf_ca, select=sel).run() assert len(ha.atomgroups) == 3 @@ -418,52 +516,55 @@ def test_residue_gaps_split(self, psf_ca): assert len(ha.atomgroups[2]) == 17 assert len(rec) == 1 warnmsg = rec[0].message.args[0] - assert 'has gaps in the residues' in warnmsg - assert 'Splitting into 3 helices' in warnmsg + assert "has gaps in the residues" in warnmsg + assert "Splitting into 3 helices" in warnmsg def test_residue_gaps_no_split(self, psf_ca): - sel = 'resid 6:50 or resid 100:130 or resid 132:148' + sel = "resid 6:50 or resid 100:130 or resid 132:148" with pytest.warns(UserWarning) as rec: - ha = hel.HELANAL(psf_ca, select=sel, - split_residue_sequences=False) + ha = hel.HELANAL(psf_ca, select=sel, split_residue_sequences=False) ha.run() assert len(ha.atomgroups) == 1 - assert len(ha.atomgroups[0]) == 45+31+17 + assert len(ha.atomgroups[0]) == 45 + 31 + 17 assert len(rec) == 1 warnmsg = rec[0].message.args[0] - assert 'has gaps in the residues' in warnmsg - assert 'Splitting into' not in warnmsg + assert "has gaps in the residues" in warnmsg + assert "Splitting into" not in warnmsg def test_len_groups_short(self, psf_ca): - sel = 'resnum 161-168' - with pytest.warns(UserWarning, match='Fewer than 9 atoms found'): + sel = "resnum 161-168" + with pytest.warns(UserWarning, match="Fewer than 9 atoms found"): ha = hel.HELANAL(psf_ca, select=sel) assert len(ha.atomgroups) < 9 - @pytest.mark.parametrize('ref_axis,screw_angles', [ - # input vectors zigzag between [-1, 0, 0] and [1, 0, 0] - # global axis is z-axis - ([0, 0, 1], [180, 0]), # calculated to x-z plane - ([1, 0, 0], [180, 0]), # calculated to x-z plane - ([-1, 0, 0], [0, 180]), # calculated to x-z plane upside down - ([0, 1, 0], [90, -90]), # calculated to y-z plane - ([0, -1, 0], [-90, 90]), # calculated to x-z plane upside down - # calculated to diagonal xy-z plane rotating around - ([1, 1, 0], [135, -45]), - ([-1, 1, 0], [45, -135]), - ([1, -1, 0], [-135, 45]), - ([-1, -1, 0], [-45, 135]), - # calculated to diagonal xyz-z plane w/o z contribution - ([1, 1, 1], [135, -45]), - ([1, 1, -1], [135, -45]), - ([1, -1, 1], [-135, 45]), - ([-1, 1, 1], [45, -135]), - ([-1, -1, 1], [-45, 135]), - ([-1, -1, -1], [-45, 135]), - ]) + @pytest.mark.parametrize( + "ref_axis,screw_angles", + [ + # input vectors zigzag between [-1, 0, 0] and [1, 0, 0] + # global axis is z-axis + ([0, 0, 1], [180, 0]), # calculated to x-z plane + ([1, 0, 0], [180, 0]), # calculated to x-z plane + ([-1, 0, 0], [0, 180]), # calculated to x-z plane upside down + ([0, 1, 0], [90, -90]), # calculated to y-z plane + ([0, -1, 0], [-90, 90]), # calculated to x-z plane upside down + # calculated to diagonal xy-z plane rotating around + ([1, 1, 0], [135, -45]), + ([-1, 1, 0], [45, -135]), + ([1, -1, 0], [-135, 45]), + ([-1, -1, 0], [-45, 135]), + # calculated to diagonal xyz-z plane w/o z contribution + ([1, 1, 1], [135, -45]), + ([1, 1, -1], [135, -45]), + ([1, -1, 1], [-135, 45]), + ([-1, 1, 1], [45, -135]), + ([-1, -1, 1], [-45, 135]), + ([-1, -1, -1], [-45, 135]), + ], + ) def test_helanal_zigzag(self, zigzag, ref_axis, screw_angles): - ha = hel.HELANAL(zigzag, select="all", ref_axis=ref_axis, - flatten_single_helix=True).run() + ha = hel.HELANAL( + zigzag, select="all", ref_axis=ref_axis, flatten_single_helix=True + ).run() assert_almost_equal(ha.results.local_twists, 180, decimal=4) assert_almost_equal(ha.results.local_nres_per_turn, 2, decimal=4) assert_almost_equal(ha.results.global_axis, [[0, 0, -1]], decimal=4) @@ -472,21 +573,28 @@ def test_helanal_zigzag(self, zigzag, ref_axis, screw_angles): assert_almost_equal(ha.results.local_bends, 0, decimal=4) assert_almost_equal(ha.results.all_bends, 0, decimal=4) assert_almost_equal(ha.results.local_heights, 0, decimal=4) - assert_almost_equal(ha.results.local_helix_directions[0][0::2], - [[-1, 0, 0]]*49, decimal=4) - assert_almost_equal(ha.results.local_helix_directions[0][1::2], - [[1, 0, 0]]*49, decimal=4) + assert_almost_equal( + ha.results.local_helix_directions[0][0::2], + [[-1, 0, 0]] * 49, + decimal=4, + ) + assert_almost_equal( + ha.results.local_helix_directions[0][1::2], + [[1, 0, 0]] * 49, + decimal=4, + ) origins = zigzag.atoms.positions[1:-1].copy() origins[:, 0] = 0 assert_almost_equal(ha.results.local_origins[0], origins, decimal=4) - assert_almost_equal(ha.results.local_screw_angles[0], - screw_angles*49, decimal=4) + assert_almost_equal( + ha.results.local_screw_angles[0], screw_angles * 49, decimal=4 + ) def test_vector_of_best_fit(): line = np.random.rand(3) unit = line / np.linalg.norm(line) - points = line*np.arange(1000)[:, np.newaxis] + points = line * np.arange(1000)[:, np.newaxis] noise = np.random.normal(size=(1000, 3)) data = points + noise diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py index 6a4970edba1..97eeb6cc016 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py @@ -23,8 +23,10 @@ import pytest from MDAnalysisTests.datafiles import ( - TRZ, TRZ_psf, - waterPSF, waterDCD, + TRZ, + TRZ_psf, + waterPSF, + waterDCD, XYZ_mini, ) from numpy.testing import assert_almost_equal @@ -33,8 +35,10 @@ from importlib import reload import MDAnalysis as mda -from MDAnalysis.analysis.hydrogenbonds import (HydrogenBondAutoCorrel as HBAC, - find_hydrogen_donors) +from MDAnalysis.analysis.hydrogenbonds import ( + HydrogenBondAutoCorrel as HBAC, + find_hydrogen_donors, +) class TestHydrogenBondAutocorrel(object): @@ -44,118 +48,166 @@ def u(self): @pytest.fixture() def hydrogens(self, u): - return u.atoms.select_atoms('name Hn') + return u.atoms.select_atoms("name Hn") @pytest.fixture() def nitrogens(self, u): - return u.atoms.select_atoms('name N') + return u.atoms.select_atoms("name N") @pytest.fixture() def oxygens(self, u): - return u.atoms.select_atoms('name O') + return u.atoms.select_atoms("name O") # regression tests for different conditions def test_continuous(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.83137828, - 0.74486804, 0.67741936, 0.60263932], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.83137828, + 0.74486804, + 0.67741936, + 0.60263932, + ], + dtype=np.float32, + ), ) def test_continuous_excl(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - exclusions=(np.arange(len(hydrogens)), np.array( - range(len(oxygens)))), - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.83137828, - 0.74486804, 0.67741936, 0.60263932], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.83137828, + 0.74486804, + 0.67741936, + 0.60263932, + ], + dtype=np.float32, + ), ) - def test_intermittent(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.84310848, - 0.79325515, 0.76392961, 0.72287393], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.84310848, + 0.79325515, + 0.76392961, + 0.72287393, + ], + dtype=np.float32, + ), ) - def test_intermittent_timecut(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - time_cut=0.01, # time cut at traj.dt == continuous - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + time_cut=0.01, # time cut at traj.dt == continuous + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.83137828, - 0.74486804, 0.67741936, 0.60263932], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.83137828, + 0.74486804, + 0.67741936, + 0.60263932, + ], + dtype=np.float32, + ), ) def test_intermittent_excl(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - exclusions=(np.arange(len(hydrogens)), np.array( - range(len(oxygens)))), - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.84310848, - 0.79325515, 0.76392961, 0.72287393], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.84310848, + 0.79325515, + 0.76392961, + 0.72287393, + ], + dtype=np.float32, + ), ) # For `solve` the test trajectories aren't long enough # So spoof the results and check that solver finds solution def test_solve_continuous(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) def actual_function_cont(t): @@ -163,24 +215,26 @@ def actual_function_cont(t): A2 = 0.25 tau1 = 0.5 tau2 = 0.1 - return A1 * np.exp(-t/tau1) + A2 * np.exp(-t/tau2) - hbond.solution['time'] = time = np.arange(0, 0.06, 0.001) - hbond.solution['results'] = actual_function_cont(time) + return A1 * np.exp(-t / tau1) + A2 * np.exp(-t / tau2) + + hbond.solution["time"] = time = np.arange(0, 0.06, 0.001) + hbond.solution["results"] = actual_function_cont(time) hbond.solve() assert_almost_equal( - hbond.solution['fit'], + hbond.solution["fit"], np.array([0.75, 0.5, 0.1]), ) def test_solve_intermittent(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + sample_time=0.06, ) def actual_function_int(t): @@ -190,69 +244,86 @@ def actual_function_int(t): tau1 = 5 tau2 = 1 tau3 = 0.1 - return A1 * np.exp(-t/tau1) + A2 * np.exp(-t/tau2) + A3 * np.exp(-t/tau3) - hbond.solution['time'] = time = np.arange(0, 6.0, 0.01) - hbond.solution['results'] = actual_function_int(time) + return ( + A1 * np.exp(-t / tau1) + + A2 * np.exp(-t / tau2) + + A3 * np.exp(-t / tau3) + ) + + hbond.solution["time"] = time = np.arange(0, 6.0, 0.01) + hbond.solution["results"] = actual_function_int(time) hbond.solve() assert_almost_equal( - hbond.solution['fit'], + hbond.solution["fit"], np.array([0.33, 0.33, 5, 1, 0.1]), ) # setup errors def test_wronglength_DA(self, u, hydrogens, oxygens, nitrogens): with pytest.raises(ValueError): - HBAC(u, - hydrogens=hydrogens[:-1], - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - exclusions=(np.arange(len(hydrogens)), np.array( - range(len(oxygens)))), - sample_time=0.06, - ) + HBAC( + u, + hydrogens=hydrogens[:-1], + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, + ) def test_exclusions(self, u, hydrogens, oxygens, nitrogens): - excl_list = (np.array(range(len(hydrogens))), np.array( - range(len(oxygens)))) + excl_list = ( + np.array(range(len(hydrogens))), + np.array(range(len(oxygens))), + ) excl_list2 = excl_list[0], excl_list[1][:-1] with pytest.raises(ValueError): - HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - exclusions=excl_list2, - sample_time=0.06, - ) + HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + exclusions=excl_list2, + sample_time=0.06, + ) def test_bond_type_VE(self, u, hydrogens, oxygens, nitrogens): with pytest.raises(ValueError): - HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='marzipan', - exclusions=(np.arange(len(hydrogens)), np.array(range( - len(oxygens)))), - sample_time=0.06, - ) + HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="marzipan", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, + ) def test_solve_before_run_VE(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) with pytest.raises(ValueError): hbond.solve() - @mock.patch('MDAnalysis.coordinates.TRZ.TRZReader._read_frame') - def test_unslicable_traj_VE(self, mock_read, u, hydrogens, oxygens, nitrogens): + @mock.patch("MDAnalysis.coordinates.TRZ.TRZReader._read_frame") + def test_unslicable_traj_VE( + self, mock_read, u, hydrogens, oxygens, nitrogens + ): mock_read.side_effect = TypeError with pytest.raises(ValueError): @@ -261,17 +332,18 @@ def test_unslicable_traj_VE(self, mock_read, u, hydrogens, oxygens, nitrogens): hydrogens=hydrogens, acceptors=oxygens, donors=nitrogens, - bond_type='continuous', - sample_time=0.06 - ) + bond_type="continuous", + sample_time=0.06, + ) def test_repr(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) assert isinstance(repr(hbond), str) @@ -279,7 +351,7 @@ def test_repr(self, u, hydrogens, oxygens, nitrogens): def test_find_donors(): u = mda.Universe(waterPSF, waterDCD) - H = u.select_atoms('name H*') + H = u.select_atoms("name H*") D = find_hydrogen_donors(H) diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py index 7a372d53c54..e1822b62fa0 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel_deprecated.py @@ -23,8 +23,10 @@ import pytest from MDAnalysisTests.datafiles import ( - TRZ, TRZ_psf, - waterPSF, waterDCD, + TRZ, + TRZ_psf, + waterPSF, + waterDCD, XYZ_mini, ) from numpy.testing import assert_almost_equal @@ -36,10 +38,12 @@ from MDAnalysis.analysis import hbonds from MDAnalysis.analysis.hbonds import HydrogenBondAutoCorrel as HBAC + @pytest.fixture(scope="module") def u_water(): return mda.Universe(waterPSF, waterDCD) + class TestHydrogenBondAutocorrel(object): @pytest.fixture() def u(self): @@ -47,118 +51,166 @@ def u(self): @pytest.fixture() def hydrogens(self, u): - return u.atoms.select_atoms('name Hn') + return u.atoms.select_atoms("name Hn") @pytest.fixture() def nitrogens(self, u): - return u.atoms.select_atoms('name N') + return u.atoms.select_atoms("name N") @pytest.fixture() def oxygens(self, u): - return u.atoms.select_atoms('name O') + return u.atoms.select_atoms("name O") # regression tests for different conditions def test_continuous(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.83137828, - 0.74486804, 0.67741936, 0.60263932], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.83137828, + 0.74486804, + 0.67741936, + 0.60263932, + ], + dtype=np.float32, + ), ) def test_continuous_excl(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - exclusions=(np.arange(len(hydrogens)), np.array( - range(len(oxygens)))), - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.83137828, - 0.74486804, 0.67741936, 0.60263932], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.83137828, + 0.74486804, + 0.67741936, + 0.60263932, + ], + dtype=np.float32, + ), ) - def test_intermittent(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.84310848, - 0.79325515, 0.76392961, 0.72287393], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.84310848, + 0.79325515, + 0.76392961, + 0.72287393, + ], + dtype=np.float32, + ), ) - def test_intermittent_timecut(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - time_cut=0.01, # time cut at traj.dt == continuous - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + time_cut=0.01, # time cut at traj.dt == continuous + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.83137828, - 0.74486804, 0.67741936, 0.60263932], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.83137828, + 0.74486804, + 0.67741936, + 0.60263932, + ], + dtype=np.float32, + ), ) def test_intermittent_excl(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - exclusions=(np.arange(len(hydrogens)), np.array( - range(len(oxygens)))), - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, ) hbond.run() assert_almost_equal( - hbond.solution['results'], - np.array([ 1. , 0.92668623, 0.84310848, - 0.79325515, 0.76392961, 0.72287393], - dtype=np.float32) + hbond.solution["results"], + np.array( + [ + 1.0, + 0.92668623, + 0.84310848, + 0.79325515, + 0.76392961, + 0.72287393, + ], + dtype=np.float32, + ), ) # For `solve` the test trajectories aren't long enough # So spoof the results and check that solver finds solution def test_solve_continuous(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) def actual_function_cont(t): @@ -166,24 +218,26 @@ def actual_function_cont(t): A2 = 0.25 tau1 = 0.5 tau2 = 0.1 - return A1 * np.exp(-t/tau1) + A2 * np.exp(-t/tau2) - hbond.solution['time'] = time = np.arange(0, 0.06, 0.001) - hbond.solution['results'] = actual_function_cont(time) + return A1 * np.exp(-t / tau1) + A2 * np.exp(-t / tau2) + + hbond.solution["time"] = time = np.arange(0, 0.06, 0.001) + hbond.solution["results"] = actual_function_cont(time) hbond.solve() assert_almost_equal( - hbond.solution['fit'], + hbond.solution["fit"], np.array([0.75, 0.5, 0.1]), ) def test_solve_intermittent(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + sample_time=0.06, ) def actual_function_int(t): @@ -193,69 +247,86 @@ def actual_function_int(t): tau1 = 5 tau2 = 1 tau3 = 0.1 - return A1 * np.exp(-t/tau1) + A2 * np.exp(-t/tau2) + A3 * np.exp(-t/tau3) - hbond.solution['time'] = time = np.arange(0, 6.0, 0.01) - hbond.solution['results'] = actual_function_int(time) + return ( + A1 * np.exp(-t / tau1) + + A2 * np.exp(-t / tau2) + + A3 * np.exp(-t / tau3) + ) + + hbond.solution["time"] = time = np.arange(0, 6.0, 0.01) + hbond.solution["results"] = actual_function_int(time) hbond.solve() assert_almost_equal( - hbond.solution['fit'], + hbond.solution["fit"], np.array([0.33, 0.33, 5, 1, 0.1]), ) # setup errors def test_wronglength_DA(self, u, hydrogens, oxygens, nitrogens): with pytest.raises(ValueError): - HBAC(u, - hydrogens=hydrogens[:-1], - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - exclusions=(np.arange(len(hydrogens)), np.array( - range(len(oxygens)))), - sample_time=0.06, - ) + HBAC( + u, + hydrogens=hydrogens[:-1], + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, + ) def test_exclusions(self, u, hydrogens, oxygens, nitrogens): - excl_list = (np.array(range(len(hydrogens))), np.array( - range(len(oxygens)))) + excl_list = ( + np.array(range(len(hydrogens))), + np.array(range(len(oxygens))), + ) excl_list2 = excl_list[0], excl_list[1][:-1] with pytest.raises(ValueError): - HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='intermittent', - exclusions=excl_list2, - sample_time=0.06, - ) + HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="intermittent", + exclusions=excl_list2, + sample_time=0.06, + ) def test_bond_type_VE(self, u, hydrogens, oxygens, nitrogens): with pytest.raises(ValueError): - HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='marzipan', - exclusions=(np.arange(len(hydrogens)), np.array(range( - len(oxygens)))), - sample_time=0.06, - ) + HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="marzipan", + exclusions=( + np.arange(len(hydrogens)), + np.array(range(len(oxygens))), + ), + sample_time=0.06, + ) def test_solve_before_run_VE(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) with pytest.raises(ValueError): hbond.solve() - @mock.patch('MDAnalysis.coordinates.TRZ.TRZReader._read_frame') - def test_unslicable_traj_VE(self, mock_read, u, hydrogens, oxygens, nitrogens): + @mock.patch("MDAnalysis.coordinates.TRZ.TRZReader._read_frame") + def test_unslicable_traj_VE( + self, mock_read, u, hydrogens, oxygens, nitrogens + ): mock_read.side_effect = TypeError with pytest.raises(ValueError): @@ -264,38 +335,40 @@ def test_unslicable_traj_VE(self, mock_read, u, hydrogens, oxygens, nitrogens): hydrogens=hydrogens, acceptors=oxygens, donors=nitrogens, - bond_type='continuous', - sample_time=0.06 - ) + bond_type="continuous", + sample_time=0.06, + ) def test_repr(self, u, hydrogens, oxygens, nitrogens): - hbond = HBAC(u, - hydrogens=hydrogens, - acceptors=oxygens, - donors=nitrogens, - bond_type='continuous', - sample_time=0.06, + hbond = HBAC( + u, + hydrogens=hydrogens, + acceptors=oxygens, + donors=nitrogens, + bond_type="continuous", + sample_time=0.06, ) assert isinstance(repr(hbond), str) def test_deprecation_warning(self, u, hydrogens, oxygens, nitrogens): - wmsg = ('`HydrogenBondAutoCorrel` is deprecated!\n' - '`HydrogenBondAutoCorrel` will be removed in release 3.0.0.\n' - 'The class was moved to MDAnalysis.analysis.hbonds.hbond_autocorrel.') + wmsg = ( + "`HydrogenBondAutoCorrel` is deprecated!\n" + "`HydrogenBondAutoCorrel` will be removed in release 3.0.0.\n" + "The class was moved to MDAnalysis.analysis.hbonds.hbond_autocorrel." + ) with pytest.warns(DeprecationWarning, match=wmsg): HBAC( u, hydrogens=hydrogens, acceptors=oxygens, donors=nitrogens, - bond_type='continuous', - sample_time=0.06 + bond_type="continuous", + sample_time=0.06, ) - def test_find_donors(u_water): - H = u_water.select_atoms('name H*') + H = u_water.select_atoms("name H*") D = hbonds.find_hydrogen_donors(H) @@ -313,19 +386,21 @@ def test_donors_nobonds(): def test_find_hydrogen_donors_deprecation_warning(u_water): - H = u_water.select_atoms('name H*') - wmsg = ('`find_hydrogen_donors` is deprecated!\n' - '`find_hydrogen_donors` will be removed in release 3.0.0.\n' - 'The function was moved to MDAnalysis.analysis.hbonds.hbond_autocorrel.') + H = u_water.select_atoms("name H*") + wmsg = ( + "`find_hydrogen_donors` is deprecated!\n" + "`find_hydrogen_donors` will be removed in release 3.0.0.\n" + "The function was moved to MDAnalysis.analysis.hbonds.hbond_autocorrel." + ) with pytest.warns(DeprecationWarning, match=wmsg): hbonds.find_hydrogen_donors(H) def test_moved_module_warning(): - wmsg = ("This module was moved to " - "MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel; " - "hbonds.hbond_autocorrel will be removed in 3.0.0.") + wmsg = ( + "This module was moved to " + "MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel; " + "hbonds.hbond_autocorrel will be removed in 3.0.0." + ) with pytest.warns(DeprecationWarning, match=wmsg): reload(hbonds.hbond_autocorrel) - - diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py index bef6b03331d..a731dd6ec93 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis.py @@ -25,13 +25,18 @@ import pytest import copy -from numpy.testing import (assert_allclose, assert_equal, - assert_array_almost_equal, assert_array_equal, - assert_almost_equal) +from numpy.testing import ( + assert_allclose, + assert_equal, + assert_array_almost_equal, + assert_array_equal, + assert_almost_equal, +) import MDAnalysis from MDAnalysis.analysis.hydrogenbonds.hbond_analysis import ( - HydrogenBondAnalysis) + HydrogenBondAnalysis, +) from MDAnalysis.exceptions import NoDataError from MDAnalysisTests.datafiles import waterPSF, waterDCD @@ -39,20 +44,20 @@ class TestHydrogenBondAnalysisTIP3P(object): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return MDAnalysis.Universe(waterPSF, waterDCD) kwargs = { - 'donors_sel': 'name OH2', - 'hydrogens_sel': 'name H1 H2', - 'acceptors_sel': 'name OH2', - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": "name OH2", + "hydrogens_sel": "name H1 H2", + "acceptors_sel": "name OH2", + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def h(self, universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis(universe, **self.kwargs) h.run(**client_HydrogenBondAnalysis) @@ -64,18 +69,22 @@ def test_hbond_analysis(self, h): assert len(h.results.hbonds) == 32 reference = { - 'distance': {'mean': 2.7627309, 'std': 0.0905052}, - 'angle': {'mean': 158.9038039, 'std': 12.0362826}, + "distance": {"mean": 2.7627309, "std": 0.0905052}, + "angle": {"mean": 158.9038039, "std": 12.0362826}, } - assert_allclose(np.mean(h.results.hbonds[:, 4]), - reference['distance']['mean']) - assert_allclose(np.std(h.results.hbonds[:, 4]), - reference['distance']['std']) - assert_allclose(np.mean(h.results.hbonds[:, 5]), - reference['angle']['mean']) - assert_allclose(np.std(h.results.hbonds[:, 5]), - reference['angle']['std']) + assert_allclose( + np.mean(h.results.hbonds[:, 4]), reference["distance"]["mean"] + ) + assert_allclose( + np.std(h.results.hbonds[:, 4]), reference["distance"]["std"] + ) + assert_allclose( + np.mean(h.results.hbonds[:, 5]), reference["angle"]["mean"] + ) + assert_allclose( + np.std(h.results.hbonds[:, 5]), reference["angle"]["std"] + ) def test_count_by_time(self, h): @@ -100,7 +109,7 @@ def test_count_by_ids(self, h, universe): unique_hbonds = h.count_by_ids() most_common_hbond_ids = [12, 14, 9] - assert_equal(unique_hbonds[0,:3], most_common_hbond_ids) + assert_equal(unique_hbonds[0, :3], most_common_hbond_ids) # count_by_ids() returns raw counts # convert to fraction of time that bond was observed @@ -117,16 +126,16 @@ def test_hbonds_deprecated_attr(self, h): class TestHydrogenBondAnalysisIdeal(object): kwargs = { - 'donors_sel': 'name O', - 'hydrogens_sel': 'name H1 H2', - 'acceptors_sel': 'name O', - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": "name O", + "hydrogens_sel": "name H1 H2", + "acceptors_sel": "name O", + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): # create two water molecules """ @@ -138,53 +147,56 @@ def universe(): """ n_residues = 2 u = MDAnalysis.Universe.empty( - n_atoms=n_residues*3, + n_atoms=n_residues * 3, n_residues=n_residues, atom_resindex=np.repeat(range(n_residues), 3), residue_segindex=[0] * n_residues, trajectory=True, # necessary for adding coordinates - ) + ) - u.add_TopologyAttr('name', ['O', 'H1', 'H2'] * n_residues) - u.add_TopologyAttr('type', ['O', 'H', 'H'] * n_residues) - u.add_TopologyAttr('resname', ['SOL'] * n_residues) - u.add_TopologyAttr('resid', list(range(1, n_residues + 1))) - u.add_TopologyAttr('id', list(range(1, (n_residues * 3) + 1))) + u.add_TopologyAttr("name", ["O", "H1", "H2"] * n_residues) + u.add_TopologyAttr("type", ["O", "H", "H"] * n_residues) + u.add_TopologyAttr("resname", ["SOL"] * n_residues) + u.add_TopologyAttr("resid", list(range(1, n_residues + 1))) + u.add_TopologyAttr("id", list(range(1, (n_residues * 3) + 1))) # Atomic coordinates with a single hydrogen bond between O1-H2---O2 - pos1 = np.array([[0, 0, 0], # O1 - [-0.249, -0.968, 0], # H1 - [1, 0, 0], # H2 - [2.5, 0, 0], # O2 - [3., 0, 0], # H3 - [2.250, 0.968, 0] # H4 - ]) + pos1 = np.array( + [ + [0, 0, 0], # O1 + [-0.249, -0.968, 0], # H1 + [1, 0, 0], # H2 + [2.5, 0, 0], # O2 + [3.0, 0, 0], # H3 + [2.250, 0.968, 0], # H4 + ] + ) # Atomic coordinates with no hydrogen bonds - pos2 = np.array([[0, 0, 0], # O1 - [-0.249, -0.968, 0], # H1 - [1, 0, 0], # H2 - [4.5, 0, 0], # O2 - [5., 0, 0], # H3 - [4.250, 0.968, 0] # H4 - ]) - - coordinates = np.empty((3, # number of frames - u.atoms.n_atoms, - 3)) + pos2 = np.array( + [ + [0, 0, 0], # O1 + [-0.249, -0.968, 0], # H1 + [1, 0, 0], # H2 + [4.5, 0, 0], # O2 + [5.0, 0, 0], # H3 + [4.250, 0.968, 0], # H4 + ] + ) + + coordinates = np.empty((3, u.atoms.n_atoms, 3)) # number of frames coordinates[0] = pos1 coordinates[1] = pos2 coordinates[2] = pos1 - u.load_new(coordinates, order='fac') + u.load_new(coordinates, order="fac") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def hydrogen_bonds(universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis( - universe, - **TestHydrogenBondAnalysisIdeal.kwargs + universe, **TestHydrogenBondAnalysisIdeal.kwargs ) h.run(**client_HydrogenBondAnalysis) return h @@ -200,42 +212,48 @@ def test_count_by_type(self, hydrogen_bonds): def test_no_bond_info_exception(self, universe): kwargs = { - 'donors_sel': None, - 'hydrogens_sel': None, - 'acceptors_sel': None, - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": None, + "hydrogens_sel": None, + "acceptors_sel": None, + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "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) + 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(u, **kwargs) def test_no_bond_donor_sel(self, universe): kwargs = { - 'donors_sel': "type O", - 'hydrogens_sel': None, - 'acceptors_sel': None, - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": "type O", + "hydrogens_sel": None, + "acceptors_sel": None, + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "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) + u.add_TopologyAttr("mass", [15.999, 1.008, 1.008] * n_residues) + u.add_TopologyAttr("charge", [-1.04, 0.52, 0.52] * n_residues) h = HydrogenBondAnalysis(u, **kwargs) donors = u.select_atoms(h.guess_donors()) def test_first_hbond(self, hydrogen_bonds): assert len(hydrogen_bonds.results.hbonds) == 2 - frame_no, donor_index, hydrogen_index, acceptor_index, da_dst, angle =\ - hydrogen_bonds.results.hbonds[0] + ( + frame_no, + donor_index, + hydrogen_index, + acceptor_index, + da_dst, + angle, + ) = hydrogen_bonds.results.hbonds[0] assert_equal(donor_index, 0) assert_equal(hydrogen_index, 2) assert_equal(acceptor_index, 3) @@ -243,7 +261,7 @@ def test_first_hbond(self, hydrogen_bonds): assert_almost_equal(angle, 180) def test_count_by_time(self, hydrogen_bonds): - ref_times = np.array([0, 1, 2]) # u.trajectory.dt is 1 + ref_times = np.array([0, 1, 2]) # u.trajectory.dt is 1 ref_counts = np.array([1, 0, 1]) counts = hydrogen_bonds.count_by_time() @@ -266,8 +284,9 @@ 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, - client_HydrogenBondAnalysis): + def test_logging_step_not_1( + self, universe, caplog, client_HydrogenBondAnalysis + ): hbonds = HydrogenBondAnalysis(universe, **self.kwargs) # using step 2 hbonds.run(**client_HydrogenBondAnalysis, step=2) @@ -275,24 +294,25 @@ def test_logging_step_not_1(self, universe, caplog, caplog.set_level(logging.WARNING) hbonds.lifetime(tau_max=2, intermittency=1) - warning = ("Autocorrelation: Hydrogen bonds were computed with " - "step > 1.") + warning = ( + "Autocorrelation: Hydrogen bonds were computed with " "step > 1." + ) assert any(warning in rec.getMessage() for rec in caplog.records) class TestHydrogenBondAnalysisNoRes(TestHydrogenBondAnalysisIdeal): kwargs = { - 'donors_sel': 'type O', -# 'hydrogens_sel': 'type H H', - 'acceptors_sel': 'type O', - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": "type O", + # 'hydrogens_sel': 'type H H', + "acceptors_sel": "type O", + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } @staticmethod - @pytest.fixture(scope='class', autouse=True) + @pytest.fixture(scope="class", autouse=True) def universe(): # create two water molecules """ @@ -304,52 +324,55 @@ def universe(): """ n_residues = 2 u = MDAnalysis.Universe.empty( - n_atoms=n_residues*3, + n_atoms=n_residues * 3, n_residues=n_residues, atom_resindex=np.repeat(range(n_residues), 3), residue_segindex=[0] * n_residues, trajectory=True, # necessary for adding coordinates - ) + ) - u.add_TopologyAttr('type', ['O', 'H', 'H'] * n_residues) - u.add_TopologyAttr('id', list(range(1, (n_residues * 3) + 1))) - u.add_TopologyAttr('mass', [15.999, 1.008, 1.008] * n_residues) - u.add_TopologyAttr('charge', [-1.04, 0.52, 0.52] * n_residues) + u.add_TopologyAttr("type", ["O", "H", "H"] * n_residues) + u.add_TopologyAttr("id", list(range(1, (n_residues * 3) + 1))) + u.add_TopologyAttr("mass", [15.999, 1.008, 1.008] * n_residues) + u.add_TopologyAttr("charge", [-1.04, 0.52, 0.52] * n_residues) # Atomic coordinates with a single hydrogen bond between O1-H2---O2 - pos1 = np.array([[0, 0, 0], # O1 - [-0.249, -0.968, 0], # H1 - [1, 0, 0], # H2 - [2.5, 0, 0], # O2 - [3., 0, 0], # H3 - [2.250, 0.968, 0] # H4 - ]) + pos1 = np.array( + [ + [0, 0, 0], # O1 + [-0.249, -0.968, 0], # H1 + [1, 0, 0], # H2 + [2.5, 0, 0], # O2 + [3.0, 0, 0], # H3 + [2.250, 0.968, 0], # H4 + ] + ) # Atomic coordinates with no hydrogen bonds - pos2 = np.array([[0, 0, 0], # O1 - [-0.249, -0.968, 0], # H1 - [1, 0, 0], # H2 - [4.5, 0, 0], # O2 - [5., 0, 0], # H3 - [4.250, 0.968, 0] # H4 - ]) - - coordinates = np.empty((3, # number of frames - u.atoms.n_atoms, - 3)) + pos2 = np.array( + [ + [0, 0, 0], # O1 + [-0.249, -0.968, 0], # H1 + [1, 0, 0], # H2 + [4.5, 0, 0], # O2 + [5.0, 0, 0], # H3 + [4.250, 0.968, 0], # H4 + ] + ) + + coordinates = np.empty((3, u.atoms.n_atoms, 3)) # number of frames coordinates[0] = pos1 coordinates[1] = pos2 coordinates[2] = pos1 - u.load_new(coordinates, order='fac') + u.load_new(coordinates, order="fac") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def hydrogen_bonds(universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis( - universe, - **TestHydrogenBondAnalysisNoRes.kwargs + universe, **TestHydrogenBondAnalysisNoRes.kwargs ) h.run(**client_HydrogenBondAnalysis) return h @@ -359,26 +382,30 @@ def test_no_hydrogen_bonds(self, universe): tmp_kwargs["d_h_a_angle_cutoff"] = 50 hbonds = HydrogenBondAnalysis(universe, **tmp_kwargs) - with pytest.warns(UserWarning, - match=("No hydrogen bonds were found given angle " - "of 50 between Donor, type O, and Acceptor," - " type O.")): + with pytest.warns( + UserWarning, + match=( + "No hydrogen bonds were found given angle " + "of 50 between Donor, type O, and Acceptor," + " type O." + ), + ): hbonds.run(step=1) class TestHydrogenBondAnalysisBetween(object): kwargs = { - 'donors_sel': 'name O P', - 'hydrogens_sel': 'name H1 H2 PH', - 'acceptors_sel': 'name O P', - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": "name O P", + "hydrogens_sel": "name H1 H2 PH", + "acceptors_sel": "name O P", + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): # create two water molecules and two "protein" molecules # P1-PH1 are the two atoms that comprise the toy protein PROT1 @@ -393,55 +420,52 @@ def universe(): n_sol_residues = 2 n_prot_residues = 2 u = MDAnalysis.Universe.empty( - n_atoms=n_sol_residues*3 + n_prot_residues*2, + n_atoms=n_sol_residues * 3 + n_prot_residues * 2, n_residues=n_residues, atom_resindex=[0, 0, 0, 1, 1, 1, 2, 2, 3, 3], residue_segindex=[0, 0, 1, 1], trajectory=True, # necessary for adding coordinates - ) - - u.add_TopologyAttr( - 'name', - ['O', 'H1', 'H2'] * n_sol_residues + ['P', 'PH'] * n_prot_residues ) + u.add_TopologyAttr( - 'type', - ['O', 'H', 'H'] * n_sol_residues + ['P', 'PH'] * n_prot_residues + "name", + ["O", "H1", "H2"] * n_sol_residues + ["P", "PH"] * n_prot_residues, ) u.add_TopologyAttr( - 'resname', - ['SOL'] * n_sol_residues + ['PROT'] * n_prot_residues + "type", + ["O", "H", "H"] * n_sol_residues + ["P", "PH"] * n_prot_residues, ) u.add_TopologyAttr( - 'resid', - list(range(1, n_residues + 1)) + "resname", ["SOL"] * n_sol_residues + ["PROT"] * n_prot_residues ) + u.add_TopologyAttr("resid", list(range(1, n_residues + 1))) u.add_TopologyAttr( - 'id', - list(range(1, (n_sol_residues * 3 + n_prot_residues * 2) + 1)) + "id", + list(range(1, (n_sol_residues * 3 + n_prot_residues * 2) + 1)), ) # Atomic coordinates with hydrogen bonds between: # O1-H2---O2 # O2-H3---P1 # P1-PH1---P2 - pos = np.array([[0, 0, 0], # O1 - [-0.249, -0.968, 0], # H1 - [1, 0, 0], # H2 - [2.5, 0, 0], # O2 - [3., 0, 0], # H3 - [2.250, 0.968, 0], # H4 - [5.5, 0, 0], # P1 - [6.5, 0, 0], # PH1 - [8.5, 0, 0], # P2 - [9.5, 0, 0], # PH2 - ]) - - coordinates = np.empty((1, # number of frames - u.atoms.n_atoms, - 3)) + pos = np.array( + [ + [0, 0, 0], # O1 + [-0.249, -0.968, 0], # H1 + [1, 0, 0], # H2 + [2.5, 0, 0], # O2 + [3.0, 0, 0], # H3 + [2.250, 0.968, 0], # H4 + [5.5, 0, 0], # P1 + [6.5, 0, 0], # PH1 + [8.5, 0, 0], # P2 + [9.5, 0, 0], # PH2 + ] + ) + + coordinates = np.empty((1, u.atoms.n_atoms, 3)) # number of frames coordinates[0] = pos - u.load_new(coordinates, order='fac') + u.load_new(coordinates, order="fac") return u @@ -454,29 +478,27 @@ def test_between_all(self, universe, client_HydrogenBondAnalysis): expected_hbond_indices = [ [0, 2, 3], # water-water [3, 4, 6], # protein-water - [6, 7, 8] # protein-protein + [6, 7, 8], # protein-protein ] expected_hbond_distances = [2.5, 3.0, 3.0] - assert_array_equal(hbonds.results.hbonds[:, 1:4], - expected_hbond_indices) + assert_array_equal( + hbonds.results.hbonds[:, 1:4], expected_hbond_indices + ) assert_allclose(hbonds.results.hbonds[:, 4], expected_hbond_distances) def test_between_PW(self, universe, client_HydrogenBondAnalysis): # Find only protein-water hydrogen bonds hbonds = HydrogenBondAnalysis( - universe, - between=["resname PROT", "resname SOL"], - **self.kwargs + universe, between=["resname PROT", "resname SOL"], **self.kwargs ) hbonds.run(**client_HydrogenBondAnalysis) # indices of [donor, hydrogen, acceptor] for each hydrogen bond - expected_hbond_indices = [ - [3, 4, 6] # protein-water - ] + expected_hbond_indices = [[3, 4, 6]] # protein-water expected_hbond_distances = [3.0] - assert_array_equal(hbonds.results.hbonds[:, 1:4], - expected_hbond_indices) + assert_array_equal( + hbonds.results.hbonds[:, 1:4], expected_hbond_indices + ) assert_allclose(hbonds.results.hbonds[:, 4], expected_hbond_distances) def test_between_PW_PP(self, universe, client_HydrogenBondAnalysis): @@ -486,42 +508,46 @@ def test_between_PW_PP(self, universe, client_HydrogenBondAnalysis): universe, between=[ ["resname PROT", "resname SOL"], - ["resname PROT", "resname PROT"] + ["resname PROT", "resname PROT"], ], - **self.kwargs + **self.kwargs, ) hbonds.run(**client_HydrogenBondAnalysis) # indices of [donor, hydrogen, acceptor] for each hydrogen bond expected_hbond_indices = [ [3, 4, 6], # protein-water - [6, 7, 8] # protein-protein + [6, 7, 8], # protein-protein ] expected_hbond_distances = [3.0, 3.0] - assert_array_equal(hbonds.results.hbonds[:, 1:4], - expected_hbond_indices) + assert_array_equal( + hbonds.results.hbonds[:, 1:4], expected_hbond_indices + ) assert_allclose(hbonds.results.hbonds[:, 4], expected_hbond_distances) -class TestHydrogenBondAnalysisTIP3P_GuessAcceptors_GuessHydrogens_UseTopology_(TestHydrogenBondAnalysisTIP3P): +class TestHydrogenBondAnalysisTIP3P_GuessAcceptors_GuessHydrogens_UseTopology_( + TestHydrogenBondAnalysisTIP3P +): """Uses the same distance and cutoff hydrogen bond criteria as :class:`TestHydrogenBondAnalysisTIP3P`, so the results are identical, but the hydrogens and acceptors are guessed whilst the donor-hydrogen pairs are determined via the topology. """ + kwargs = { - 'donors_sel': None, - 'hydrogens_sel': None, - 'acceptors_sel': None, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": None, + "hydrogens_sel": None, + "acceptors_sel": None, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } 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() - test_kwargs['donors_sel'] = None # use topology to find pairs - test_kwargs['hydrogens_sel'] = "name H" # no atoms have name H + test_kwargs["donors_sel"] = None # use topology to find pairs + test_kwargs["hydrogens_sel"] = "name H" # no atoms have name H h = HydrogenBondAnalysis(universe, **test_kwargs) h.run(**client_HydrogenBondAnalysis) @@ -532,24 +558,23 @@ def test_no_hydrogens(self, universe, client_HydrogenBondAnalysis): class TestHydrogenBondAnalysisTIP3P_GuessDonors_NoTopology(object): - """Guess the donor atoms involved in hydrogen bonds using the partial charges of the atoms. - """ + """Guess the donor atoms involved in hydrogen bonds using the partial charges of the atoms.""" @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return MDAnalysis.Universe(waterPSF, waterDCD) kwargs = { - 'donors_sel': None, - 'hydrogens_sel': None, - 'acceptors_sel': None, - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": None, + "hydrogens_sel": None, + "acceptors_sel": None, + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def h(self, universe): h = HydrogenBondAnalysis(universe, **self.kwargs) return h @@ -557,7 +582,7 @@ def h(self, universe): def test_guess_donors(self, h): ref_donors = "(resname TIP3 and name OH2)" - donors = h.guess_donors(select='all', max_charge=-0.5) + donors = h.guess_donors(select="all", max_charge=-0.5) assert donors == ref_donors @@ -568,41 +593,40 @@ class TestHydrogenBondAnalysisTIP3P_GuessHydrogens_NoTopology(object): """ @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return MDAnalysis.Universe(waterPSF, waterDCD) kwargs = { - 'donors_sel': None, - 'hydrogens_sel': None, - 'acceptors_sel': None, - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": None, + "hydrogens_sel": None, + "acceptors_sel": None, + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def h(self, universe): h = HydrogenBondAnalysis(universe, **self.kwargs) return h def test_guess_hydrogens(self, h): - ref_hydrogens = "(resname TIP3 and name H1) or (resname TIP3 and name H2)" - hydrogens = h.guess_hydrogens(select='all') + ref_hydrogens = ( + "(resname TIP3 and name H1) or (resname TIP3 and name H2)" + ) + hydrogens = h.guess_hydrogens(select="all") assert hydrogens == ref_hydrogens pytest.mark.parametrize( "min_mass, max_mass, min_charge", - [ - (1.05, 1.10, 0.30), - (0.90, 0.95, 0.30), - (0.90, 1.10, 1.00) - ] + [(1.05, 1.10, 0.30), (0.90, 0.95, 0.30), (0.90, 1.10, 1.00)], ) + def test_guess_hydrogens_empty_selection(self, h): - hydrogens = h.guess_hydrogens(select='all', min_charge=1.0) + hydrogens = h.guess_hydrogens(select="all", min_charge=1.0) assert hydrogens == "" def test_guess_hydrogens_min_max_mass(self, h): @@ -611,7 +635,8 @@ def test_guess_hydrogens_min_max_mass(self, h): with pytest.raises(ValueError, match=errmsg): - h.guess_hydrogens(select='all', min_mass=1.1, max_mass=0.9) + h.guess_hydrogens(select="all", min_mass=1.1, max_mass=0.9) + class TestHydrogenBondAnalysisTIP3PStartStep(object): """Uses the same distance and cutoff hydrogen bond criteria as :class:`TestHydrogenBondAnalysisTIP3P` but starting @@ -619,20 +644,20 @@ class TestHydrogenBondAnalysisTIP3PStartStep(object): """ @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return MDAnalysis.Universe(waterPSF, waterDCD) kwargs = { - 'donors_sel': 'name OH2', - 'hydrogens_sel': 'name H1 H2', - 'acceptors_sel': 'name OH2', - 'd_h_cutoff': 1.2, - 'd_a_cutoff': 3.0, - 'd_h_a_angle_cutoff': 120.0 + "donors_sel": "name OH2", + "hydrogens_sel": "name H1 H2", + "acceptors_sel": "name OH2", + "d_h_cutoff": 1.2, + "d_a_cutoff": 3.0, + "d_h_a_angle_cutoff": 120.0, } - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def h(self, universe, client_HydrogenBondAnalysis): h = HydrogenBondAnalysis(universe, **self.kwargs) h.run(**client_HydrogenBondAnalysis, start=1, step=2) @@ -644,22 +669,34 @@ def test_hbond_analysis(self, h): assert len(h.results.hbonds) == 15 reference = { - 'distance': {'mean': 2.73942464, 'std': 0.05867924}, - 'angle': {'mean': 157.07768079, 'std': 9.72636682}, + "distance": {"mean": 2.73942464, "std": 0.05867924}, + "angle": {"mean": 157.07768079, "std": 9.72636682}, } - assert_allclose(np.mean(h.results.hbonds[:, 4]), - reference['distance']['mean']) - assert_allclose(np.std(h.results.hbonds[:, 4]), - reference['distance']['std']) - assert_allclose(np.mean(h.results.hbonds[:, 5]), - reference['angle']['mean']) - assert_allclose(np.std(h.results.hbonds[:, 5]), - reference['angle']['std']) + assert_allclose( + np.mean(h.results.hbonds[:, 4]), reference["distance"]["mean"] + ) + assert_allclose( + np.std(h.results.hbonds[:, 4]), reference["distance"]["std"] + ) + assert_allclose( + np.mean(h.results.hbonds[:, 5]), reference["angle"]["mean"] + ) + assert_allclose( + np.std(h.results.hbonds[:, 5]), reference["angle"]["std"] + ) def test_count_by_time(self, h): - ref_times = np.array([0.04, 0.08, 0.12, 0.16, 0.20, ]) + ref_times = np.array( + [ + 0.04, + 0.08, + 0.12, + 0.16, + 0.20, + ] + ) ref_counts = np.array([2, 4, 4, 2, 3]) counts = h.count_by_time() @@ -678,29 +715,32 @@ def test_count_by_type(self, h): class TestHydrogenBondAnalysisEmptySelections: @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return MDAnalysis.Universe(waterPSF, waterDCD) - msg = ("{} is an empty selection string - no hydrogen bonds will " - "be found. This may be intended, but please check your " - "selection." - ) + msg = ( + "{} is an empty selection string - no hydrogen bonds will " + "be found. This may be intended, but please check your " + "selection." + ) - @pytest.mark.parametrize('seltype', - ['donors_sel', 'hydrogens_sel', 'acceptors_sel']) + @pytest.mark.parametrize( + "seltype", ["donors_sel", "hydrogens_sel", "acceptors_sel"] + ) def test_empty_sel(self, universe, seltype): - sel_kwarg = {seltype: ' '} + sel_kwarg = {seltype: " "} with pytest.warns(UserWarning, match=self.msg.format(seltype)): HydrogenBondAnalysis(universe, **sel_kwarg) def test_hbond_analysis(self, universe, client_HydrogenBondAnalysis): - h = HydrogenBondAnalysis(universe, donors_sel=' ', hydrogens_sel=' ', - acceptors_sel=' ') + h = HydrogenBondAnalysis( + universe, donors_sel=" ", hydrogens_sel=" ", acceptors_sel=" " + ) h.run(**client_HydrogenBondAnalysis) - assert h.donors_sel == '' - assert h.hydrogens_sel == '' - assert h.acceptors_sel == '' + assert h.donors_sel == "" + assert h.hydrogens_sel == "" + assert h.acceptors_sel == "" assert h.results.hbonds.size == 0 diff --git a/testsuite/MDAnalysisTests/analysis/test_leaflet.py b/testsuite/MDAnalysisTests/analysis/test_leaflet.py index 0c4839f36b5..e968bbf8c9c 100644 --- a/testsuite/MDAnalysisTests/analysis/test_leaflet.py +++ b/testsuite/MDAnalysisTests/analysis/test_leaflet.py @@ -43,14 +43,14 @@ def lipid_heads(universe): return universe.select_atoms(LIPID_HEAD_STRING) -@pytest.mark.skipif(HAS_NX, reason='networkx is installed') +@pytest.mark.skipif(HAS_NX, reason="networkx is installed") def test_optional_nx(): errmsg = "The LeafletFinder class requires an installation of networkx" with pytest.raises(ImportError, match=errmsg): _ = LeafletFinder(universe, lipid_heads, pbc=True) -@pytest.mark.skipif(not HAS_NX, reason='needs networkx') +@pytest.mark.skipif(not HAS_NX, reason="needs networkx") class TestLeafletFinder: @staticmethod def lines2one(lines): @@ -61,13 +61,22 @@ def test_leaflet_finder(self, universe, lipid_heads): lfls = LeafletFinder(universe, lipid_heads, pbc=True) top_heads, bottom_heads = lfls.groups() # Make top be... on top. - if top_heads.center_of_geometry()[2] < bottom_heads.center_of_geometry()[2]: - top_heads,bottom_heads = (bottom_heads,top_heads) - assert_equal(top_heads.indices, np.arange(1,2150,12), - err_msg="Found wrong leaflet lipids") - assert_equal(bottom_heads.indices, np.arange(2521,4670,12), - err_msg="Found wrong leaflet lipids") - + if ( + top_heads.center_of_geometry()[2] + < bottom_heads.center_of_geometry()[2] + ): + top_heads, bottom_heads = (bottom_heads, top_heads) + assert_equal( + top_heads.indices, + np.arange(1, 2150, 12), + err_msg="Found wrong leaflet lipids", + ) + assert_equal( + bottom_heads.indices, + np.arange(2521, 4670, 12), + err_msg="Found wrong leaflet lipids", + ) + def test_string_vs_atomgroup_proper(self, universe, lipid_heads): lfls_ag = LeafletFinder(universe, lipid_heads, pbc=True) lfls_string = LeafletFinder(universe, LIPID_HEAD_STRING, pbc=True) @@ -75,52 +84,58 @@ def test_string_vs_atomgroup_proper(self, universe, lipid_heads): groups_string = lfls_string.groups() assert_equal(groups_string[0].indices, groups_ag[0].indices) assert_equal(groups_string[1].indices, groups_ag[1].indices) - + def test_optimize_cutoff(self, universe, lipid_heads): cutoff, N = optimize_cutoff(universe, lipid_heads, pbc=True) assert N == 2 assert_almost_equal(cutoff, 10.5, decimal=4) - + def test_pbc_on_off(self, universe, lipid_heads): lfls_pbc_on = LeafletFinder(universe, lipid_heads, pbc=True) lfls_pbc_off = LeafletFinder(universe, lipid_heads, pbc=False) assert lfls_pbc_on.graph.size() > lfls_pbc_off.graph.size() - + def test_pbc_on_off_difference(self, universe, lipid_heads): import networkx lfls_pbc_on = LeafletFinder(universe, lipid_heads, cutoff=7, pbc=True) - lfls_pbc_off = LeafletFinder(universe, lipid_heads, cutoff=7, pbc=False) + lfls_pbc_off = LeafletFinder( + universe, lipid_heads, cutoff=7, pbc=False + ) pbc_on_graph = lfls_pbc_on.graph pbc_off_graph = lfls_pbc_off.graph diff_graph = networkx.difference(pbc_on_graph, pbc_off_graph) - assert_equal(set(diff_graph.edges), {(69, 153), (73, 79), - (206, 317), (313, 319)}) - + assert_equal( + set(diff_graph.edges), + {(69, 153), (73, 79), (206, 317), (313, 319)}, + ) + @pytest.mark.parametrize("sparse", [True, False, None]) def test_sparse_on_off_none(self, universe, lipid_heads, sparse): - lfls_ag = LeafletFinder(universe, lipid_heads, cutoff=15.0, pbc=True, - sparse=sparse) + lfls_ag = LeafletFinder( + universe, lipid_heads, cutoff=15.0, pbc=True, sparse=sparse + ) assert_almost_equal(len(lfls_ag.graph.edges), 1903, decimal=4) - + def test_cutoff_update(self, universe, lipid_heads): lfls_ag = LeafletFinder(universe, lipid_heads, cutoff=15.0, pbc=True) lfls_ag.update(cutoff=1.0) assert_almost_equal(lfls_ag.cutoff, 1.0, decimal=4) assert_almost_equal(len(lfls_ag.groups()), 360, decimal=4) - + def test_cutoff_update_default(self, universe, lipid_heads): lfls_ag = LeafletFinder(universe, lipid_heads, cutoff=15.0, pbc=True) lfls_ag.update() assert_almost_equal(lfls_ag.cutoff, 15.0, decimal=4) assert_almost_equal(len(lfls_ag.groups()), 2, decimal=4) - + def test_write_selection(self, universe, lipid_heads, tmpdir): lfls_ag = LeafletFinder(universe, lipid_heads, cutoff=15.0, pbc=True) with tmpdir.as_cwd(): - filename = lfls_ag.write_selection('leaflet.vmd') - expected_output = self.lines2one([ - """# leaflets based on select= cutoff=15.000000 + filename = lfls_ag.write_selection("leaflet.vmd") + expected_output = self.lines2one( + [ + """# leaflets based on select= cutoff=15.000000 # MDAnalysis VMD selection atomselect macro leaflet_1 {index 1 13 25 37 49 61 73 85 \\ 97 109 121 133 145 157 169 181 \\ @@ -170,10 +185,17 @@ def test_write_selection(self, universe, lipid_heads, tmpdir): 4537 4549 4561 4573 4585 4597 4609 4621 \\ 4633 4645 4657 4669 } - """]) - - assert self.lines2one(open('leaflet.vmd').readlines()) == expected_output - + """ + ] + ) + + assert ( + self.lines2one(open("leaflet.vmd").readlines()) + == expected_output + ) + def test_component_index_is_not_none(self, universe, lipid_heads): lfls_ag = LeafletFinder(universe, lipid_heads, cutoff=15.0, pbc=True) - assert_almost_equal(len(lfls_ag.groups(component_index=0)), 180, decimal=4) + assert_almost_equal( + len(lfls_ag.groups(component_index=0)), 180, decimal=4 + ) diff --git a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py index 2b6ce161cb6..9f5963938bb 100644 --- a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py +++ b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py @@ -35,7 +35,7 @@ def test_invalid_grouping(): """Invalid groupings raise AttributeError""" universe = mda.Universe(waterPSF, waterDCD) - sel_string = 'all' + sel_string = "all" selection = universe.select_atoms(sel_string) with pytest.raises(AttributeError): # centroid is attribute of AtomGroup, but not valid here @@ -44,55 +44,128 @@ def test_invalid_grouping(): # test data for grouping='atoms' -expected_masses_atoms = np.array([15.9994, 1.008, 1.008, 15.9994, 1.008, 1.008, - 15.9994, 1.008, 1.008, 15.9994, 1.008, 1.008, - 15.9994, 1.008, 1.008]) -expected_charges_atoms = np.array([-0.834, 0.417, 0.417, -0.834, 0.417, - 0.417, -0.834, 0.417, 0.417, -0.834, - 0.417, 0.417, -0.834, 0.417, 0.417]) -expected_xmass_atoms = np.array([0., 0., 0., 0.00723323, 0.00473288, 0., - 0., 0., 0., 0.]) -expected_xcharge_atoms = np.array([0., 0., 0., 2.21582311e-05, - -2.21582311e-05, 0., 0., 0., 0., 0.]) +expected_masses_atoms = np.array( + [ + 15.9994, + 1.008, + 1.008, + 15.9994, + 1.008, + 1.008, + 15.9994, + 1.008, + 1.008, + 15.9994, + 1.008, + 1.008, + 15.9994, + 1.008, + 1.008, + ] +) +expected_charges_atoms = np.array( + [ + -0.834, + 0.417, + 0.417, + -0.834, + 0.417, + 0.417, + -0.834, + 0.417, + 0.417, + -0.834, + 0.417, + 0.417, + -0.834, + 0.417, + 0.417, + ] +) +expected_xmass_atoms = np.array( + [0.0, 0.0, 0.0, 0.00723323, 0.00473288, 0.0, 0.0, 0.0, 0.0, 0.0] +) +expected_xcharge_atoms = np.array( + [0.0, 0.0, 0.0, 2.21582311e-05, -2.21582311e-05, 0.0, 0.0, 0.0, 0.0, 0.0] +) # test data for grouping='residues' -expected_masses_residues = np.array([18.0154, 18.0154, 18.0154, 18.0154, - 18.0154]) +expected_masses_residues = np.array( + [18.0154, 18.0154, 18.0154, 18.0154, 18.0154] +) expected_charges_residues = np.array([0, 0, 0, 0, 0]) -expected_xmass_residues = np.array([0., 0., 0., 0.00717967, 0.00478644, - 0., 0., 0., 0., 0.]) -expected_xcharge_residues = np.array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) +expected_xmass_residues = np.array( + [0.0, 0.0, 0.0, 0.00717967, 0.00478644, 0.0, 0.0, 0.0, 0.0, 0.0] +) +expected_xcharge_residues = np.array( + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +) # test data for grouping='segments' expected_masses_segments = np.array([90.0770]) expected_charges_segments = np.array([0]) -expected_xmass_segments = np.array([0., 0., 0., 0.01196611, 0., - 0., 0., 0., 0., 0.]) -expected_xcharge_segments = np.array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) +expected_xmass_segments = np.array( + [0.0, 0.0, 0.0, 0.01196611, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +) +expected_xcharge_segments = np.array( + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +) # test data for grouping='fragments' -expected_masses_fragments = np.array([18.0154, 18.0154, 18.0154, 18.0154, - 18.0154]) +expected_masses_fragments = np.array( + [18.0154, 18.0154, 18.0154, 18.0154, 18.0154] +) expected_charges_fragments = np.array([0, 0, 0, 0, 0]) -expected_xmass_fragments = np.array([0., 0., 0., 0.00717967, 0.00478644, - 0., 0., 0., 0., 0.]) -expected_xcharge_fragments = np.array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) - - -@pytest.mark.parametrize("grouping, expected_masses, expected_charges, expected_xmass, expected_xcharge", [ - ("atoms", expected_masses_atoms, expected_charges_atoms, - expected_xmass_atoms, expected_xcharge_atoms), - ("residues", expected_masses_residues, expected_charges_residues, - expected_xmass_residues, expected_xcharge_residues), - ("segments", expected_masses_segments, expected_charges_segments, - expected_xmass_segments, expected_xcharge_segments), - ("fragments", expected_masses_fragments, expected_charges_fragments, - expected_xmass_fragments, expected_xcharge_fragments) -]) -def test_lineardensity(grouping, expected_masses, expected_charges, - expected_xmass, expected_xcharge): +expected_xmass_fragments = np.array( + [0.0, 0.0, 0.0, 0.00717967, 0.00478644, 0.0, 0.0, 0.0, 0.0, 0.0] +) +expected_xcharge_fragments = np.array( + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +) + + +@pytest.mark.parametrize( + "grouping, expected_masses, expected_charges, expected_xmass, expected_xcharge", + [ + ( + "atoms", + expected_masses_atoms, + expected_charges_atoms, + expected_xmass_atoms, + expected_xcharge_atoms, + ), + ( + "residues", + expected_masses_residues, + expected_charges_residues, + expected_xmass_residues, + expected_xcharge_residues, + ), + ( + "segments", + expected_masses_segments, + expected_charges_segments, + expected_xmass_segments, + expected_xcharge_segments, + ), + ( + "fragments", + expected_masses_fragments, + expected_charges_fragments, + expected_xmass_fragments, + expected_xcharge_fragments, + ), + ], +) +def test_lineardensity( + grouping, + expected_masses, + expected_charges, + expected_xmass, + expected_xcharge, +): universe = mda.Universe(waterPSF, waterDCD) - sel_string = 'all' + sel_string = "all" selection = universe.select_atoms(sel_string) ld = LinearDensity(selection, grouping, binsize=5).run() assert_allclose(ld.masses, expected_masses) @@ -105,26 +178,30 @@ def test_lineardensity(grouping, expected_masses, expected_charges, @pytest.fixture(scope="module") def testing_Universe(): """Generate a universe for testing whether LinearDensity works with - updating atom groups. Also used for parallel analysis test.""" + updating atom groups. Also used for parallel analysis test.""" n_atoms = 3 - u = mda.Universe.empty(n_atoms=n_atoms, - n_residues=n_atoms, - n_segments=n_atoms, - atom_resindex=np.arange(n_atoms), - residue_segindex=np.arange(n_atoms)) + u = mda.Universe.empty( + n_atoms=n_atoms, + n_residues=n_atoms, + n_segments=n_atoms, + atom_resindex=np.arange(n_atoms), + residue_segindex=np.arange(n_atoms), + ) for attr in ["charges", "masses"]: u.add_TopologyAttr(attr, values=np.ones(n_atoms)) - coords = np.array([ - [[1., 1., 1.], [1., 2., 1.], [2., 1., 1.]], - [[1., 1., 2.], [1., 2., 1.], [2., 1., 1.]], - [[1., 1., 3.], [1., 2., 1.], [2., 1., 1.]], - [[1., 1., 4.], [1., 2., 1.], [2., 1., 1.]], - [[1., 1., 5.], [1., 2., 1.], [2., 1., 1.]] - ]) + coords = np.array( + [ + [[1.0, 1.0, 1.0], [1.0, 2.0, 1.0], [2.0, 1.0, 1.0]], + [[1.0, 1.0, 2.0], [1.0, 2.0, 1.0], [2.0, 1.0, 1.0]], + [[1.0, 1.0, 3.0], [1.0, 2.0, 1.0], [2.0, 1.0, 1.0]], + [[1.0, 1.0, 4.0], [1.0, 2.0, 1.0], [2.0, 1.0, 1.0]], + [[1.0, 1.0, 5.0], [1.0, 2.0, 1.0], [2.0, 1.0, 1.0]], + ] + ) - u.trajectory = get_reader_for(coords)(coords, order='fac', n_atoms=n_atoms) + u.trajectory = get_reader_for(coords)(coords, order="fac", n_atoms=n_atoms) for ts in u.trajectory: ts.dimensions = np.array([2, 2, 6, 90, 90, 90]) @@ -133,7 +210,7 @@ def testing_Universe(): def test_updating_atomgroup(testing_Universe): - expected_z_pos = np.array([0., 0.91329641, 0.08302695, 0., 0., 0.]) + expected_z_pos = np.array([0.0, 0.91329641, 0.08302695, 0.0, 0.0, 0.0]) u = testing_Universe selection = u.select_atoms("prop z < 3", updating=True) ld = LinearDensity(selection, binsize=1).run() @@ -143,27 +220,31 @@ def test_updating_atomgroup(testing_Universe): assert_allclose(ld.results.x.hist_bin_edges, expected_bin_edges) -testdict = {"pos": "mass_density", - "pos_std": "mass_density_stddev", - "char": "charge_density", - "char_std": "charge_density_stddev"} +testdict = { + "pos": "mass_density", + "pos_std": "mass_density_stddev", + "char": "charge_density", + "char_std": "charge_density_stddev", +} # TODO: Remove in 3.0.0 def test_old_name_deprecations(): universe = mda.Universe(waterPSF, waterDCD) - sel_string = 'all' + sel_string = "all" selection = universe.select_atoms(sel_string) ld = LinearDensity(selection, binsize=5).run() with pytest.warns(DeprecationWarning): assert_allclose(ld.results.x.pos, ld.results.x.mass_density) assert_allclose(ld.results.x.pos_std, ld.results.x.mass_density_stddev) assert_allclose(ld.results.x.char, ld.results.x.charge_density) - assert_allclose(ld.results.x.char_std, - ld.results.x.charge_density_stddev) + assert_allclose( + ld.results.x.char_std, ld.results.x.charge_density_stddev + ) for key in testdict.keys(): - assert_allclose(ld.results["x"][key], - ld.results["x"][testdict[key]]) + assert_allclose( + ld.results["x"][key], ld.results["x"][testdict[key]] + ) # Check that no DeprecationWarning is raised with new attributes with no_deprecated_call(): @@ -185,8 +266,13 @@ def test_parallel_analysis(testing_Universe): ld1 = LinearDensity(selection1, binsize=1).run() ld2 = LinearDensity(selection2, binsize=1).run() ld_whole = LinearDensity(selection_whole, binsize=1).run() - with pytest.warns(DeprecationWarning, - match="`_add_other_results` is deprecated!"): + with pytest.warns( + DeprecationWarning, match="`_add_other_results` is deprecated!" + ): ld1._add_other_results(ld2) - assert_allclose(ld1.results.z.mass_density, ld_whole.results.z.mass_density) - assert_allclose(ld1.results.x.mass_density, ld_whole.results.x.mass_density) + assert_allclose( + ld1.results.z.mass_density, ld_whole.results.z.mass_density + ) + assert_allclose( + ld1.results.x.mass_density, ld_whole.results.x.mass_density + ) diff --git a/testsuite/MDAnalysisTests/analysis/test_msd.py b/testsuite/MDAnalysisTests/analysis/test_msd.py index 3b96e40c61a..757fe6552cf 100644 --- a/testsuite/MDAnalysisTests/analysis/test_msd.py +++ b/testsuite/MDAnalysisTests/analysis/test_msd.py @@ -25,7 +25,7 @@ from MDAnalysis.analysis.msd import EinsteinMSD as MSD import MDAnalysis as mda -from numpy.testing import (assert_almost_equal, assert_equal) +from numpy.testing import assert_almost_equal, assert_equal import numpy as np from MDAnalysisTests.datafiles import PSF, DCD, RANDOM_WALK, RANDOM_WALK_TOPO @@ -34,38 +34,38 @@ import pytest -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def SELECTION(): - selection = 'backbone and name CA and resid 1-10' + selection = "backbone and name CA and resid 1-10" return selection -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return mda.Universe(PSF, DCD) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def NSTEP(): nstep = 5000 return nstep -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def random_walk_u(): # 100x100 return mda.Universe(RANDOM_WALK_TOPO, RANDOM_WALK) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def msd(u, SELECTION): # non fft msd - m = MSD(u, SELECTION, msd_type='xyz', fft=False) + m = MSD(u, SELECTION, msd_type="xyz", fft=False) m.run() return m -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def step_traj(NSTEP): # constant velocity x = np.arange(NSTEP) traj = np.vstack([x, x, x]).T @@ -75,7 +75,7 @@ def step_traj(NSTEP): # constant velocity return u -@block_import('tidynamics') +@block_import("tidynamics") def test_notidynamics(u, SELECTION): with pytest.raises(ImportError, match="tidynamics was not found"): u = mda.Universe(PSF, DCD) @@ -86,7 +86,7 @@ def test_notidynamics(u, SELECTION): def characteristic_poly(n, d): # polynomial that describes unit step traj MSD x = np.arange(0, n) - y = d*x*x + y = d * x * x return y @@ -98,65 +98,86 @@ def test_selection_works(self, msd): def test_ag_accepted(self, u): ag = u.select_atoms("resid 1") - m = MSD(ag, msd_type='xyz', fft=False) + m = MSD(ag, msd_type="xyz", fft=False) def test_updating_ag_rejected(self, u): updating_ag = u.select_atoms("around 3.5 resid 1", updating=True) errmsg = "UpdatingAtomGroups are not valid" with pytest.raises(TypeError, match=errmsg): - m = MSD(updating_ag, msd_type='xyz', fft=False) + m = MSD(updating_ag, msd_type="xyz", fft=False) - @pytest.mark.parametrize('msdtype', ['foo', 'bar', 'yx', 'zyx']) + @pytest.mark.parametrize("msdtype", ["foo", "bar", "yx", "zyx"]) def test_msdtype_error(self, u, SELECTION, msdtype): errmsg = f"invalid msd_type: {msdtype}" with pytest.raises(ValueError, match=errmsg): m = MSD(u, SELECTION, msd_type=msdtype) - @pytest.mark.parametrize("dim, dim_factor", [ - ('xyz', 3), ('xy', 2), ('xz', 2), ('yz', 2), ('x', 1), ('y', 1), - ('z', 1) - ]) - def test_simple_step_traj_all_dims(self, step_traj, NSTEP, dim, - dim_factor): + @pytest.mark.parametrize( + "dim, dim_factor", + [ + ("xyz", 3), + ("xy", 2), + ("xz", 2), + ("yz", 2), + ("x", 1), + ("y", 1), + ("z", 1), + ], + ) + def test_simple_step_traj_all_dims( + self, step_traj, NSTEP, dim, dim_factor + ): # testing the "simple" algorithm on constant velocity trajectory # should fit the polynomial y=dim_factor*x**2 - m_simple = MSD(step_traj, 'all', msd_type=dim, fft=False) + m_simple = MSD(step_traj, "all", msd_type=dim, fft=False) m_simple.run() poly = characteristic_poly(NSTEP, dim_factor) assert_almost_equal(m_simple.results.timeseries, poly, decimal=4) - @pytest.mark.parametrize("dim, dim_factor", [ - ('xyz', 3), ('xy', 2), ('xz', 2), ('yz', 2), ('x', 1), ('y', 1), - ('z', 1) - ]) - def test_simple_start_stop_step_all_dims(self, step_traj, NSTEP, dim, - dim_factor): + @pytest.mark.parametrize( + "dim, dim_factor", + [ + ("xyz", 3), + ("xy", 2), + ("xz", 2), + ("yz", 2), + ("x", 1), + ("y", 1), + ("z", 1), + ], + ) + def test_simple_start_stop_step_all_dims( + self, step_traj, NSTEP, dim, dim_factor + ): # testing the "simple" algorithm on constant velocity trajectory # test start stop step is working correctly - m_simple = MSD(step_traj, 'all', msd_type=dim, fft=False) + m_simple = MSD(step_traj, "all", msd_type=dim, fft=False) m_simple.run(start=10, stop=1000, step=10) poly = characteristic_poly(NSTEP, dim_factor) # polynomial must take offset start into account - assert_almost_equal(m_simple.results.timeseries, poly[0:990:10], - decimal=4) + assert_almost_equal( + m_simple.results.timeseries, poly[0:990:10], decimal=4 + ) def test_random_walk_u_simple(self, random_walk_u): # regress against random_walk test data - msd_rw = MSD(random_walk_u, 'all', msd_type='xyz', fft=False) + msd_rw = MSD(random_walk_u, "all", msd_type="xyz", fft=False) msd_rw.run() norm = np.linalg.norm(msd_rw.results.timeseries) val = 3932.39927487146 assert_almost_equal(norm, val, decimal=5) -@pytest.mark.skipif(import_not_available("tidynamics"), - reason="Test skipped because tidynamics not found") +@pytest.mark.skipif( + import_not_available("tidynamics"), + reason="Test skipped because tidynamics not found", +) class TestMSDFFT(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def msd_fft(self, u, SELECTION): # fft msd - m = MSD(u, SELECTION, msd_type='xyz', fft=True) + m = MSD(u, SELECTION, msd_type="xyz", fft=True) m.run() return m @@ -172,7 +193,7 @@ def test_fft_vs_simple_default_per_particle(self, msd, msd_fft): per_particle_fft = msd_fft.results.msds_by_particle assert_almost_equal(per_particle_simple, per_particle_fft, decimal=4) - @pytest.mark.parametrize("dim", ['xyz', 'xy', 'xz', 'yz', 'x', 'y', 'z']) + @pytest.mark.parametrize("dim", ["xyz", "xy", "xz", "yz", "x", "y", "z"]) def test_fft_vs_simple_all_dims(self, u, SELECTION, dim): # check fft and simple give same result for each dimensionality m_simple = MSD(u, SELECTION, msd_type=dim, fft=False) @@ -183,7 +204,7 @@ def test_fft_vs_simple_all_dims(self, u, SELECTION, dim): timeseries_fft = m_fft.results.timeseries assert_almost_equal(timeseries_simple, timeseries_fft, decimal=4) - @pytest.mark.parametrize("dim", ['xyz', 'xy', 'xz', 'yz', 'x', 'y', 'z']) + @pytest.mark.parametrize("dim", ["xyz", "xy", "xz", "yz", "x", "y", "z"]) def test_fft_vs_simple_all_dims_per_particle(self, u, SELECTION, dim): # check fft and simple give same result for each particle in each # dimension @@ -195,40 +216,58 @@ def test_fft_vs_simple_all_dims_per_particle(self, u, SELECTION, dim): per_particle_fft = m_fft.results.msds_by_particle assert_almost_equal(per_particle_simple, per_particle_fft, decimal=4) - @pytest.mark.parametrize("dim, dim_factor", [ - ('xyz', 3), ('xy', 2), ('xz', 2), ('yz', 2), ('x', 1), ('y', 1), - ('z', 1) - ]) + @pytest.mark.parametrize( + "dim, dim_factor", + [ + ("xyz", 3), + ("xy", 2), + ("xz", 2), + ("yz", 2), + ("x", 1), + ("y", 1), + ("z", 1), + ], + ) def test_fft_step_traj_all_dims(self, step_traj, NSTEP, dim, dim_factor): # testing the fft algorithm on constant velocity trajectory # this should fit the polynomial y=dim_factor*x**2 # fft based tests require a slight decrease in expected prescision # primarily due to roundoff in fft(ifft()) calls. # relative accuracy expected to be around ~1e-12 - m_simple = MSD(step_traj, 'all', msd_type=dim, fft=True) + m_simple = MSD(step_traj, "all", msd_type=dim, fft=True) m_simple.run() poly = characteristic_poly(NSTEP, dim_factor) # this was relaxed from decimal=4 for numpy=1.13 test assert_almost_equal(m_simple.results.timeseries, poly, decimal=3) - @pytest.mark.parametrize("dim, dim_factor", [( - 'xyz', 3), ('xy', 2), ('xz', 2), ('yz', 2), ('x', 1), ('y', 1), - ('z', 1) - ]) - def test_fft_start_stop_step_all_dims(self, step_traj, NSTEP, dim, - dim_factor): + @pytest.mark.parametrize( + "dim, dim_factor", + [ + ("xyz", 3), + ("xy", 2), + ("xz", 2), + ("yz", 2), + ("x", 1), + ("y", 1), + ("z", 1), + ], + ) + def test_fft_start_stop_step_all_dims( + self, step_traj, NSTEP, dim, dim_factor + ): # testing the fft algorithm on constant velocity trajectory # test start stop step is working correctly - m_simple = MSD(step_traj, 'all', msd_type=dim, fft=True) + m_simple = MSD(step_traj, "all", msd_type=dim, fft=True) m_simple.run(start=10, stop=1000, step=10) poly = characteristic_poly(NSTEP, dim_factor) # polynomial must take offset start into account - assert_almost_equal(m_simple.results.timeseries, poly[0:990:10], - decimal=3) + assert_almost_equal( + m_simple.results.timeseries, poly[0:990:10], decimal=3 + ) def test_random_walk_u_fft(self, random_walk_u): # regress against random_walk test data - msd_rw = MSD(random_walk_u, 'all', msd_type='xyz', fft=True) + msd_rw = MSD(random_walk_u, "all", msd_type="xyz", fft=True) msd_rw.run() norm = np.linalg.norm(msd_rw.results.timeseries) val = 3932.39927487146 diff --git a/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py b/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py index ce2ae5e4864..e695a605691 100644 --- a/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py +++ b/testsuite/MDAnalysisTests/analysis/test_nucleicacids.py @@ -27,15 +27,19 @@ from pytest import approx -from MDAnalysis.analysis.nucleicacids import (NucPairDist, WatsonCrickDist, - MajorPairDist, MinorPairDist) +from MDAnalysis.analysis.nucleicacids import ( + NucPairDist, + WatsonCrickDist, + MajorPairDist, + MinorPairDist, +) from MDAnalysisTests.datafiles import RNA_PSF, RNA_PDB from MDAnalysis.core.groups import ResidueGroup -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return mda.Universe(RNA_PSF, RNA_PDB) @@ -51,10 +55,10 @@ def test_empty_ag_error(strand): strand2 = ResidueGroup([strand.residues[1]]) with pytest.raises(ValueError, match="returns an empty AtomGroup"): - NucPairDist.select_strand_atoms(strand1, strand2, 'UNK1', 'O2') + NucPairDist.select_strand_atoms(strand1, strand2, "UNK1", "O2") -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def wc_rna(strand, client_NucPairDist): strand1 = ResidueGroup([strand.residues[0], strand.residues[21]]) strand2 = ResidueGroup([strand.residues[1], strand.residues[22]]) diff --git a/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py b/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py index ea6c3e03fbf..61577599a00 100644 --- a/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py +++ b/testsuite/MDAnalysisTests/analysis/test_nuclinfo.py @@ -30,147 +30,212 @@ ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return mda.Universe(RNA_PSF, RNA_PDB) -@pytest.mark.parametrize('i, bp, seg1, seg2, expected_value', ( - ( 1, 2, 'RNAA', 'RNAA', 4.3874702), - (22, 23, 'RNAA', 'RNAA', 4.1716404), -)) +@pytest.mark.parametrize( + "i, bp, seg1, seg2, expected_value", + ( + (1, 2, "RNAA", "RNAA", 4.3874702), + (22, 23, "RNAA", "RNAA", 4.1716404), + ), +) def test_wc_pair(u, i, bp, seg1, seg2, expected_value): val = nuclinfo.wc_pair(u, i, bp, seg1=seg1, seg2=seg2) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('i, bp, seg1, seg2, expected_value', ( - ( 3, 17, 'RNAA', 'RNAA', 15.06506), - (20, 5, 'RNAA', 'RNAA', 3.219116), -)) +@pytest.mark.parametrize( + "i, bp, seg1, seg2, expected_value", + ( + (3, 17, "RNAA", "RNAA", 15.06506), + (20, 5, "RNAA", "RNAA", 3.219116), + ), +) def test_minor_pair(u, i, bp, seg1, seg2, expected_value): val = nuclinfo.minor_pair(u, i, bp, seg1=seg1, seg2=seg2) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('i, bp, seg1, seg2, expected_value', ( - (2, 12, 'RNAA', 'RNAA', 26.884272), - (5, 9, 'RNAA', 'RNAA', 13.578535), -)) +@pytest.mark.parametrize( + "i, bp, seg1, seg2, expected_value", + ( + (2, 12, "RNAA", "RNAA", 26.884272), + (5, 9, "RNAA", "RNAA", 13.578535), + ), +) def test_major_pair(u, i, bp, seg1, seg2, expected_value): val = nuclinfo.major_pair(u, i, bp, seg1=seg1, seg2=seg2) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 9, 3.16497), - ('RNAA', 21, 22.07721), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 9, 3.16497), + ("RNAA", 21, 22.07721), + ), +) def test_phase_cp(u, seg, i, expected_value): val = nuclinfo.phase_cp(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 1, 359.57580), - ('RNAA', 11, 171.71645), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 1, 359.57580), + ("RNAA", 11, 171.71645), + ), +) def test_phase_as(u, seg, i, expected_value): val = nuclinfo.phase_as(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 5, [302.203802, 179.043077, 35.271411, 79.499729, 201.000393, - 282.14321 , 210.709327]), - ('RNAA', 21, [280.388619, 185.12919 , 56.616215, 64.87354 , 187.153367, - 279.340915, 215.332144]), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ( + "RNAA", + 5, + [ + 302.203802, + 179.043077, + 35.271411, + 79.499729, + 201.000393, + 282.14321, + 210.709327, + ], + ), + ( + "RNAA", + 21, + [ + 280.388619, + 185.12919, + 56.616215, + 64.87354, + 187.153367, + 279.340915, + 215.332144, + ], + ), + ), +) def test_tors(u, seg, i, expected_value): val = nuclinfo.tors(u, seg=seg, i=i) assert_allclose(val, expected_value, rtol=1e-03) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 6, 279.15103), - ('RNAA', 18, 298.09936), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 6, 279.15103), + ("RNAA", 18, 298.09936), + ), +) def test_tors_alpha(u, seg, i, expected_value): val = nuclinfo.tors_alpha(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 7, 184.20501), - ('RNAA', 15, 169.70042), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 7, 184.20501), + ("RNAA", 15, 169.70042), + ), +) def test_tors_beta(u, seg, i, expected_value): val = nuclinfo.tors_beta(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 7, 52.72022), - ('RNAA', 15, 54.59684), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 7, 52.72022), + ("RNAA", 15, 54.59684), + ), +) def test_tors_gamma(u, seg, i, expected_value): val = nuclinfo.tors_gamma(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 7, 84.80554), - ('RNAA', 15, 82.00043), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 7, 84.80554), + ("RNAA", 15, 82.00043), + ), +) def test_tors_delta(u, seg, i, expected_value): val = nuclinfo.tors_delta(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 7, 200.40990), - ('RNAA', 15, 210.96953), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 7, 200.40990), + ("RNAA", 15, 210.96953), + ), +) def test_tors_eps(u, seg, i, expected_value): val = nuclinfo.tors_eps(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 7, 297.84736), - ('RNAA', 15, 330.24898) -)) +@pytest.mark.parametrize( + "seg, i, expected_value", (("RNAA", 7, 297.84736), ("RNAA", 15, 330.24898)) +) def test_tors_zeta(u, seg, i, expected_value): val = nuclinfo.tors_zeta(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 1, 178.37435), - ('RNAA', 2, 202.03418), - ('RNAA', 7, 200.91674), - ('RNAA', 15, 209.32109), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 1, 178.37435), + ("RNAA", 2, 202.03418), + ("RNAA", 7, 200.91674), + ("RNAA", 15, 209.32109), + ), +) def test_tors_chi(u, seg, i, expected_value): val = nuclinfo.tors_chi(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('seg, i, expected_value', ( - ('RNAA', 20, 103.07024), - ('RNAA', 5, 156.62223), - ('RNAA', 7 , 77.94538), - ('RNAA', 15, 130.18539), -)) +@pytest.mark.parametrize( + "seg, i, expected_value", + ( + ("RNAA", 20, 103.07024), + ("RNAA", 5, 156.62223), + ("RNAA", 7, 77.94538), + ("RNAA", 15, 130.18539), + ), +) def test_hydroxyl(u, seg, i, expected_value): val = nuclinfo.hydroxyl(u, seg=seg, i=i) assert_almost_equal(val, expected_value, decimal=3) -@pytest.mark.parametrize('bp1, bp2, i, seg1, seg2, seg3, expected_value', ( - (16, 2, 3, 'RNAA', 'RNAA', 'RNAA', 314.69804), - (8, 9, 10, 'RNAA', 'RNAA', 'RNAA', 34.50106), -)) -def test_pseudo_dihe_baseflip(u, bp1, bp2, i, seg1, seg2, seg3, expected_value): +@pytest.mark.parametrize( + "bp1, bp2, i, seg1, seg2, seg3, expected_value", + ( + (16, 2, 3, "RNAA", "RNAA", "RNAA", 314.69804), + (8, 9, 10, "RNAA", "RNAA", "RNAA", 34.50106), + ), +) +def test_pseudo_dihe_baseflip( + u, bp1, bp2, i, seg1, seg2, seg3, expected_value +): val = nuclinfo.pseudo_dihe_baseflip(u, bp1, bp2, i, seg1, seg2, seg3) assert_almost_equal(val, expected_value, decimal=3) diff --git a/testsuite/MDAnalysisTests/analysis/test_pca.py b/testsuite/MDAnalysisTests/analysis/test_pca.py index 19dca6cf3b0..663aa5e68ff 100644 --- a/testsuite/MDAnalysisTests/analysis/test_pca.py +++ b/testsuite/MDAnalysisTests/analysis/test_pca.py @@ -24,44 +24,58 @@ import MDAnalysis as mda from MDAnalysis.analysis import align import MDAnalysis.analysis.pca -from MDAnalysis.analysis.pca import (PCA, cosine_content, - rmsip, cumulative_overlap) +from MDAnalysis.analysis.pca import ( + PCA, + cosine_content, + rmsip, + cumulative_overlap, +) -from numpy.testing import (assert_almost_equal, assert_equal, - assert_array_almost_equal, assert_allclose,) +from numpy.testing import ( + assert_almost_equal, + assert_equal, + assert_array_almost_equal, + assert_allclose, +) -from MDAnalysisTests.datafiles import (PSF, DCD, RANDOM_WALK, RANDOM_WALK_TOPO, - waterPSF, waterDCD) +from MDAnalysisTests.datafiles import ( + PSF, + DCD, + RANDOM_WALK, + RANDOM_WALK_TOPO, + waterPSF, + waterDCD, +) import pytest -SELECTION = 'backbone and name CA and resid 1-10' +SELECTION = "backbone and name CA and resid 1-10" -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return mda.Universe(PSF, DCD) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def u_fresh(): # each test gets a fresh universe return mda.Universe(PSF, DCD) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u_aligned(): u = mda.Universe(PSF, DCD, in_memory=True) align.AlignTraj(u, u, select=SELECTION).run() return u -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def pca(u): u.transfer_to_memory() return PCA(u, select=SELECTION).run() -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def pca_aligned(u): # run on a copy so positions in u are unchanged u_copy = u.copy() @@ -85,15 +99,17 @@ def test_cum_var(pca): def test_pcs(pca): - assert_equal(pca.results.p_components.shape, (pca._n_atoms * 3, - pca._n_atoms * 3)) + assert_equal( + pca.results.p_components.shape, (pca._n_atoms * 3, pca._n_atoms * 3) + ) def test_pcs_n_components(u): pca = PCA(u, select=SELECTION).run() - assert_equal(pca.n_components, pca._n_atoms*3) - assert_equal(pca.results.p_components.shape, (pca._n_atoms * 3, - pca._n_atoms * 3)) + assert_equal(pca.n_components, pca._n_atoms * 3) + assert_equal( + pca.results.p_components.shape, (pca._n_atoms * 3, pca._n_atoms * 3) + ) pca.n_components = 10 assert_equal(pca.n_components, 10) assert_equal(pca.results.p_components.shape, (pca._n_atoms * 3, 10)) @@ -102,27 +118,27 @@ def test_pcs_n_components(u): def test_different_steps(pca, u): atoms = u.select_atoms(SELECTION) dot = pca.transform(atoms, start=5, stop=7, step=1) - assert_equal(dot.shape, (2, atoms.n_atoms*3)) + assert_equal(dot.shape, (2, atoms.n_atoms * 3)) def test_transform_different_atoms(pca, u): - atoms = u.select_atoms('backbone and name N and resid 1-10') + atoms = u.select_atoms("backbone and name N and resid 1-10") with pytest.warns(UserWarning): pca.transform(atoms, start=5, stop=7, step=1) def test_transform_rerun(u): - atoms = u.select_atoms('bynum 1-10') + atoms = u.select_atoms("bynum 1-10") u.transfer_to_memory() - pca = PCA(u, select='bynum 1-10').run(stop=5) + pca = PCA(u, select="bynum 1-10").run(stop=5) dot = pca.transform(atoms) assert_equal(dot.shape, (98, atoms.n_atoms * 3)) def test_pca_not_run(u): - atoms = u.select_atoms('bynum 1-10') + atoms = u.select_atoms("bynum 1-10") u.transfer_to_memory() - pca = PCA(u, select='bynum 1-10') + pca = PCA(u, select="bynum 1-10") with pytest.raises(ValueError): dot = pca.transform(atoms, stop=5) @@ -137,7 +153,7 @@ def test_no_frames(u): def test_can_run_frames(u): atoms = u.select_atoms(SELECTION) u.transfer_to_memory() - PCA(u, select=SELECTION).run(frames=[0,1]) + PCA(u, select=SELECTION).run(frames=[0, 1]) def test_can_run_frames(u): @@ -168,37 +184,39 @@ def test_project_no_pca_run(u, pca): pca_class = PCA(u, select=SELECTION) with pytest.raises(ValueError) as exc: pca_class.project_single_frame() - assert 'Call run() on the PCA before projecting' in str(exc.value) + assert "Call run() on the PCA before projecting" in str(exc.value) def test_project_none_anchor(u, pca): - group = u.select_atoms('resnum 1') + group = u.select_atoms("resnum 1") with pytest.raises(ValueError) as exc: func = pca.project_single_frame(0, group=group, anchor=None) - assert ("'anchor' cannot be 'None'" + - " if 'group' is not 'None'") in str(exc.value) + assert ("'anchor' cannot be 'None'" + " if 'group' is not 'None'") in str( + exc.value + ) def test_project_more_anchor(u, pca): - group = u.select_atoms('resnum 1') + group = u.select_atoms("resnum 1") with pytest.raises(ValueError) as exc: - project = pca.project_single_frame(0, group=group, anchor='backbone') + project = pca.project_single_frame(0, group=group, anchor="backbone") assert "More than one 'anchor' found in residues" in str(exc.value) def test_project_less_anchor(u, pca): - group = u.select_atoms('all') + group = u.select_atoms("all") with pytest.raises(ValueError) as exc: - project = pca.project_single_frame(0, group=group, anchor='name CB') - assert ("Some residues in 'group'" + - " do not have an 'anchor'") in str(exc.value) + project = pca.project_single_frame(0, group=group, anchor="name CB") + assert ("Some residues in 'group'" + " do not have an 'anchor'") in str( + exc.value + ) def test_project_invalid_anchor(u): - pca = PCA(u, select='name CA').run() - group = u.select_atoms('all') + pca = PCA(u, select="name CA").run() + group = u.select_atoms("all") with pytest.raises(ValueError) as exc: - project = pca.project_single_frame(0, group=group, anchor='name N') + project = pca.project_single_frame(0, group=group, anchor="name N") assert "Some 'anchors' are not part of PCA class" in str(exc.value) @@ -227,8 +245,7 @@ def test_project_reconstruct_whole(u, u_fresh): @pytest.mark.parametrize( - ("n1", "n2"), - [(0, 0), (0, [0]), ([0, 1], [0, 1]), (0, 1), (1, 0)] + ("n1", "n2"), [(0, 0), (0, [0]), ([0, 1], [0, 1]), (0, 1), (1, 0)] ) def test_project_twice_projection(u_fresh, n1, n2): # Two succesive projections are applied. The second projection does nothing @@ -252,17 +269,14 @@ def test_project_twice_projection(u_fresh, n1, n2): def test_project_extrapolate_translation(u_fresh): # when the projection is extended to non-PCA atoms, # non-PCA atoms' coordinates will be conserved relative to the anchor atom - pca = PCA(u_fresh, select='resnum 1 and backbone').run() - sel = 'resnum 1 and name CA CB CG' + pca = PCA(u_fresh, select="resnum 1 and backbone").run() + sel = "resnum 1 and name CA CB CG" group = u_fresh.select_atoms(sel) - project = pca.project_single_frame(0, group=group, - anchor='name CA') + project = pca.project_single_frame(0, group=group, anchor="name CA") - distances_original = ( - mda.lib.distances.self_distance_array(group.positions) - ) - distances_new = ( - mda.lib.distances.self_distance_array(project(group).positions) + distances_original = mda.lib.distances.self_distance_array(group.positions) + distances_new = mda.lib.distances.self_distance_array( + project(group).positions ) assert_allclose(distances_original, distances_new, rtol=1e-05) @@ -273,7 +287,7 @@ def test_cosine_content(): pca_random = PCA(rand).run() dot = pca_random.transform(rand.atoms) content = cosine_content(dot, 0) - assert_almost_equal(content, .99, 1) + assert_almost_equal(content, 0.99, 1) def test_mean_shape(pca_aligned, u): @@ -285,19 +299,17 @@ def test_mean_shape(pca_aligned, u): def test_calculate_mean(pca_aligned, u, u_aligned): ag = u_aligned.select_atoms(SELECTION) coords = u_aligned.trajectory.coordinate_array[:, ag.ix] - assert_almost_equal(pca_aligned.mean, coords.mean( - axis=0), decimal=5) + assert_almost_equal(pca_aligned.mean, coords.mean(axis=0), decimal=5) def test_given_mean(pca, u): - pca = PCA(u, select=SELECTION, align=False, - mean=pca.mean).run() + pca = PCA(u, select=SELECTION, align=False, mean=pca.mean).run() assert_almost_equal(pca.cov, pca.cov, decimal=5) def test_wrong_num_given_mean(u): wrong_mean = [[0, 0, 0], [1, 1, 1]] - with pytest.raises(ValueError, match='Number of atoms in'): + with pytest.raises(ValueError, match="Number of atoms in"): pca = PCA(u, select=SELECTION, mean=wrong_mean).run() @@ -316,22 +328,24 @@ def test_pca_rmsip_self(pca): def test_rmsip_ortho(pca): - value = rmsip(pca.results.p_components[:, :10].T, - pca.results.p_components[:, 10:20].T) + value = rmsip( + pca.results.p_components[:, :10].T, + pca.results.p_components[:, 10:20].T, + ) assert_almost_equal(value, 0.0) def test_pytest_too_many_components(pca): with pytest.raises(ValueError) as exc: pca.rmsip(pca, n_components=(1, 2, 3)) - assert 'Too many values' in str(exc.value) + assert "Too many values" in str(exc.value) def test_asymmetric_rmsip(pca): a = pca.rmsip(pca, n_components=(10, 4)) b = pca.rmsip(pca, n_components=(4, 10)) - assert abs(a-b) > 0.1, 'RMSIP should be asymmetric' + assert abs(a - b) > 0.1, "RMSIP should be asymmetric" assert_almost_equal(b, 1.0) @@ -346,51 +360,47 @@ def test_cumulative_overlap_ortho(pca): assert_almost_equal(value, 0.0) -@pytest.mark.parametrize( - 'method', ['rmsip', - 'cumulative_overlap']) +@pytest.mark.parametrize("method", ["rmsip", "cumulative_overlap"]) def test_compare_not_run_other(u, pca, method): pca2 = PCA(u) func = getattr(pca, method) with pytest.raises(ValueError) as exc: func(pca2) - assert 'Call run()' in str(exc.value) + assert "Call run()" in str(exc.value) -@pytest.mark.parametrize( - 'method', ['rmsip', - 'cumulative_overlap']) +@pytest.mark.parametrize("method", ["rmsip", "cumulative_overlap"]) def test_compare_not_run_self(u, pca, method): pca2 = PCA(u) func = getattr(pca2, method) with pytest.raises(ValueError) as exc: func(pca) - assert 'Call run()' in str(exc.value) + assert "Call run()" in str(exc.value) -@pytest.mark.parametrize( - 'method', ['rmsip', - 'cumulative_overlap']) +@pytest.mark.parametrize("method", ["rmsip", "cumulative_overlap"]) def test_compare_wrong_class(u, pca, method): func = getattr(pca, method) with pytest.raises(ValueError) as exc: func(3) - assert 'must be another PCA class' in str(exc.value) + assert "must be another PCA class" in str(exc.value) -@pytest.mark.parametrize("attr", ("p_components", "variance", - "cumulated_variance")) +@pytest.mark.parametrize( + "attr", ("p_components", "variance", "cumulated_variance") +) def test_pca_attr_warning(u, attr): pca = PCA(u, select=SELECTION).run(stop=2) 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 @@ -399,9 +409,8 @@ def test_class_is_parallelizable(classname, is_parallelizable): @pytest.mark.parametrize( "classname,backends", [ - (MDAnalysis.analysis.pca.PCA, ('serial',)), - ] + (MDAnalysis.analysis.pca.PCA, ("serial",)), + ], ) def test_supported_backends(classname, backends): assert classname.get_supported_backends() == backends - diff --git a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py index 5d8790ab3db..3cbee2952cf 100644 --- a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py +++ b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py @@ -45,8 +45,7 @@ def u(): @staticmethod @pytest.fixture() def p(u): - ags = [r.atoms.select_atoms('name C* N*') - for r in u.residues] + ags = [r.atoms.select_atoms("name C* N*") for r in u.residues] p = polymer.PersistenceLength(ags) return p @@ -69,17 +68,19 @@ def test_lb(self, p_run): def test_fit(self, p_run): assert_almost_equal(p_run.results.lp, 6.504, 3) - assert len(p_run.results.fit) == len(p_run.results.bond_autocorrelation) + assert len(p_run.results.fit) == len( + p_run.results.bond_autocorrelation + ) def test_raise_NoDataError(self, p): - #Ensure that a NoDataError is raised if perform_fit() + # Ensure that a NoDataError is raised if perform_fit() # is called before the run() method of AnalysisBase with pytest.raises(NoDataError): p._perform_fit() def test_plot_ax_return(self, p_run): - '''Ensure that a matplotlib axis object is - returned when plot() is called.''' + """Ensure that a matplotlib axis object is + returned when plot() is called.""" actual = p_run.plot() expected = matplotlib.axes.Axes assert isinstance(actual, expected) @@ -125,7 +126,7 @@ def test_fit_noisy(self): class TestSortBackbone(object): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(): return mda.Universe(TRZ_psf, TRZ) @@ -160,10 +161,9 @@ def test_branches(self, u): def test_circular(self): u = mda.Universe.empty(6, trajectory=True) # circular structure - bondlist = [(0, 1), (1, 2), (2, 3), - (3, 4), (4, 5), (5, 0)] + bondlist = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)] u.add_TopologyAttr(Bonds(bondlist)) with pytest.raises(ValueError) as ex: polymer.sort_backbone(u.atoms) - assert 'cyclical' in str(ex.value) + assert "cyclical" in str(ex.value) diff --git a/testsuite/MDAnalysisTests/analysis/test_rdf.py b/testsuite/MDAnalysisTests/analysis/test_rdf.py index 90adef77e3a..d3507e56734 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rdf.py +++ b/testsuite/MDAnalysisTests/analysis/test_rdf.py @@ -33,7 +33,7 @@ @pytest.fixture() def u(): u = mda.Universe(two_water_gro, in_memory=True) - u.add_TopologyAttr('chainIDs', u.atoms.resids) + u.add_TopologyAttr("chainIDs", u.atoms.resids) return u @@ -43,8 +43,8 @@ def sels(u): # (NOTE: requires in-memory coordinates to make them permanent) for at, (x, y) in zip(u.atoms, zip([1] * 3 + [2] * 3, [2, 1, 3] * 2)): at.position = x, y, 0.0 - s1 = u.select_atoms('name OW') - s2 = u.select_atoms('name HW1 HW2') + s1 = u.select_atoms("name OW") + s2 = u.select_atoms("name HW1 HW2") return s1, s2 @@ -96,10 +96,9 @@ def test_exclusion(sels): assert rdf.results.count.sum() == 4 -@pytest.mark.parametrize("attr, count", [ - ("residue", 8), - ("segment", 0), - ("chain", 8)]) +@pytest.mark.parametrize( + "attr, count", [("residue", 8), ("segment", 0), ("chain", 8)] +) def test_ignore_same_residues(sels, attr, count): # should see two distances with 4 counts each s1, s2 = sels @@ -110,13 +109,18 @@ def test_ignore_same_residues(sels, attr, count): def test_ignore_same_residues_fails(sels): s1, s2 = sels - with pytest.raises(ValueError, match="The exclude_same argument to InterRDF must be"): + with pytest.raises( + ValueError, match="The exclude_same argument to InterRDF must be" + ): InterRDF(s2, s2, exclude_same="unsupported").run() - with pytest.raises(ValueError, match="The exclude_same argument to InterRDF cannot be used with"): + with pytest.raises( + ValueError, + match="The exclude_same argument to InterRDF cannot be used with", + ): InterRDF(s2, s2, exclude_same="residue", exclusion_block=tuple()).run() - - + + @pytest.mark.parametrize("attr", ("rdf", "bins", "edges", "count")) def test_rdf_attr_warning(sels, attr): s1, s2 = sels @@ -126,18 +130,18 @@ def test_rdf_attr_warning(sels, attr): getattr(rdf, attr) is rdf.results[attr] -@pytest.mark.parametrize("norm, value", [ - ("density", 1.956823), - ("rdf", 244602.88385), - ("none", 4)]) +@pytest.mark.parametrize( + "norm, value", [("density", 1.956823), ("rdf", 244602.88385), ("none", 4)] +) def test_norm(sels, norm, value): s1, s2 = sels rdf = InterRDF(s1, s2, norm=norm).run() assert_allclose(max(rdf.results.rdf), value) -@pytest.mark.parametrize("norm, norm_required", [ - ("Density", "density"), (None, "none")]) +@pytest.mark.parametrize( + "norm, norm_required", [("Density", "density"), (None, "none")] +) def test_norm_values(sels, norm, norm_required): s1, s2 = sels rdf = InterRDF(s1, s2, norm=norm).run() diff --git a/testsuite/MDAnalysisTests/analysis/test_rdf_s.py b/testsuite/MDAnalysisTests/analysis/test_rdf_s.py index 49e2a66bd5b..5c6e8b6031a 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rdf_s.py +++ b/testsuite/MDAnalysisTests/analysis/test_rdf_s.py @@ -30,23 +30,24 @@ from MDAnalysisTests.datafiles import GRO_MEMPROT, XTC_MEMPROT -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def u(): return mda.Universe(GRO_MEMPROT, XTC_MEMPROT) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def sels(u): - s1 = u.select_atoms('name ZND and resid 289') + s1 = u.select_atoms("name ZND and resid 289") s2 = u.select_atoms( - '(name OD1 or name OD2) and resid 51 and sphzone 5.0 (resid 289)') - s3 = u.select_atoms('name ZND and (resid 291 or resid 292)') - s4 = u.select_atoms('(name OD1 or name OD2) and sphzone 5.0 (resid 291)') + "(name OD1 or name OD2) and resid 51 and sphzone 5.0 (resid 289)" + ) + s3 = u.select_atoms("name ZND and (resid 291 or resid 292)") + s4 = u.select_atoms("(name OD1 or name OD2) and sphzone 5.0 (resid 291)") ags = [[s1, s2], [s3, s4]] return ags -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def rdf(u, sels): return InterRDF_s(u, sels).run() @@ -100,22 +101,27 @@ def test_double_run(rdf): def test_cdf(rdf): rdf.get_cdf() - ref = rdf.results.count[0][0][0].sum()/rdf.n_frames + ref = rdf.results.count[0][0][0].sum() / rdf.n_frames assert rdf.results.cdf[0][0][0][-1] == ref -@pytest.mark.parametrize("density, value", [ - (None, 26551.55088100731), # default, like False (no kwarg, see below) - (False, 26551.55088100731), - (True, 0.021915460340071267)]) +@pytest.mark.parametrize( + "density, value", + [ + (None, 26551.55088100731), # default, like False (no kwarg, see below) + (False, 26551.55088100731), + (True, 0.021915460340071267), + ], +) def test_density(u, sels, density, value): - kwargs = {'density': density} if density is not None else {} + kwargs = {"density": density} if density is not None else {} rdf = InterRDF_s(u, sels, **kwargs).run() assert_almost_equal(max(rdf.results.rdf[0][0][0]), value) if not density: - s1 = u.select_atoms('name ZND and resid 289') + s1 = u.select_atoms("name ZND and resid 289") s2 = u.select_atoms( - 'name OD1 and resid 51 and sphzone 5.0 (resid 289)') + "name OD1 and resid 51 and sphzone 5.0 (resid 289)" + ) rdf_ref = InterRDF(s1, s2).run() assert_almost_equal(rdf_ref.results.rdf, rdf.results.rdf[0][0][0]) @@ -125,23 +131,29 @@ def test_overwrite_norm(u, sels): assert rdf.norm == "density" -@pytest.mark.parametrize("norm, value", [ - ("density", 0.021915460340071267), - ("rdf", 26551.55088100731), - ("none", 0.6)]) +@pytest.mark.parametrize( + "norm, value", + [ + ("density", 0.021915460340071267), + ("rdf", 26551.55088100731), + ("none", 0.6), + ], +) def test_norm(u, sels, norm, value): rdf = InterRDF_s(u, sels, norm=norm).run() assert_allclose(max(rdf.results.rdf[0][0][0]), value) if norm == "rdf": - s1 = u.select_atoms('name ZND and resid 289') + s1 = u.select_atoms("name ZND and resid 289") s2 = u.select_atoms( - 'name OD1 and resid 51 and sphzone 5.0 (resid 289)') + "name OD1 and resid 51 and sphzone 5.0 (resid 289)" + ) rdf_ref = InterRDF(s1, s2).run() assert_almost_equal(rdf_ref.results.rdf, rdf.results.rdf[0][0][0]) -@pytest.mark.parametrize("norm, norm_required", [ - ("Density", "density"), (None, "none")]) +@pytest.mark.parametrize( + "norm, norm_required", [("Density", "density"), (None, "none")] +) def test_norm_values(u, sels, norm, norm_required): rdf = InterRDF_s(u, sels, norm=norm).run() assert rdf.norm == norm_required diff --git a/testsuite/MDAnalysisTests/analysis/test_results.py b/testsuite/MDAnalysisTests/analysis/test_results.py index e3d8fa6ca95..adfbc4b5063 100644 --- a/testsuite/MDAnalysisTests/analysis/test_results.py +++ b/testsuite/MDAnalysisTests/analysis/test_results.py @@ -156,7 +156,9 @@ def merger(self): @pytest.mark.parametrize("n", [1, 2, 5, 14]) def test_all_results(self, results_0, results_1, merger, n): - objects = [obj for obj, _ in zip(cycle([results_0, results_1]), range(n))] + objects = [ + obj for obj, _ in zip(cycle([results_0, results_1]), range(n)) + ] arr = [i for _, i in zip(range(n), cycle([0, 1]))] answers = { @@ -169,14 +171,19 @@ 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=}") + 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'"): + 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 diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index deb46885c82..f39baa68f12 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -61,17 +61,20 @@ def u2(self): @pytest.fixture() def p_first(self, u): u.trajectory[0] - return u.select_atoms('protein') + return u.select_atoms("protein") @pytest.fixture() def p_last(self, u2): u2.trajectory[-1] - return u2.select_atoms('protein') + return u2.select_atoms("protein") # internal test def test_p_frames(self, p_first, p_last): # check that these fixtures are really different - assert p_first.universe.trajectory.ts.frame != p_last.universe.trajectory.ts.frame + assert ( + p_first.universe.trajectory.ts.frame + != p_last.universe.trajectory.ts.frame + ) assert not np.allclose(p_first.positions, p_last.positions) def test_no_center(self, a, b): @@ -83,14 +86,12 @@ def test_center(self, a, b): assert_almost_equal(rmsd, 0.0) def test_list(self, a, b): - rmsd = rms.rmsd(a.tolist(), - b.tolist(), - center=False) + rmsd = rms.rmsd(a.tolist(), b.tolist(), center=False) assert_almost_equal(rmsd, 1.0) - @pytest.mark.parametrize('dtype', [np.float32, np.float64]) + @pytest.mark.parametrize("dtype", [np.float32, np.float64]) def test_superposition(self, a, b, u, dtype): - bb = u.atoms.select_atoms('backbone') + bb = u.atoms.select_atoms("backbone") a = bb.positions.copy() u.trajectory[-1] b = bb.positions.copy() @@ -110,20 +111,29 @@ def test_weights(self, a, b): def test_weights_and_superposition_1(self, u): weights = np.ones(len(u.trajectory[0])) - weighted = rms.rmsd(u.trajectory[0], u.trajectory[1], - weights=weights, superposition=True) - firstCoords = rms.rmsd(u.trajectory[0], u.trajectory[1], - superposition=True) + weighted = rms.rmsd( + u.trajectory[0], + u.trajectory[1], + weights=weights, + superposition=True, + ) + firstCoords = rms.rmsd( + u.trajectory[0], u.trajectory[1], superposition=True + ) assert_almost_equal(weighted, firstCoords, decimal=5) def test_weights_and_superposition_2(self, u): weights = np.zeros(len(u.trajectory[0])) weights[:100] = 1 - weighted = rms.rmsd(u.trajectory[0], u.trajectory[-1], - weights=weights, superposition=True) - firstCoords = rms.rmsd(u.trajectory[0][:100], - u.trajectory[-1][:100], - superposition=True) + weighted = rms.rmsd( + u.trajectory[0], + u.trajectory[-1], + weights=weights, + superposition=True, + ) + firstCoords = rms.rmsd( + u.trajectory[0][:100], u.trajectory[-1][:100], superposition=True + ) # very close to zero, change significant decimal places to 5 assert_almost_equal(weighted, firstCoords, decimal=5) @@ -162,7 +172,7 @@ def universe(self): @pytest.fixture() def outfile(self, tmpdir): - return os.path.join(str(tmpdir), 'rmsd.txt') + return os.path.join(str(tmpdir), "rmsd.txt") @pytest.fixture() def correct_values(self): @@ -178,13 +188,11 @@ def correct_values_mass_add_ten(self): @pytest.fixture() def correct_values_group(self): - return [[0, 1, 0, 0, 0], - [49, 50, 4.7857, 4.7048, 4.6924]] + return [[0, 1, 0, 0, 0], [49, 50, 4.7857, 4.7048, 4.6924]] @pytest.fixture() def correct_values_backbone_group(self): - return [[0, 1, 0, 0, 0], - [49, 50, 4.6997, 1.9154, 2.7139]] + return [[0, 1, 0, 0, 0], [49, 50, 4.6997, 1.9154, 2.7139]] def test_rmsd(self, universe, correct_values, client_RMSD): # client_RMSD is defined in testsuite/analysis/conftest.py @@ -192,197 +200,276 @@ def test_rmsd(self, universe, correct_values, client_RMSD): # collect all possible backends and reasonable number of workers # for a given AnalysisBase subclass, and extend the tests # to run with all of them. - RMSD = MDAnalysis.analysis.rms.RMSD(universe, select='name CA') + RMSD = MDAnalysis.analysis.rms.RMSD(universe, select="name CA") RMSD.run(step=49, **client_RMSD) - assert_almost_equal(RMSD.results.rmsd, correct_values, 4, - err_msg="error: rmsd profile should match" + - "test values") + assert_almost_equal( + RMSD.results.rmsd, + correct_values, + 4, + err_msg="error: rmsd profile should match" + "test values", + ) def test_rmsd_frames(self, universe, correct_values, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, select='name CA') + RMSD = MDAnalysis.analysis.rms.RMSD(universe, select="name CA") RMSD.run(frames=[0, 49], **client_RMSD) - assert_almost_equal(RMSD.results.rmsd, correct_values, 4, - err_msg="error: rmsd profile should match" + - "test values") - - def test_rmsd_unicode_selection(self, universe, correct_values, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, select=u'name CA') + assert_almost_equal( + RMSD.results.rmsd, + correct_values, + 4, + err_msg="error: rmsd profile should match" + "test values", + ) + + def test_rmsd_unicode_selection( + self, universe, correct_values, client_RMSD + ): + RMSD = MDAnalysis.analysis.rms.RMSD(universe, select="name CA") RMSD.run(step=49, **client_RMSD) - assert_almost_equal(RMSD.results.rmsd, correct_values, 4, - err_msg="error: rmsd profile should match" + - "test values") + assert_almost_equal( + RMSD.results.rmsd, + correct_values, + 4, + err_msg="error: rmsd profile should match" + "test values", + ) def test_rmsd_atomgroup_selections(self, universe, client_RMSD): # see Issue #1684 - R1 = MDAnalysis.analysis.rms.RMSD(universe.atoms, - select="resid 1-30").run(**client_RMSD) - R2 = MDAnalysis.analysis.rms.RMSD(universe.atoms.select_atoms("name CA"), - select="resid 1-30").run(**client_RMSD) + R1 = MDAnalysis.analysis.rms.RMSD( + universe.atoms, select="resid 1-30" + ).run(**client_RMSD) + R2 = MDAnalysis.analysis.rms.RMSD( + universe.atoms.select_atoms("name CA"), select="resid 1-30" + ).run(**client_RMSD) assert not np.allclose(R1.results.rmsd[:, 2], R2.results.rmsd[:, 2]) def test_rmsd_single_frame(self, universe, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, select='name CA', - ).run(start=5, stop=6, **client_RMSD) + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, + select="name CA", + ).run(start=5, stop=6, **client_RMSD) single_frame = [[5, 6, 0.91544906]] - assert_almost_equal(RMSD.results.rmsd, single_frame, 4, - err_msg="error: rmsd profile should match" + - "test values") + assert_almost_equal( + RMSD.results.rmsd, + single_frame, + 4, + err_msg="error: rmsd profile should match" + "test values", + ) def test_mass_weighted(self, universe, correct_values, client_RMSD): # mass weighting the CA should give the same answer as weighing # equally because all CA have the same mass - RMSD = MDAnalysis.analysis.rms.RMSD(universe, select='name CA', - weights='mass').run(step=49, **client_RMSD) + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, select="name CA", weights="mass" + ).run(step=49, **client_RMSD) - assert_almost_equal(RMSD.results.rmsd, correct_values, 4, - err_msg="error: rmsd profile should match" - "test values") + assert_almost_equal( + RMSD.results.rmsd, + correct_values, + 4, + err_msg="error: rmsd profile should match" "test values", + ) def test_custom_weighted(self, universe, correct_values_mass, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, weights="mass").run(step=49, **client_RMSD) + RMSD = MDAnalysis.analysis.rms.RMSD(universe, weights="mass").run( + step=49, **client_RMSD + ) - assert_almost_equal(RMSD.results.rmsd, correct_values_mass, 4, - err_msg="error: rmsd profile should match" - "test values") + assert_almost_equal( + RMSD.results.rmsd, + correct_values_mass, + 4, + err_msg="error: rmsd profile should match" "test values", + ) def test_weights_mass_is_mass_weighted(self, universe, client_RMSD): - RMSD_mass = MDAnalysis.analysis.rms.RMSD(universe, - weights="mass").run(step=49, **client_RMSD) - RMSD_cust = MDAnalysis.analysis.rms.RMSD(universe, - weights=universe.atoms.masses).run(step=49, **client_RMSD) - assert_almost_equal(RMSD_mass.results.rmsd, RMSD_cust.results.rmsd, 4, - err_msg="error: rmsd profiles should match for 'mass' " - "and universe.atoms.masses") - - def test_custom_weighted_list(self, universe, correct_values_mass, client_RMSD): + RMSD_mass = MDAnalysis.analysis.rms.RMSD(universe, weights="mass").run( + step=49, **client_RMSD + ) + RMSD_cust = MDAnalysis.analysis.rms.RMSD( + universe, weights=universe.atoms.masses + ).run(step=49, **client_RMSD) + assert_almost_equal( + RMSD_mass.results.rmsd, + RMSD_cust.results.rmsd, + 4, + err_msg="error: rmsd profiles should match for 'mass' " + "and universe.atoms.masses", + ) + + def test_custom_weighted_list( + self, universe, correct_values_mass, client_RMSD + ): weights = universe.atoms.masses - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - weights=list(weights)).run(step=49, **client_RMSD) - assert_almost_equal(RMSD.results.rmsd, correct_values_mass, 4, - err_msg="error: rmsd profile should match" + - "test values") - - def test_custom_groupselection_weights_applied_1D_array(self, universe, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - 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, - **client_RMSD - ) - - assert_almost_equal(RMSD.results.rmsd.T[3], RMSD.results.rmsd.T[4], 4, - err_msg="error: rmsd profile should match " - "for applied weight array and selected resid") - - def test_custom_groupselection_weights_applied_mass(self, universe, correct_values_mass, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - select='backbone', - groupselections=['all', 'all'], - weights=None, - weights_groupselections=['mass', - universe.atoms.masses]).run(step=49, - **client_RMSD - ) - - assert_almost_equal(RMSD.results.rmsd.T[3], RMSD.results.rmsd.T[4], 4, - err_msg="error: rmsd profile should match " - "between applied mass and universe.atoms.masses") + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, weights=list(weights) + ).run(step=49, **client_RMSD) + assert_almost_equal( + RMSD.results.rmsd, + correct_values_mass, + 4, + err_msg="error: rmsd profile should match" + "test values", + ) + + def test_custom_groupselection_weights_applied_1D_array( + self, universe, client_RMSD + ): + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, + 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, **client_RMSD) + + assert_almost_equal( + RMSD.results.rmsd.T[3], + RMSD.results.rmsd.T[4], + 4, + err_msg="error: rmsd profile should match " + "for applied weight array and selected resid", + ) + + def test_custom_groupselection_weights_applied_mass( + self, universe, correct_values_mass, client_RMSD + ): + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, + select="backbone", + groupselections=["all", "all"], + weights=None, + weights_groupselections=["mass", universe.atoms.masses], + ).run(step=49, **client_RMSD) + + assert_almost_equal( + RMSD.results.rmsd.T[3], + RMSD.results.rmsd.T[4], + 4, + err_msg="error: rmsd profile should match " + "between applied mass and universe.atoms.masses", + ) def test_rmsd_scalar_weights_raises_ValueError(self, universe): with pytest.raises(ValueError): - RMSD = MDAnalysis.analysis.rms.RMSD( - universe, weights=42) + RMSD = MDAnalysis.analysis.rms.RMSD(universe, weights=42) def test_rmsd_string_weights_raises_ValueError(self, universe): with pytest.raises(ValueError): - RMSD = MDAnalysis.analysis.rms.RMSD( - universe, weights="Jabberwock") + RMSD = MDAnalysis.analysis.rms.RMSD(universe, weights="Jabberwock") def test_rmsd_mismatched_weights_raises_ValueError(self, universe): with pytest.raises(ValueError): RMSD = MDAnalysis.analysis.rms.RMSD( - universe, weights=universe.atoms.masses[:-1]) + universe, weights=universe.atoms.masses[:-1] + ) - def test_rmsd_misuse_weights_for_groupselection_raises_TypeError(self, universe): + def test_rmsd_misuse_weights_for_groupselection_raises_TypeError( + self, universe + ): with pytest.raises(TypeError): RMSD = MDAnalysis.analysis.rms.RMSD( - universe, groupselections=['all'], - weights=[universe.atoms.masses, universe.atoms.masses[:-1]]) - - def test_rmsd_mismatched_weights_in_groupselection_raises_ValueError(self, universe): + universe, + groupselections=["all"], + weights=[universe.atoms.masses, universe.atoms.masses[:-1]], + ) + + def test_rmsd_mismatched_weights_in_groupselection_raises_ValueError( + self, universe + ): with pytest.raises(ValueError): RMSD = MDAnalysis.analysis.rms.RMSD( - universe, groupselections=['all'], + universe, + groupselections=["all"], weights=universe.atoms.masses, - weights_groupselections = [universe.atoms.masses[:-1]]) + weights_groupselections=[universe.atoms.masses[:-1]], + ) def test_rmsd_list_of_weights_wrong_length(self, universe): with pytest.raises(ValueError): RMSD = MDAnalysis.analysis.rms.RMSD( - universe, groupselections=['backbone', 'name CA'], - weights='mass', - weights_groupselections=[None]) - - def test_rmsd_group_selections(self, universe, correct_values_group, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - groupselections=['backbone', 'name CA'] - ).run(step=49, **client_RMSD) - assert_almost_equal(RMSD.results.rmsd, correct_values_group, 4, - err_msg="error: rmsd profile should match" - "test values") - - def test_rmsd_backbone_and_group_selection(self, universe, - correct_values_backbone_group, - client_RMSD): + universe, + groupselections=["backbone", "name CA"], + weights="mass", + weights_groupselections=[None], + ) + + def test_rmsd_group_selections( + self, universe, correct_values_group, client_RMSD + ): + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, groupselections=["backbone", "name CA"] + ).run(step=49, **client_RMSD) + assert_almost_equal( + RMSD.results.rmsd, + correct_values_group, + 4, + err_msg="error: rmsd profile should match" "test values", + ) + + def test_rmsd_backbone_and_group_selection( + self, universe, correct_values_backbone_group, client_RMSD + ): RMSD = MDAnalysis.analysis.rms.RMSD( universe, reference=universe, select="backbone", - groupselections=['backbone and resid 1:10', - 'backbone and resid 10:20']).run(step=49, **client_RMSD) + groupselections=[ + "backbone and resid 1:10", + "backbone and resid 10:20", + ], + ).run(step=49, **client_RMSD) assert_almost_equal( - RMSD.results.rmsd, correct_values_backbone_group, 4, - err_msg="error: rmsd profile should match test values") + RMSD.results.rmsd, + correct_values_backbone_group, + 4, + err_msg="error: rmsd profile should match test values", + ) def test_ref_length_unequal_len(self, universe): reference = MDAnalysis.Universe(PSF, DCD) reference.atoms = reference.atoms[:-1] with pytest.raises(SelectionError): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - reference=reference) + RMSD = MDAnalysis.analysis.rms.RMSD(universe, reference=reference) def test_mass_mismatches(self, universe): reference = MDAnalysis.Universe(PSF, DCD) reference.atoms.masses = 10 with pytest.raises(SelectionError): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - reference=reference) + RMSD = MDAnalysis.analysis.rms.RMSD(universe, reference=reference) - def test_ref_mobile_mass_mismapped(self, universe,correct_values_mass_add_ten, client_RMSD): + def test_ref_mobile_mass_mismapped( + self, universe, correct_values_mass_add_ten, client_RMSD + ): reference = MDAnalysis.Universe(PSF, DCD) universe.atoms.masses = universe.atoms.masses + 10 - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - reference=reference, - select='all', - weights='mass', - tol_mass=100) + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, + reference=reference, + select="all", + weights="mass", + tol_mass=100, + ) RMSD.run(step=49, **client_RMSD) - assert_almost_equal(RMSD.results.rmsd, correct_values_mass_add_ten, 4, - err_msg="error: rmsd profile should match " - "between true values and calculated values") + assert_almost_equal( + RMSD.results.rmsd, + correct_values_mass_add_ten, + 4, + err_msg="error: rmsd profile should match " + "between true values and calculated values", + ) def test_group_selections_unequal_len(self, universe): reference = MDAnalysis.Universe(PSF, DCD) - reference.atoms[0].residue.resname = 'NOTMET' + reference.atoms[0].residue.resname = "NOTMET" with pytest.raises(SelectionError): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, - reference=reference, - groupselections=['resname MET', 'type NH3']) + RMSD = MDAnalysis.analysis.rms.RMSD( + universe, + reference=reference, + groupselections=["resname MET", "type NH3"], + ) def test_rmsd_attr_warning(self, universe, client_RMSD): - RMSD = MDAnalysis.analysis.rms.RMSD( - universe, select='name CA').run(stop=2, **client_RMSD) + RMSD = MDAnalysis.analysis.rms.RMSD(universe, select="name CA").run( + stop=2, **client_RMSD + ) wmsg = "The `rmsd` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): @@ -395,23 +482,29 @@ def universe(self): return mda.Universe(GRO, XTC) def test_rmsf(self, universe, client_RMSF): - rmsfs = rms.RMSF(universe.select_atoms('name CA')) + rmsfs = rms.RMSF(universe.select_atoms("name CA")) rmsfs.run(**client_RMSF) test_rmsfs = np.load(rmsfArray) - assert_almost_equal(rmsfs.results.rmsf, test_rmsfs, 5, - err_msg="error: rmsf profile should match test " - "values") + assert_almost_equal( + rmsfs.results.rmsf, + test_rmsfs, + 5, + err_msg="error: rmsf profile should match test " "values", + ) def test_rmsf_single_frame(self, universe, client_RMSF): - rmsfs = rms.RMSF(universe.select_atoms('name CA')).run(start=5, stop=6, **client_RMSF) + rmsfs = rms.RMSF(universe.select_atoms("name CA")).run( + start=5, stop=6, **client_RMSF + ) - assert_almost_equal(rmsfs.results.rmsf, 0, 5, - err_msg="error: rmsfs should all be zero") + assert_almost_equal( + rmsfs.results.rmsf, 0, 5, err_msg="error: rmsfs should all be zero" + ) def test_rmsf_identical_frames(self, universe, tmpdir, client_RMSF): - outfile = os.path.join(str(tmpdir), 'rmsf.xtc') + outfile = os.path.join(str(tmpdir), "rmsf.xtc") # write a dummy trajectory of all the same frame with mda.Writer(outfile, universe.atoms.n_atoms) as W: @@ -419,13 +512,16 @@ def test_rmsf_identical_frames(self, universe, tmpdir, client_RMSF): W.write(universe) universe = mda.Universe(GRO, outfile) - rmsfs = rms.RMSF(universe.select_atoms('name CA')) + rmsfs = rms.RMSF(universe.select_atoms("name CA")) rmsfs.run(**client_RMSF) - assert_almost_equal(rmsfs.results.rmsf, 0, 5, - err_msg="error: rmsfs should all be 0") + assert_almost_equal( + rmsfs.results.rmsf, 0, 5, err_msg="error: rmsfs should all be 0" + ) def test_rmsf_attr_warning(self, universe, client_RMSF): - rmsfs = rms.RMSF(universe.select_atoms('name CA')).run(stop=2, **client_RMSF) + rmsfs = rms.RMSF(universe.select_atoms("name CA")).run( + stop=2, **client_RMSF + ) wmsg = "The `rmsf` attribute was deprecated in MDAnalysis 2.0.0" with pytest.warns(DeprecationWarning, match=wmsg): @@ -437,7 +533,7 @@ def test_rmsf_attr_warning(self, universe, client_RMSF): [ (MDAnalysis.analysis.rms.RMSD, True), (MDAnalysis.analysis.rms.RMSF, False), - ] + ], ) def test_class_is_parallelizable(classname, is_parallelizable): assert classname._analysis_algorithm_is_parallelizable == is_parallelizable @@ -446,9 +542,16 @@ def test_class_is_parallelizable(classname, is_parallelizable): @pytest.mark.parametrize( "classname,backends", [ - (MDAnalysis.analysis.rms.RMSD, ('serial', 'multiprocessing', 'dask',)), - (MDAnalysis.analysis.rms.RMSF, ('serial',)), - ] + ( + MDAnalysis.analysis.rms.RMSD, + ( + "serial", + "multiprocessing", + "dask", + ), + ), + (MDAnalysis.analysis.rms.RMSF, ("serial",)), + ], ) def test_supported_backends(classname, backends): assert classname.get_supported_backends() == backends diff --git a/testsuite/MDAnalysisTests/analysis/test_wbridge.py b/testsuite/MDAnalysisTests/analysis/test_wbridge.py index 39fe372409c..2b53d2f1d35 100644 --- a/testsuite/MDAnalysisTests/analysis/test_wbridge.py +++ b/testsuite/MDAnalysisTests/analysis/test_wbridge.py @@ -2,157 +2,160 @@ from collections import defaultdict from numpy.testing import ( - assert_equal, assert_array_equal,) + assert_equal, + assert_array_equal, +) import pytest import MDAnalysis from MDAnalysis.analysis.hydrogenbonds.wbridge_analysis import ( - WaterBridgeAnalysis, ) + WaterBridgeAnalysis, +) class TestWaterBridgeAnalysis(object): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_empty(): - '''A universe with no hydrogen bonds''' - grofile = '''Test gro file + """A universe with no hydrogen bonds""" + grofile = """Test gro file 5 1ALA N 1 0.000 0.000 0.000 1ALA H 2 0.100 0.000 0.000 2SOL OW 3 3.000 0.000 0.000 4ALA H 4 0.500 0.000 0.000 4ALA N 5 0.600 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_DA(): - '''A universe with one hydrogen bond acceptor bonding to a hydrogen bond - donor''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to a hydrogen bond + donor""" + grofile = """Test gro file 3 1ALA N 1 0.000 0.000 0.000 1ALA H 2 0.100 0.000 0.000 4ALA O 3 0.300 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_DA_PBC(): - '''A universe with one hydrogen bond acceptor bonding to a hydrogen bond - donor but in a PBC condition''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to a hydrogen bond + donor but in a PBC condition""" + grofile = """Test gro file 3 1ALA N 1 0.800 0.000 0.000 1ALA H 2 0.900 0.000 0.000 4ALA O 3 0.100 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_AD(): - '''A universe with one hydrogen bond donor bonding to a hydrogen bond - acceptor''' - grofile = '''Test gro file + """A universe with one hydrogen bond donor bonding to a hydrogen bond + acceptor""" + grofile = """Test gro file 3 1ALA O 1 0.000 0.000 0.000 4ALA H 2 0.200 0.000 0.000 4ALA N 3 0.300 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_loop(): - '''A universe with one hydrogen bond acceptor bonding to a water which - bonds back to the first hydrogen bond acceptor and thus form a loop''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to a water which + bonds back to the first hydrogen bond acceptor and thus form a loop""" + grofile = """Test gro file 5 1ALA O 1 0.000 0.001 0.000 2SOL OW 2 0.300 0.001 0.000 2SOL HW1 3 0.200 0.002 0.000 2SOL HW2 4 0.200 0.000 0.000 4ALA O 5 0.600 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_DWA(): - '''A universe with one hydrogen bond donor bonding to a hydrogen bond - acceptor through a water''' - grofile = '''Test gro file + """A universe with one hydrogen bond donor bonding to a hydrogen bond + acceptor through a water""" + grofile = """Test gro file 5 1ALA N 1 0.000 0.000 0.000 1ALA H 2 0.100 0.000 0.000 2SOL OW 3 0.300 0.000 0.000 2SOL HW2 4 0.400 0.000 0.000 4ALA O 5 0.600 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_DWD(): - '''A universe with one hydrogen bond donor bonding to a hydrogen bond - donor through a water''' - grofile = '''Test gro file + """A universe with one hydrogen bond donor bonding to a hydrogen bond + donor through a water""" + grofile = """Test gro file 5 1ALA N 1 0.000 0.000 0.000 1ALA H 2 0.100 0.000 0.000 2SOL OW 3 0.300 0.000 0.000 4ALA H 4 0.500 0.000 0.000 4ALA N 5 0.600 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_AWA(): - '''A universe with two hydrogen bond acceptor are joined by a water''' - grofile = '''Test gro file + """A universe with two hydrogen bond acceptor are joined by a water""" + grofile = """Test gro file 5 1ALA O 1 0.000 0.000 0.000 2SOL OW 2 0.300 0.000 0.000 2SOL HW1 3 0.200 0.000 0.000 2SOL HW2 4 0.400 0.000 0.000 4ALA O 5 0.600 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_AWD(): - '''A universe with one hydrogen bond acceptor bonding to a hydrogen - bond donor through a water''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to a hydrogen + bond donor through a water""" + grofile = """Test gro file 5 1ALA O 1 0.000 0.000 0.000 2SOL OW 2 0.300 0.000 0.000 2SOL HW1 3 0.200 0.000 0.000 4ALA H 4 0.500 0.000 0.000 4ALA N 5 0.600 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_AWWA(): - '''A universe with one hydrogen bond acceptor bonding to a hydrogen bond - acceptor through two waters''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to a hydrogen bond + acceptor through two waters""" + grofile = """Test gro file 7 1ALA O 1 0.000 0.000 0.000 2SOL OW 2 0.300 0.000 0.000 @@ -161,16 +164,16 @@ def universe_AWWA(): 3SOL OW 5 0.600 0.000 0.000 3SOL HW1 6 0.700 0.000 0.000 4ALA O 7 0.900 0.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_AWWWA(): - '''A universe with one hydrogen bond acceptor bonding to a hydrogen bond - acceptor through three waters''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to a hydrogen bond + acceptor through three waters""" + grofile = """Test gro file 9 1ALA O 1 0.000 0.000 0.000 2SOL OW 2 0.300 0.000 0.000 @@ -181,16 +184,16 @@ def universe_AWWWA(): 4SOL OW 7 0.900 0.000 0.000 4SOL HW1 8 1.000 0.000 0.000 5ALA O 9 1.200 0.000 0.000 - 10.0 10.0 10.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 10.0 10.0 10.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_AWWWWA(): - '''A universe with one hydrogen bond acceptor bonding to a hydrogen bond - acceptor through three waters''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to a hydrogen bond + acceptor through three waters""" + grofile = """Test gro file 11 1ALA O 1 0.000 0.000 0.000 2SOL OW 2 0.300 0.000 0.000 @@ -203,16 +206,16 @@ def universe_AWWWWA(): 5SOL OW 9 1.200 0.000 0.000 5SOL HW1 10 1.300 0.000 0.000 6ALA O 11 1.400 0.000 0.000 - 10.0 10.0 10.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 10.0 10.0 10.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_branch(): - '''A universe with one hydrogen bond acceptor bonding to two hydrogen - bond acceptor in selection 2''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptor bonding to two hydrogen + bond acceptor in selection 2""" + grofile = """Test gro file 9 1ALA O 1 0.000 0.000 0.000 2SOL OW 2 0.300 0.000 0.000 @@ -223,16 +226,16 @@ def universe_branch(): 3SOL HW2 7 0.600 0.100 0.000 4ALA O 8 0.900 0.000 0.000 5ALA O 9 0.600 0.300 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe_AWA_AWWA(): - '''A universe with one hydrogen bond acceptors are bonded through one or - two water''' - grofile = '''Test gro file + """A universe with one hydrogen bond acceptors are bonded through one or + two water""" + grofile = """Test gro file 12 1ALA O 1 0.000 0.000 0.000 2SOL OW 2 0.300 0.000 0.000 @@ -246,15 +249,15 @@ def universe_AWA_AWWA(): 7SOL OW 10 0.600 1.000 0.000 7SOL HW1 11 0.700 1.000 0.000 8ALA O 12 0.900 1.000 0.000 - 1.0 1.0 1.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') + 1.0 1.0 1.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") return u @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def wb_multiframe(): - '''A water bridge object with multipley frames''' - grofile = '''Test gro file + """A water bridge object with multipley frames""" + grofile = """Test gro file 13 1ALA O 1 0.000 0.000 0.000 1ALA H 2 0.000 0.000 0.000 @@ -269,113 +272,164 @@ def wb_multiframe(): 5SOL HW1 11 1.300 0.000 0.000 6ALA H 12 1.400 0.000 0.000 6ALA O 13 1.400 0.000 0.000 - 10.0 10.0 10.0''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') - wb = WaterBridgeAnalysis(u, 'protein and (resid 1)', 'protein and (resid 4)', - order=4) + 10.0 10.0 10.0""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") + wb = WaterBridgeAnalysis( + u, "protein and (resid 1)", "protein and (resid 4)", order=4 + ) # Build an dummy WaterBridgeAnalysis object for testing wb.results.network = [] wb.results.network.append({(1, 0, 12, None, 2.0, 180.0): None}) wb.results.network.append({(0, None, 12, 13, 2.0, 180.0): None}) - wb.results.network.append({(1, 0, 3, None, 2.0, 180.0): - {(4, 2, 12, None, 2.0, 180.0): None}}) - wb.results.network.append({(0, None, 3, 2, 2.0, 180.0): - {(4, 2, 5, None, 2.0, 180.0): - {(5, None, 11, 12, 2.0, 180.0): None}}}) + wb.results.network.append( + {(1, 0, 3, None, 2.0, 180.0): {(4, 2, 12, None, 2.0, 180.0): None}} + ) + wb.results.network.append( + { + (0, None, 3, 2, 2.0, 180.0): { + (4, 2, 5, None, 2.0, 180.0): { + (5, None, 11, 12, 2.0, 180.0): None + } + } + } + ) wb.timesteps = range(len(wb.results.network)) return wb def test_nodata(self, universe_DA): - '''Test if the funtions can run when there is no data. - This is achieved by not runing the run() first.''' - wb = WaterBridgeAnalysis(universe_DA, 'protein and (resid 1)', - 'protein and (resid 4)', order=0) + """Test if the funtions can run when there is no data. + This is achieved by not runing the run() first.""" + wb = WaterBridgeAnalysis( + universe_DA, + "protein and (resid 1)", + "protein and (resid 4)", + order=0, + ) wb.generate_table() assert_equal(wb.timesteps_by_type(), None) assert_equal(wb.count_by_time(), None) assert_equal(wb.count_by_type(), None) def test_selection_type_error(self, universe_DA): - '''Test the case when the wrong selection1_type is given''' + """Test the case when the wrong selection1_type is given""" try: - wb = WaterBridgeAnalysis(universe_DA, 'protein and (resid 1)', - 'protein and (resid 4)', order=0, selection1_type='aaa') + wb = WaterBridgeAnalysis( + universe_DA, + "protein and (resid 1)", + "protein and (resid 4)", + order=0, + selection1_type="aaa", + ) except ValueError: pass else: raise pytest.fail("selection_type aaa should rasie error") def test_distance_type_error(self, universe_DA): - '''Test the case when the wrong selection1_type is given''' - with pytest.raises(ValueError, match="Only 'hydrogen' and 'heavy' are allowed for option `distance_type'"): - WaterBridgeAnalysis(universe_DA, 'protein and (resid 1)', - 'protein and (resid 4)', order=0, - selection1_type='aaa', distance_type='aaa') + """Test the case when the wrong selection1_type is given""" + with pytest.raises( + ValueError, + match="Only 'hydrogen' and 'heavy' are allowed for option `distance_type'", + ): + WaterBridgeAnalysis( + universe_DA, + "protein and (resid 1)", + "protein and (resid 4)", + order=0, + selection1_type="aaa", + distance_type="aaa", + ) def test_selection2_type_error(self, universe_DA): - '''Test the case when the wrong selection1_type is given''' - with pytest.raises(ValueError, match="`selection2_type` is not a keyword argument."): - WaterBridgeAnalysis(universe_DA, 'protein and (resid 1)', - 'protein and (resid 4)', order=0, - selection1_type='aaa', selection2_type='aaa') - + """Test the case when the wrong selection1_type is given""" + with pytest.raises( + ValueError, match="`selection2_type` is not a keyword argument." + ): + WaterBridgeAnalysis( + universe_DA, + "protein and (resid 1)", + "protein and (resid 4)", + order=0, + selection1_type="aaa", + selection2_type="aaa", + ) def test_empty_selection(self, universe_DA): - '''Test the case when selection yields empty result''' - wb = WaterBridgeAnalysis(universe_DA, 'protein and (resid 9)', - 'protein and (resid 10)', order=0) + """Test the case when selection yields empty result""" + wb = WaterBridgeAnalysis( + universe_DA, + "protein and (resid 9)", + "protein and (resid 10)", + order=0, + ) wb.run() assert wb.results.network == [{}] def test_loop(self, universe_loop): - '''Test if loop can be handled correctly''' - wb = WaterBridgeAnalysis(universe_loop, 'protein and (resid 1)', - 'protein and (resid 1 or resid 4)') + """Test if loop can be handled correctly""" + wb = WaterBridgeAnalysis( + universe_loop, + "protein and (resid 1)", + "protein and (resid 1 or resid 4)", + ) wb.run() assert_equal(len(wb.results.network[0].keys()), 2) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_donor_accepter(self, universe_DA, distance_type): - '''Test zeroth order donor to acceptor hydrogen bonding''' - wb = WaterBridgeAnalysis(universe_DA, 'protein and (resid 1)', - 'protein and (resid 4)', - order=0, - update_selection=True, - debug=True, - distance_type=distance_type) + """Test zeroth order donor to acceptor hydrogen bonding""" + wb = WaterBridgeAnalysis( + universe_DA, + "protein and (resid 1)", + "protein and (resid 4)", + order=0, + update_selection=True, + debug=True, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (1, 0, 2, None)) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_donor_accepter_pbc(self, universe_DA_PBC, distance_type): - '''Test zeroth order donor to acceptor hydrogen bonding in PBC conditions''' - wb = WaterBridgeAnalysis(universe_DA_PBC, - 'protein and (resid 1)', - 'protein and (resid 4)', - order=0, - pbc=True, - distance_type=distance_type) + """Test zeroth order donor to acceptor hydrogen bonding in PBC conditions""" + wb = WaterBridgeAnalysis( + universe_DA_PBC, + "protein and (resid 1)", + "protein and (resid 4)", + order=0, + pbc=True, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (1, 0, 2, None)) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_accepter_donor(self, universe_AD, distance_type): - '''Test zeroth order acceptor to donor hydrogen bonding''' - wb = WaterBridgeAnalysis(universe_AD, 'protein and (resid 1)', - 'protein and (resid 4)', order=0, - distance_type=distance_type) + """Test zeroth order acceptor to donor hydrogen bonding""" + wb = WaterBridgeAnalysis( + universe_AD, + "protein and (resid 1)", + "protein and (resid 4)", + order=0, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 1, 2)) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_acceptor_water_accepter(self, universe_AWA, distance_type): - '''Test case where the hydrogen bond acceptor from selection 1 form - water bridge with hydrogen bond acceptor from selection 2''' - wb = WaterBridgeAnalysis(universe_AWA, 'protein and (resid 1)', - 'protein and (resid 4)', distance_type=distance_type) + """Test case where the hydrogen bond acceptor from selection 1 form + water bridge with hydrogen bond acceptor from selection 2""" + wb = WaterBridgeAnalysis( + universe_AWA, + "protein and (resid 1)", + "protein and (resid 4)", + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -383,12 +437,16 @@ def test_acceptor_water_accepter(self, universe_AWA, distance_type): assert_equal(list(second.keys())[0][:4], (3, 1, 4, None)) assert_equal(second[list(second.keys())[0]], None) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_donor_water_accepter(self, universe_DWA, distance_type): - '''Test case where the hydrogen bond donor from selection 1 form - water bridge with hydrogen bond acceptor from selection 2''' - wb = WaterBridgeAnalysis(universe_DWA, 'protein and (resid 1)', - 'protein and (resid 4)', distance_type=distance_type) + """Test case where the hydrogen bond donor from selection 1 form + water bridge with hydrogen bond acceptor from selection 2""" + wb = WaterBridgeAnalysis( + universe_DWA, + "protein and (resid 1)", + "protein and (resid 4)", + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (1, 0, 2, None)) @@ -396,12 +454,16 @@ def test_donor_water_accepter(self, universe_DWA, distance_type): assert_equal(list(second.keys())[0][:4], (3, 2, 4, None)) assert_equal(second[list(second.keys())[0]], None) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_acceptor_water_donor(self, universe_AWD, distance_type): - '''Test case where the hydrogen bond acceptor from selection 1 form - water bridge with hydrogen bond donor from selection 2''' - wb = WaterBridgeAnalysis(universe_AWD, 'protein and (resid 1)', - 'protein and (resid 4)', distance_type=distance_type) + """Test case where the hydrogen bond acceptor from selection 1 form + water bridge with hydrogen bond donor from selection 2""" + wb = WaterBridgeAnalysis( + universe_AWD, + "protein and (resid 1)", + "protein and (resid 4)", + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -409,12 +471,16 @@ def test_acceptor_water_donor(self, universe_AWD, distance_type): assert_equal(list(second.keys())[0][:4], (1, None, 3, 4)) assert_equal(second[list(second.keys())[0]], None) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_donor_water_donor(self, universe_DWD, distance_type): - '''Test case where the hydrogen bond donor from selection 1 form - water bridge with hydrogen bond donor from selection 2''' - wb = WaterBridgeAnalysis(universe_DWD, 'protein and (resid 1)', - 'protein and (resid 4)', distance_type=distance_type) + """Test case where the hydrogen bond donor from selection 1 form + water bridge with hydrogen bond donor from selection 2""" + wb = WaterBridgeAnalysis( + universe_DWD, + "protein and (resid 1)", + "protein and (resid 4)", + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (1, 0, 2, None)) @@ -423,38 +489,44 @@ def test_donor_water_donor(self, universe_DWD, distance_type): assert_equal(second[list(second.keys())[0]], None) def test_empty(self, universe_empty): - '''Test case where no water bridge exists''' - wb = WaterBridgeAnalysis(universe_empty, 'protein', 'protein') + """Test case where no water bridge exists""" + wb = WaterBridgeAnalysis(universe_empty, "protein", "protein") wb.run(verbose=False) assert_equal(wb.results.network[0], defaultdict(dict)) def test_same_selection(self, universe_DWA): - ''' + """ This test tests that if the selection 1 and selection 2 are both protein. However, the protein only forms one hydrogen bond with the water. This entry won't be included. - ''' - wb = WaterBridgeAnalysis(universe_DWA, 'protein and resid 1', - 'protein and resid 1') + """ + wb = WaterBridgeAnalysis( + universe_DWA, "protein and resid 1", "protein and resid 1" + ) wb.run(verbose=False) assert_equal(wb.results.network[0], defaultdict(dict)) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_acceptor_2water_accepter(self, universe_AWWA, distance_type): - '''Test case where the hydrogen bond acceptor from selection 1 form second order - water bridge with hydrogen bond acceptor from selection 2''' + """Test case where the hydrogen bond acceptor from selection 1 form second order + water bridge with hydrogen bond acceptor from selection 2""" # test first order - wb = WaterBridgeAnalysis(universe_AWWA, 'protein and (resid 1)', - 'protein and (resid 4)', - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWWA, + "protein and (resid 1)", + "protein and (resid 4)", + distance_type=distance_type, + ) wb.run(verbose=False) assert_equal(wb.results.network[0], defaultdict(dict)) # test second order - wb = WaterBridgeAnalysis(universe_AWWA, - 'protein and (resid 1)', - 'protein and (resid 4)', - order=2, - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWWA, + "protein and (resid 1)", + "protein and (resid 4)", + order=2, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -464,9 +536,13 @@ def test_acceptor_2water_accepter(self, universe_AWWA, distance_type): assert_equal(list(third.keys())[0][:4], (5, 4, 6, None)) assert_equal(third[list(third.keys())[0]], None) # test third order - wb = WaterBridgeAnalysis(universe_AWWA, 'protein and (resid 1)', - 'protein and (resid 4)', order=3, - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWWA, + "protein and (resid 1)", + "protein and (resid 4)", + order=3, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -476,19 +552,27 @@ def test_acceptor_2water_accepter(self, universe_AWWA, distance_type): assert_equal(list(third.keys())[0][:4], (5, 4, 6, None)) assert_equal(third[list(third.keys())[0]], None) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_acceptor_3water_accepter(self, universe_AWWWA, distance_type): - '''Test case where the hydrogen bond acceptor from selection 1 form third order - water bridge with hydrogen bond acceptor from selection 2''' - wb = WaterBridgeAnalysis(universe_AWWWA, 'protein and (resid 1)', - 'protein and (resid 5)', order=2, - distance_type=distance_type) + """Test case where the hydrogen bond acceptor from selection 1 form third order + water bridge with hydrogen bond acceptor from selection 2""" + wb = WaterBridgeAnalysis( + universe_AWWWA, + "protein and (resid 1)", + "protein and (resid 5)", + order=2, + distance_type=distance_type, + ) wb.run(verbose=False) assert_equal(wb.results.network[0], defaultdict(dict)) - wb = WaterBridgeAnalysis(universe_AWWWA, 'protein and (resid 1)', - 'protein and (resid 5)', order=3, - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWWWA, + "protein and (resid 1)", + "protein and (resid 5)", + order=3, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -500,9 +584,13 @@ def test_acceptor_3water_accepter(self, universe_AWWWA, distance_type): assert_equal(list(fourth.keys())[0][:4], (7, 6, 8, None)) assert_equal(fourth[list(fourth.keys())[0]], None) - wb = WaterBridgeAnalysis(universe_AWWWA, 'protein and (resid 1)', - 'protein and (resid 5)', order=4, - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWWWA, + "protein and (resid 1)", + "protein and (resid 5)", + order=4, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -514,19 +602,27 @@ def test_acceptor_3water_accepter(self, universe_AWWWA, distance_type): assert_equal(list(fourth.keys())[0][:4], (7, 6, 8, None)) assert_equal(fourth[list(fourth.keys())[0]], None) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_acceptor_4water_accepter(self, universe_AWWWWA, distance_type): - '''Test case where the hydrogen bond acceptor from selection 1 form fourth order - water bridge with hydrogen bond acceptor from selection 2''' - wb = WaterBridgeAnalysis(universe_AWWWWA, 'protein and (resid 1)', - 'protein and (resid 6)', order=3, - distance_type=distance_type) + """Test case where the hydrogen bond acceptor from selection 1 form fourth order + water bridge with hydrogen bond acceptor from selection 2""" + wb = WaterBridgeAnalysis( + universe_AWWWWA, + "protein and (resid 1)", + "protein and (resid 6)", + order=3, + distance_type=distance_type, + ) wb.run(verbose=False) assert_equal(wb.results.network[0], defaultdict(dict)) - wb = WaterBridgeAnalysis(universe_AWWWWA, 'protein and (resid 1)', - 'protein and (resid 6)', order=4, - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWWWWA, + "protein and (resid 1)", + "protein and (resid 6)", + order=4, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -540,9 +636,13 @@ def test_acceptor_4water_accepter(self, universe_AWWWWA, distance_type): assert_equal(list(fifth.keys())[0][:4], (9, 8, 10, None)) assert_equal(fifth[list(fifth.keys())[0]], None) - wb = WaterBridgeAnalysis(universe_AWWWWA, 'protein and (resid 1)', - 'protein and (resid 6)', order=5, - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWWWWA, + "protein and (resid 1)", + "protein and (resid 6)", + order=5, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -556,55 +656,89 @@ def test_acceptor_4water_accepter(self, universe_AWWWWA, distance_type): assert_equal(list(fifth.keys())[0][:4], (9, 8, 10, None)) assert_equal(fifth[list(fifth.keys())[0]], None) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_acceptor_22water_accepter(self, universe_branch, distance_type): - '''Test case where the hydrogen bond acceptor from selection 1 form a second order + """Test case where the hydrogen bond acceptor from selection 1 form a second order water bridge with hydrogen bond acceptor from selection 2 - and the last water is linked to two residues in selection 2''' - wb = WaterBridgeAnalysis(universe_branch, 'protein and (resid 1)', - 'protein and (resid 4 or resid 5)', order=2, - distance_type=distance_type) + and the last water is linked to two residues in selection 2""" + wb = WaterBridgeAnalysis( + universe_branch, + "protein and (resid 1)", + "protein and (resid 4 or resid 5)", + order=2, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) second = network[list(network.keys())[0]] assert_equal(list(second.keys())[0][:4], (3, 1, 4, None)) third = second[list(second.keys())[0]] - assert_equal([(5, 4, 7, None), (6, 4, 8, None)], - sorted([key[:4] for key in list(third.keys())])) + assert_equal( + [(5, 4, 7, None), (6, 4, 8, None)], + sorted([key[:4] for key in list(third.keys())]), + ) def test_timeseries_wba(self, universe_branch): - '''Test if the time series data is correctly generated in water bridge analysis format''' - wb = WaterBridgeAnalysis(universe_branch, 'protein and (resid 1)', - 'protein and (resid 4 or resid 5)', order=2) - wb.output_format = 'sele1_sele2' + """Test if the time series data is correctly generated in water bridge analysis format""" + wb = WaterBridgeAnalysis( + universe_branch, + "protein and (resid 1)", + "protein and (resid 4 or resid 5)", + order=2, + ) + wb.output_format = "sele1_sele2" wb.run(verbose=False) timeseries = sorted(wb.results.timeseries[0]) - assert_equal(timeseries[0][:4], (0, 2, ('ALA', 1, 'O'), ('SOL', 2, 'HW1'))) - assert_equal(timeseries[1][:4], (3, 4, ('SOL', 2, 'HW2'), ('SOL', 3, 'OW'))) - assert_equal(timeseries[2][:4], (5, 7, ('SOL', 3, 'HW1'), ('ALA', 4, 'O'))) - assert_equal(timeseries[3][:4], (6, 8, ('SOL', 3, 'HW2'), ('ALA', 5, 'O'))) + assert_equal( + timeseries[0][:4], (0, 2, ("ALA", 1, "O"), ("SOL", 2, "HW1")) + ) + assert_equal( + timeseries[1][:4], (3, 4, ("SOL", 2, "HW2"), ("SOL", 3, "OW")) + ) + assert_equal( + timeseries[2][:4], (5, 7, ("SOL", 3, "HW1"), ("ALA", 4, "O")) + ) + assert_equal( + timeseries[3][:4], (6, 8, ("SOL", 3, "HW2"), ("ALA", 5, "O")) + ) def test_timeseries_hba(self, universe_branch): - '''Test if the time series data is correctly generated in hydrogen bond analysis format''' - wb = WaterBridgeAnalysis(universe_branch, 'protein and (resid 1)', - 'protein and (resid 4 or resid 5)', order=2) - wb.output_format = 'donor_acceptor' + """Test if the time series data is correctly generated in hydrogen bond analysis format""" + wb = WaterBridgeAnalysis( + universe_branch, + "protein and (resid 1)", + "protein and (resid 4 or resid 5)", + order=2, + ) + wb.output_format = "donor_acceptor" wb.run(verbose=False) timeseries = sorted(wb.results.timeseries[0]) - assert_equal(timeseries[0][:4], (2, 0, ('SOL', 2, 'HW1'), ('ALA', 1, 'O'))) - assert_equal(timeseries[1][:4], (3, 4, ('SOL', 2, 'HW2'), ('SOL', 3, 'OW'))) - assert_equal(timeseries[2][:4], (5, 7, ('SOL', 3, 'HW1'), ('ALA', 4, 'O'))) - assert_equal(timeseries[3][:4], (6, 8, ('SOL', 3, 'HW2'), ('ALA', 5, 'O'))) + assert_equal( + timeseries[0][:4], (2, 0, ("SOL", 2, "HW1"), ("ALA", 1, "O")) + ) + assert_equal( + timeseries[1][:4], (3, 4, ("SOL", 2, "HW2"), ("SOL", 3, "OW")) + ) + assert_equal( + timeseries[2][:4], (5, 7, ("SOL", 3, "HW1"), ("ALA", 4, "O")) + ) + assert_equal( + timeseries[3][:4], (6, 8, ("SOL", 3, "HW2"), ("ALA", 5, "O")) + ) - @pytest.mark.parametrize('distance_type', ["hydrogen", "heavy"]) + @pytest.mark.parametrize("distance_type", ["hydrogen", "heavy"]) def test_acceptor_12water_accepter(self, universe_AWA_AWWA, distance_type): - '''Test of independent first order and second can be recognised correctely''' - wb = WaterBridgeAnalysis(universe_AWA_AWWA, 'protein and (resid 1 or resid 5)', - 'protein and (resid 4 or resid 8)', order=1, - distance_type=distance_type) + """Test of independent first order and second can be recognised correctely""" + wb = WaterBridgeAnalysis( + universe_AWA_AWWA, + "protein and (resid 1 or resid 5)", + "protein and (resid 4 or resid 8)", + order=1, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] assert_equal(list(network.keys())[0][:4], (0, None, 2, 1)) @@ -612,174 +746,294 @@ def test_acceptor_12water_accepter(self, universe_AWA_AWWA, distance_type): assert_equal(list(second.keys())[0][:4], (3, 1, 4, None)) assert_equal(second[list(second.keys())[0]], None) network = wb.results.network[0] - wb = WaterBridgeAnalysis(universe_AWA_AWWA, 'protein and (resid 1 or resid 5)', - 'protein and (resid 4 or resid 8)', order=2, - distance_type=distance_type) + wb = WaterBridgeAnalysis( + universe_AWA_AWWA, + "protein and (resid 1 or resid 5)", + "protein and (resid 4 or resid 8)", + order=2, + distance_type=distance_type, + ) wb.run(verbose=False) network = wb.results.network[0] - assert_equal([(0, None, 2, 1), (5, None, 7, 6)], - sorted([key[:4] for key in list(network.keys())])) + assert_equal( + [(0, None, 2, 1), (5, None, 7, 6)], + sorted([key[:4] for key in list(network.keys())]), + ) def test_count_by_type_single_link(self, universe_DWA): - ''' + """ This test tests the simplest water bridge to see if count_by_type() works. - ''' - wb = WaterBridgeAnalysis(universe_DWA, 'protein and (resid 1)', - 'protein and (resid 4)') + """ + wb = WaterBridgeAnalysis( + universe_DWA, "protein and (resid 1)", "protein and (resid 4)" + ) wb.run(verbose=False) - assert_equal(wb.count_by_type(), [(1, 4, 'ALA', 1, 'H', 'ALA', 4, 'O', 1.)]) + assert_equal( + wb.count_by_type(), [(1, 4, "ALA", 1, "H", "ALA", 4, "O", 1.0)] + ) def test_count_by_type_multiple_link(self, universe_AWA_AWWA): - ''' + """ This test tests if count_by_type() can give the correct result for more than 1 links. - ''' - wb = WaterBridgeAnalysis(universe_AWA_AWWA, 'protein and (resid 1 or resid 5)', - 'protein and (resid 4 or resid 8)', order=2) + """ + wb = WaterBridgeAnalysis( + universe_AWA_AWWA, + "protein and (resid 1 or resid 5)", + "protein and (resid 4 or resid 8)", + order=2, + ) wb.run(verbose=False) - assert_equal(sorted(wb.count_by_type()), - [[0, 4, 'ALA', 1, 'O', 'ALA', 4, 'O', 1.0], - [5, 11, 'ALA', 5, 'O', 'ALA', 8, 'O', 1.0]]) - + assert_equal( + sorted(wb.count_by_type()), + [ + [0, 4, "ALA", 1, "O", "ALA", 4, "O", 1.0], + [5, 11, "ALA", 5, "O", "ALA", 8, "O", 1.0], + ], + ) def test_count_by_type_multiple_frame(self, wb_multiframe): - ''' + """ This test tests if count_by_type() works in multiply situations. :return: - ''' - result = [[0, 11, 'ALA', 1, 'O', 'ALA', 6, 'H', 0.25], - [0, 12, 'ALA', 1, 'O', 'ALA', 6, 'O', 0.25], - [1, 12, 'ALA', 1, 'H', 'ALA', 6, 'O', 0.5]] + """ + result = [ + [0, 11, "ALA", 1, "O", "ALA", 6, "H", 0.25], + [0, 12, "ALA", 1, "O", "ALA", 6, "O", 0.25], + [1, 12, "ALA", 1, "H", "ALA", 6, "O", 0.5], + ] assert_equal(sorted(wb_multiframe.count_by_type()), result) def test_count_by_type_filter(self, wb_multiframe): - ''' + """ This test tests if modifying analysis_func allows some results to be filtered out in count_by_type(). :return: - ''' + """ + def analysis(current, output, u): - sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = current[0] - atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = current[-1] + sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = ( + current[0] + ) + atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = ( + current[-1] + ) sele1 = u.atoms[sele1_index] sele2 = u.atoms[sele2_index] - (s1_resname, s1_resid, s1_name) = (sele1.resname, sele1.resid, sele1.name) - (s2_resname, s2_resid, s2_name) = (sele2.resname, sele2.resid, sele2.name) - - key = (sele1_index, sele2_index, s1_resname, s1_resid, s1_name, s2_resname, s2_resid, s2_name) - if s2_name == 'H': + (s1_resname, s1_resid, s1_name) = ( + sele1.resname, + sele1.resid, + sele1.name, + ) + (s2_resname, s2_resid, s2_name) = ( + sele2.resname, + sele2.resid, + sele2.name, + ) + + key = ( + sele1_index, + sele2_index, + s1_resname, + s1_resid, + s1_name, + s2_resname, + s2_resid, + s2_name, + ) + if s2_name == "H": output[key] += 1 - result = [((0, 11, 'ALA', 1, 'O', 'ALA', 6, 'H'), 0.25)] - assert_equal(sorted(wb_multiframe.count_by_type(analysis_func=analysis)), result) + + result = [((0, 11, "ALA", 1, "O", "ALA", 6, "H"), 0.25)] + assert_equal( + sorted(wb_multiframe.count_by_type(analysis_func=analysis)), result + ) def test_count_by_type_merge(self, wb_multiframe): - ''' + """ This test tests if modifying analysis_func allows some same residue to be merged in count_by_type(). - ''' + """ + def analysis(current, output, u): - sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = current[0] - atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = current[-1] + sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = ( + current[0] + ) + atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = ( + current[-1] + ) sele1 = u.atoms[sele1_index] sele2 = u.atoms[sele2_index] - (s1_resname, s1_resid, s1_name) = (sele1.resname, sele1.resid, sele1.name) - (s2_resname, s2_resid, s2_name) = (sele2.resname, sele2.resid, sele2.name) + (s1_resname, s1_resid, s1_name) = ( + sele1.resname, + sele1.resid, + sele1.name, + ) + (s2_resname, s2_resid, s2_name) = ( + sele2.resname, + sele2.resid, + sele2.name, + ) key = (s1_resname, s1_resid, s2_resname, s2_resid) output[key] = 1 - result = [(('ALA', 1, 'ALA', 6), 1.0)] - assert_equal(sorted(wb_multiframe.count_by_type(analysis_func=analysis)), result) + + result = [(("ALA", 1, "ALA", 6), 1.0)] + assert_equal( + sorted(wb_multiframe.count_by_type(analysis_func=analysis)), result + ) def test_count_by_type_order(self, wb_multiframe): - ''' + """ This test tests if modifying analysis_func allows the order of water bridge to be separated in count_by_type(). :return: - ''' + """ + def analysis(current, output, u): - sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = current[0] - atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = current[-1] + sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = ( + current[0] + ) + atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = ( + current[-1] + ) sele1 = u.atoms[sele1_index] sele2 = u.atoms[sele2_index] - (s1_resname, s1_resid, s1_name) = (sele1.resname, sele1.resid, sele1.name) - (s2_resname, s2_resid, s2_name) = (sele2.resname, sele2.resid, sele2.name) - key = (s1_resname, s1_resid, s2_resname, s2_resid, len(current)-1) + (s1_resname, s1_resid, s1_name) = ( + sele1.resname, + sele1.resid, + sele1.name, + ) + (s2_resname, s2_resid, s2_name) = ( + sele2.resname, + sele2.resid, + sele2.name, + ) + key = ( + s1_resname, + s1_resid, + s2_resname, + s2_resid, + len(current) - 1, + ) output[key] = 1 - result = [(('ALA', 1, 'ALA', 6, 0), 0.5), - (('ALA', 1, 'ALA', 6, 1), 0.25), - (('ALA', 1, 'ALA', 6, 2), 0.25)] - assert_equal(sorted(wb_multiframe.count_by_type(analysis_func=analysis)), result) + + result = [ + (("ALA", 1, "ALA", 6, 0), 0.5), + (("ALA", 1, "ALA", 6, 1), 0.25), + (("ALA", 1, "ALA", 6, 2), 0.25), + ] + assert_equal( + sorted(wb_multiframe.count_by_type(analysis_func=analysis)), result + ) def test_count_by_time(self, wb_multiframe): - ''' + """ This test tests if count_by_times() works. :return: - ''' - assert_equal(wb_multiframe.count_by_time(), [(0, 1), (1, 1), (2, 1), (3, 1)]) - + """ + assert_equal( + wb_multiframe.count_by_time(), [(0, 1), (1, 1), (2, 1), (3, 1)] + ) def test_count_by_time_weight(self, universe_AWA_AWWA): - ''' + """ This test tests if modyfing the analysis_func allows the weight to be changed in count_by_type(). :return: - ''' - wb = WaterBridgeAnalysis(universe_AWA_AWWA, 'protein and (resid 1 or resid 5)', - 'protein and (resid 4 or resid 8)', order=2) + """ + wb = WaterBridgeAnalysis( + universe_AWA_AWWA, + "protein and (resid 1 or resid 5)", + "protein and (resid 4 or resid 8)", + order=2, + ) wb.run(verbose=False) + def analysis(current, output, u): - sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = current[0] - atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = current[-1] + sele1_index, sele1_heavy_index, atom2, heavy_atom2, dist, angle = ( + current[0] + ) + atom1, heavy_atom1, sele2_index, sele2_heavy_index, dist, angle = ( + current[-1] + ) sele1 = u.atoms[sele1_index] sele2 = u.atoms[sele2_index] - (s1_resname, s1_resid, s1_name) = (sele1.resname, sele1.resid, sele1.name) - (s2_resname, s2_resid, s2_name) = (sele2.resname, sele2.resid, sele2.name) + (s1_resname, s1_resid, s1_name) = ( + sele1.resname, + sele1.resid, + sele1.name, + ) + (s2_resname, s2_resid, s2_name) = ( + sele2.resname, + sele2.resid, + sele2.name, + ) key = (s1_resname, s1_resid, s2_resname, s2_resid) - output[key] += len(current)-1 - assert_equal(wb.count_by_time(analysis_func=analysis), [(0,3), ]) + output[key] += len(current) - 1 + + assert_equal( + wb.count_by_time(analysis_func=analysis), + [ + (0, 3), + ], + ) def test_count_by_time_empty(self, universe_AWA_AWWA): - ''' + """ See if count_by_time() can handle zero well. :return: - ''' - wb = WaterBridgeAnalysis(universe_AWA_AWWA, 'protein and (resid 1 or resid 5)', - 'protein and (resid 4 or resid 8)', order=2) + """ + wb = WaterBridgeAnalysis( + universe_AWA_AWWA, + "protein and (resid 1 or resid 5)", + "protein and (resid 4 or resid 8)", + order=2, + ) wb.run(verbose=False) + def analysis(current, output, u): pass - assert_equal(wb.count_by_time(analysis_func=analysis), [(0,0), ]) + + assert_equal( + wb.count_by_time(analysis_func=analysis), + [ + (0, 0), + ], + ) def test_generate_table_hba(self, wb_multiframe): - '''Test generate table using hydrogen bond analysis format''' - table = wb_multiframe.generate_table(output_format='donor_acceptor') + """Test generate table using hydrogen bond analysis format""" + table = wb_multiframe.generate_table(output_format="donor_acceptor") assert_array_equal( sorted(table.donor_resid), [1, 1, 2, 2, 2, 6, 6], ) def test_generate_table_s1s2(self, wb_multiframe): - '''Test generate table using hydrogen bond analysis format''' - table = wb_multiframe.generate_table(output_format='sele1_sele2') + """Test generate table using hydrogen bond analysis format""" + table = wb_multiframe.generate_table(output_format="sele1_sele2") assert_array_equal( sorted(table.sele1_resid), [1, 1, 1, 1, 2, 2, 3], ) def test_timesteps_by_type(self, wb_multiframe): - '''Test the timesteps_by_type function''' + """Test the timesteps_by_type function""" timesteps = sorted(wb_multiframe.timesteps_by_type()) - assert_array_equal(timesteps[3], [1, 12, 'ALA', 1, 'H', 'ALA', 6, 'O', 0, 2]) + assert_array_equal( + timesteps[3], [1, 12, "ALA", 1, "H", "ALA", 6, "O", 0, 2] + ) def test_duplicate_water(self): - '''A case #3119 where + """A case #3119 where Acceptor···H−O···H-Donor | H···O-H will be recognised as 3rd order water bridge. - ''' - grofile = '''Test gro file + """ + grofile = """Test gro file 7 1LEU O 1 1.876 0.810 1.354 117SOL HW1 2 1.853 0.831 1.162 @@ -788,16 +1042,21 @@ def test_duplicate_water(self): 135SOL OW 5 1.924 0.713 0.845 1LEU H 6 1.997 0.991 1.194 1LEU N 7 2.041 1.030 1.274 - 2.22092 2.22092 2.22092''' - u = MDAnalysis.Universe(StringIO(grofile), format='gro') - wb = WaterBridgeAnalysis(u, 'resname LEU and name O', - 'resname LEU and name N H', order=4) + 2.22092 2.22092 2.22092""" + u = MDAnalysis.Universe(StringIO(grofile), format="gro") + wb = WaterBridgeAnalysis( + u, "resname LEU and name O", "resname LEU and name N H", order=4 + ) wb.run() assert len(wb.results.timeseries[0]) == 2 def test_warn_results_deprecated(self, universe_DA): - wb = WaterBridgeAnalysis(universe_DA, 'protein and (resid 9)', - 'protein and (resid 10)', order=0) + wb = WaterBridgeAnalysis( + universe_DA, + "protein and (resid 9)", + "protein and (resid 10)", + order=0, + ) wb.run() wmsg = "The `network` attribute was deprecated in MDAnalysis 2.0.0" diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 8c12d164e7f..4a9a4bf6251 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -162,6 +162,7 @@ setup\.py | MDAnalysisTests/auxiliary/.*\.py | MDAnalysisTests/lib/.*\.py | MDAnalysisTests/transformations/.*\.py +| MDAnalysisTests/analysis/.*\.py | MDAnalysisTests/guesser/.*\.py | MDAnalysisTests/converters/.*\.py | MDAnalysisTests/coordinates/.*\.py From 453be6cf7b6613a258e0790bbe2367fc572ac739 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Sun, 29 Dec 2024 23:54:23 +0100 Subject: [PATCH 49/58] [fmt] Format topology module and tests (#4849) --- package/MDAnalysis/topology/CRDParser.py | 78 ++- package/MDAnalysis/topology/DLPolyParser.py | 20 +- package/MDAnalysis/topology/DMSParser.py | 99 ++-- .../MDAnalysis/topology/ExtendedPDBParser.py | 63 ++- package/MDAnalysis/topology/FHIAIMSParser.py | 25 +- package/MDAnalysis/topology/GMSParser.py | 31 +- package/MDAnalysis/topology/GROParser.py | 33 +- package/MDAnalysis/topology/GSDParser.py | 44 +- package/MDAnalysis/topology/HoomdXMLParser.py | 53 +- package/MDAnalysis/topology/ITPParser.py | 274 +++++---- package/MDAnalysis/topology/LAMMPSParser.py | 275 +++++---- package/MDAnalysis/topology/MMTFParser.py | 99 ++-- package/MDAnalysis/topology/MOL2Parser.py | 90 +-- package/MDAnalysis/topology/MinimalParser.py | 5 +- package/MDAnalysis/topology/PDBQTParser.py | 38 +- package/MDAnalysis/topology/PQRParser.py | 78 ++- package/MDAnalysis/topology/PSFParser.py | 135 +++-- package/MDAnalysis/topology/ParmEdParser.py | 2 +- package/MDAnalysis/topology/TOPParser.py | 139 +++-- package/MDAnalysis/topology/TPRParser.py | 22 +- package/MDAnalysis/topology/TXYZParser.py | 39 +- package/MDAnalysis/topology/XYZParser.py | 21 +- package/MDAnalysis/topology/core.py | 14 +- package/MDAnalysis/topology/guessers.py | 71 ++- package/MDAnalysis/topology/tpr/obj.py | 62 +- package/MDAnalysis/topology/tpr/setting.py | 29 +- package/pyproject.toml | 1 + testsuite/MDAnalysisTests/topology/base.py | 59 +- .../MDAnalysisTests/topology/test_altloc.py | 2 +- .../MDAnalysisTests/topology/test_crd.py | 18 +- .../MDAnalysisTests/topology/test_dlpoly.py | 37 +- .../MDAnalysisTests/topology/test_dms.py | 23 +- .../MDAnalysisTests/topology/test_fhiaims.py | 19 +- .../MDAnalysisTests/topology/test_gms.py | 20 +- .../MDAnalysisTests/topology/test_gro.py | 32 +- .../MDAnalysisTests/topology/test_gsd.py | 29 +- .../MDAnalysisTests/topology/test_guessers.py | 177 +++--- .../MDAnalysisTests/topology/test_hoomdxml.py | 15 +- .../MDAnalysisTests/topology/test_itp.py | 181 ++++-- .../topology/test_lammpsdata.py | 144 +++-- .../MDAnalysisTests/topology/test_minimal.py | 35 +- .../MDAnalysisTests/topology/test_mmtf.py | 48 +- .../MDAnalysisTests/topology/test_mol2.py | 101 ++-- .../MDAnalysisTests/topology/test_pdb.py | 197 ++++--- .../MDAnalysisTests/topology/test_pdbqt.py | 7 +- .../MDAnalysisTests/topology/test_pqr.py | 44 +- .../MDAnalysisTests/topology/test_psf.py | 37 +- .../MDAnalysisTests/topology/test_tables.py | 1 - .../MDAnalysisTests/topology/test_top.py | 532 +++++++++++------- .../topology/test_topology_base.py | 29 +- .../topology/test_topology_str_types.py | 67 ++- .../topology/test_tprparser.py | 313 +++++++---- .../MDAnalysisTests/topology/test_txyz.py | 30 +- .../MDAnalysisTests/topology/test_xpdb.py | 19 +- .../MDAnalysisTests/topology/test_xyz.py | 6 +- testsuite/pyproject.toml | 1 + 56 files changed, 2504 insertions(+), 1559 deletions(-) diff --git a/package/MDAnalysis/topology/CRDParser.py b/package/MDAnalysis/topology/CRDParser.py index d1896423a84..c0322824baa 100644 --- a/package/MDAnalysis/topology/CRDParser.py +++ b/package/MDAnalysis/topology/CRDParser.py @@ -89,7 +89,8 @@ class CRDParser(TopologyReaderBase): 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' + + format = "CRD" def parse(self, **kwargs): """Create the Topology object @@ -102,8 +103,10 @@ def parse(self, **kwargs): ---- Could use the resnum and temp factor better """ - extformat = FORTRANReader('2I10,2X,A8,2X,A8,3F20.10,2X,A8,2X,A8,F20.10') - stdformat = FORTRANReader('2I5,1X,A4,1X,A4,3F10.5,1X,A4,1X,A4,F10.5') + extformat = FORTRANReader( + "2I10,2X,A8,2X,A8,3F20.10,2X,A8,2X,A8,F20.10" + ) + stdformat = FORTRANReader("2I5,1X,A4,1X,A4,3F10.5,1X,A4,1X,A4,F10.5") atomids = [] atomnames = [] @@ -116,21 +119,36 @@ def parse(self, **kwargs): with openany(self.filename) as crd: for linenum, line in enumerate(crd): # reading header - if line.split()[0] == '*': + if line.split()[0] == "*": continue - elif line.split()[-1] == 'EXT' and int(line.split()[0]): + elif line.split()[-1] == "EXT" and int(line.split()[0]): r = extformat continue - elif line.split()[0] == line.split()[-1] and line.split()[0] != '*': + elif ( + line.split()[0] == line.split()[-1] + and line.split()[0] != "*" + ): r = stdformat continue # anything else should be an atom try: - (serial, resnum, resName, name, - x, y, z, segid, resid, tempFactor) = r.read(line) + ( + serial, + resnum, + resName, + name, + x, + y, + z, + segid, + resid, + tempFactor, + ) = r.read(line) except Exception: - errmsg = (f"Check CRD format at line {linenum + 1}: " - f"{line.rstrip()}") + errmsg = ( + f"Check CRD format at line {linenum + 1}: " + f"{line.rstrip()}" + ) raise ValueError(errmsg) from None atomids.append(serial) @@ -150,22 +168,28 @@ def parse(self, **kwargs): resnums = np.array(resnums, dtype=np.int32) segids = np.array(segids, dtype=object) - atom_residx, (res_resids, res_resnames, res_resnums, res_segids) = change_squash( - (resids, resnames), (resids, resnames, resnums, segids)) - res_segidx, (seg_segids,) = change_squash( - (res_segids,), (res_segids,)) - - top = Topology(len(atomids), len(res_resids), len(seg_segids), - attrs=[ - Atomids(atomids), - Atomnames(atomnames), - Tempfactors(tempfactors), - Resids(res_resids), - Resnames(res_resnames), - Resnums(res_resnums), - Segids(seg_segids), - ], - atom_resindex=atom_residx, - residue_segindex=res_segidx) + atom_residx, (res_resids, res_resnames, res_resnums, res_segids) = ( + change_squash( + (resids, resnames), (resids, resnames, resnums, segids) + ) + ) + res_segidx, (seg_segids,) = change_squash((res_segids,), (res_segids,)) + + top = Topology( + len(atomids), + len(res_resids), + len(seg_segids), + attrs=[ + Atomids(atomids), + Atomnames(atomnames), + Tempfactors(tempfactors), + Resids(res_resids), + Resnames(res_resnames), + Resnums(res_resnums), + Segids(seg_segids), + ], + atom_resindex=atom_residx, + residue_segindex=res_segidx, + ) return top diff --git a/package/MDAnalysis/topology/DLPolyParser.py b/package/MDAnalysis/topology/DLPolyParser.py index 5452dbad3ce..6c03ed62c2d 100644 --- a/package/MDAnalysis/topology/DLPolyParser.py +++ b/package/MDAnalysis/topology/DLPolyParser.py @@ -69,7 +69,8 @@ class ConfigParser(TopologyReaderBase): Removed type and mass guessing (attributes guessing takes place now through universe.guess_TopologyAttrs() API). """ - format = 'CONFIG' + + format = "CONFIG" def parse(self, **kwargs): with openany(self.filename) as inf: @@ -117,10 +118,9 @@ def parse(self, **kwargs): Atomids(ids), Resids(np.array([1])), Resnums(np.array([1])), - Segids(np.array(['SYSTEM'], dtype=object)), + Segids(np.array(["SYSTEM"], dtype=object)), ] - top = Topology(n_atoms, 1, 1, - attrs=attrs) + top = Topology(n_atoms, 1, 1, attrs=attrs) return top @@ -130,7 +130,8 @@ class HistoryParser(TopologyReaderBase): .. versionadded:: 0.10.1 """ - format = 'HISTORY' + + format = "HISTORY" def parse(self, **kwargs): with openany(self.filename) as inf: @@ -143,10 +144,10 @@ def parse(self, **kwargs): line = inf.readline() while not (len(line.split()) == 4 or len(line.split()) == 5): line = inf.readline() - if line == '': + if line == "": raise EOFError("End of file reached when reading HISTORY.") - while line and not line.startswith('timestep'): + while line and not line.startswith("timestep"): name = line[:8].strip() names.append(name) try: @@ -179,9 +180,8 @@ def parse(self, **kwargs): Atomids(ids), Resids(np.array([1])), Resnums(np.array([1])), - Segids(np.array(['SYSTEM'], dtype=object)), + Segids(np.array(["SYSTEM"], dtype=object)), ] - top = Topology(n_atoms, 1, 1, - attrs=attrs) + top = Topology(n_atoms, 1, 1, attrs=attrs) return top diff --git a/package/MDAnalysis/topology/DMSParser.py b/package/MDAnalysis/topology/DMSParser.py index 84c04fd2ac8..691b47eed64 100644 --- a/package/MDAnalysis/topology/DMSParser.py +++ b/package/MDAnalysis/topology/DMSParser.py @@ -63,8 +63,9 @@ class Atomnums(AtomAttr): """The number for each Atom""" - attrname = 'atomnums' - singular = 'atomnum' + + attrname = "atomnums" + singular = "atomnum" class DMSParser(TopologyReaderBase): @@ -100,7 +101,8 @@ class DMSParser(TopologyReaderBase): through universe.guess_TopologyAttrs() API). """ - format = 'DMS' + + format = "DMS" def parse(self, **kwargs): """Parse DMS file *filename* and return the Topology object""" @@ -121,28 +123,29 @@ def dict_factory(cursor, row): attrs = {} # Row factories for different data types - facs = {np.int32: lambda c, r: r[0], - np.float32: lambda c, r: r[0], - object: lambda c, r: str(r[0].strip())} + facs = { + np.int32: lambda c, r: r[0], + np.float32: lambda c, r: r[0], + object: lambda c, r: str(r[0].strip()), + } with sqlite3.connect(self.filename) as con: # Selecting single column, so just strip tuple for attrname, dt in [ - ('id', np.int32), - ('anum', np.int32), - ('mass', np.float32), - ('charge', np.float32), - ('name', object), - ('resname', object), - ('resid', np.int32), - ('chain', object), - ('segid', object), + ("id", np.int32), + ("anum", np.int32), + ("mass", np.float32), + ("charge", np.float32), + ("name", object), + ("resname", object), + ("resid", np.int32), + ("chain", object), + ("segid", object), ]: try: cur = con.cursor() cur.row_factory = facs[dt] - cur.execute('SELECT {} FROM particle' - ''.format(attrname)) + cur.execute("SELECT {} FROM particle" "".format(attrname)) vals = cur.fetchall() except sqlite3.DatabaseError: errmsg = "Failed reading the atoms from DMS Database" @@ -152,7 +155,7 @@ def dict_factory(cursor, row): try: cur.row_factory = dict_factory - cur.execute('SELECT * FROM bond') + cur.execute("SELECT * FROM bond") bonds = cur.fetchall() except sqlite3.DatabaseError: errmsg = "Failed reading the bonds from DMS Database" @@ -161,35 +164,36 @@ def dict_factory(cursor, row): bondlist = [] bondorder = {} for b in bonds: - desc = tuple(sorted([b['p0'], b['p1']])) + desc = tuple(sorted([b["p0"], b["p1"]])) bondlist.append(desc) - bondorder[desc] = b['order'] - attrs['bond'] = bondlist - attrs['bondorder'] = bondorder + bondorder[desc] = b["order"] + attrs["bond"] = bondlist + attrs["bondorder"] = bondorder topattrs = [] # Bundle in Atom level objects for attr, cls in [ - ('id', Atomids), - ('anum', Atomnums), - ('mass', Masses), - ('charge', Charges), - ('name', Atomnames), - ('chain', ChainIDs), + ("id", Atomids), + ("anum", Atomnums), + ("mass", Masses), + ("charge", Charges), + ("name", Atomnames), + ("chain", ChainIDs), ]: topattrs.append(cls(attrs[attr])) # Residues - atom_residx, (res_resids, - res_resnums, - res_resnames, - res_segids) = change_squash( - (attrs['resid'], attrs['resname'], attrs['segid']), - (attrs['resid'], - attrs['resid'].copy(), - attrs['resname'], - attrs['segid']), + atom_residx, (res_resids, res_resnums, res_resnames, res_segids) = ( + change_squash( + (attrs["resid"], attrs["resname"], attrs["segid"]), + ( + attrs["resid"], + attrs["resid"].copy(), + attrs["resname"], + attrs["segid"], + ), ) + ) n_residues = len(res_resids) topattrs.append(Resids(res_resids)) @@ -197,8 +201,9 @@ def dict_factory(cursor, row): topattrs.append(Resnames(res_resnames)) if any(res_segids) and not any(val is None for val in res_segids): - res_segidx, (res_segids,) = change_squash((res_segids,), - (res_segids,)) + res_segidx, (res_segids,) = change_squash( + (res_segids,), (res_segids,) + ) uniq_seg = np.unique(res_segids) idx2seg = {idx: res_segids[idx] for idx in res_segidx} @@ -211,14 +216,18 @@ def dict_factory(cursor, row): topattrs.append(Segids(res_segids)) else: n_segments = 1 - topattrs.append(Segids(np.array(['SYSTEM'], dtype=object))) + topattrs.append(Segids(np.array(["SYSTEM"], dtype=object))) res_segidx = None - topattrs.append(Bonds(attrs['bond'])) + topattrs.append(Bonds(attrs["bond"])) - top = Topology(len(attrs['id']), n_residues, n_segments, - attrs=topattrs, - atom_resindex=atom_residx, - residue_segindex=res_segidx) + top = Topology( + len(attrs["id"]), + n_residues, + n_segments, + attrs=topattrs, + atom_resindex=atom_residx, + residue_segindex=res_segidx, + ) return top diff --git a/package/MDAnalysis/topology/ExtendedPDBParser.py b/package/MDAnalysis/topology/ExtendedPDBParser.py index ef4f25fee48..e1d76791986 100644 --- a/package/MDAnalysis/topology/ExtendedPDBParser.py +++ b/package/MDAnalysis/topology/ExtendedPDBParser.py @@ -60,35 +60,36 @@ class ExtendedPDBParser(PDBParser.PDBParser): """Parser that handles non-standard "extended" PDB file. - Extended PDB files (MDAnalysis format specifier *XPDB*) may contain residue - sequence numbers up to 99,999 by utilizing the insertion character field of - the PDB standard. - - Creates a Topology with the following Attributes (if present): - - serials - - names - - altLocs - - chainids - - tempfactors - - occupancies - - resids - - resnames - - segids - - elements - - 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 - -------- - :class:`MDAnalysis.coordinates.PDB.ExtendedPDBReader` - - .. versionadded:: 0.8 + Extended PDB files (MDAnalysis format specifier *XPDB*) may contain residue + sequence numbers up to 99,999 by utilizing the insertion character field of + the PDB standard. + + Creates a Topology with the following Attributes (if present): + - serials + - names + - altLocs + - chainids + - tempfactors + - occupancies + - resids + - resnames + - segids + - elements + - 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 + -------- + :class:`MDAnalysis.coordinates.PDB.ExtendedPDBReader` + + .. versionadded:: 0.8 """ - format = 'XPDB' + + format = "XPDB" diff --git a/package/MDAnalysis/topology/FHIAIMSParser.py b/package/MDAnalysis/topology/FHIAIMSParser.py index a47d367ec9e..c3aa80aced4 100644 --- a/package/MDAnalysis/topology/FHIAIMSParser.py +++ b/package/MDAnalysis/topology/FHIAIMSParser.py @@ -77,7 +77,8 @@ class FHIAIMSParser(TopologyReaderBase): Removed type and mass guessing (attributes guessing takes place now through universe.guess_TopologyAttrs() API). """ - format = ['IN', 'FHIAIMS'] + + format = ["IN", "FHIAIMS"] def parse(self, **kwargs): """Read the file and return the structure. @@ -99,18 +100,22 @@ def parse(self, **kwargs): continue # we are now seeing something that's neither atom nor lattice raise ValueError( - 'Non-conforming line: ({0})in FHI-AIMS input file {0}'.format(line, self.filename)) + "Non-conforming line: ({0})in FHI-AIMS input file {0}".format( + line, self.filename + ) + ) names = np.asarray(names) natoms = len(names) - attrs = [Atomnames(names), - Atomids(np.arange(natoms) + 1), - Resids(np.array([1])), - Resnums(np.array([1])), - Segids(np.array(['SYSTEM'], dtype=object)), - Elements(names)] + attrs = [ + Atomnames(names), + Atomids(np.arange(natoms) + 1), + Resids(np.array([1])), + Resnums(np.array([1])), + Segids(np.array(["SYSTEM"], dtype=object)), + Elements(names), + ] - top = Topology(natoms, 1, 1, - attrs=attrs) + top = Topology(natoms, 1, 1, attrs=attrs) return top diff --git a/package/MDAnalysis/topology/GMSParser.py b/package/MDAnalysis/topology/GMSParser.py index 2ea7fe23004..41e85f73ec8 100644 --- a/package/MDAnalysis/topology/GMSParser.py +++ b/package/MDAnalysis/topology/GMSParser.py @@ -60,10 +60,11 @@ AtomAttr, ) + class AtomicCharges(AtomAttr): - attrname = 'atomiccharges' - singular = 'atomiccharge' - per_object = 'atom' + attrname = "atomiccharges" + singular = "atomiccharge" + per_object = "atom" class GMSParser(TopologyReaderBase): @@ -86,7 +87,8 @@ class GMSParser(TopologyReaderBase): through universe.guess_TopologyAttrs() API). """ - format = 'GMS' + + format = "GMS" def parse(self, **kwargs): """Read list of atoms from a GAMESS file.""" @@ -98,16 +100,18 @@ def parse(self, **kwargs): line = inf.readline() if not line: raise EOFError - if re.match(r'^\s+ATOM\s+ATOMIC\s+COORDINATES\s*\(BOHR\).*',\ - line): + if re.match( + r"^\s+ATOM\s+ATOMIC\s+COORDINATES\s*\(BOHR\).*", line + ): break - line = inf.readline() # skip + line = inf.readline() # skip while True: line = inf.readline() - _m = re.match(\ -r'^\s*([A-Za-z_][A-Za-z_0-9]*)\s+([0-9]+\.[0-9]+)\s+(\-?[0-9]+\.[0-9]+)\s+(\-?[0-9]+\.[0-9]+)\s+(\-?[0-9]+\.[0-9]+).*', - line) + _m = re.match( + r"^\s*([A-Za-z_][A-Za-z_0-9]*)\s+([0-9]+\.[0-9]+)\s+(\-?[0-9]+\.[0-9]+)\s+(\-?[0-9]+\.[0-9]+)\s+(\-?[0-9]+\.[0-9]+).*", + line, + ) if _m is None: break name = _m.group(1) @@ -115,7 +119,7 @@ def parse(self, **kwargs): names.append(name) at_charges.append(at_charge) - #TODO: may be use coordinates info from _m.group(3-5) ?? + # TODO: may be use coordinates info from _m.group(3-5) ?? n_atoms = len(names) attrs = [ @@ -124,9 +128,8 @@ def parse(self, **kwargs): AtomicCharges(np.array(at_charges, dtype=np.int32)), Resids(np.array([1])), Resnums(np.array([1])), - Segids(np.array(['SYSTEM'], dtype=object)), + Segids(np.array(["SYSTEM"], dtype=object)), ] - top = Topology(n_atoms, 1, 1, - attrs=attrs) + top = Topology(n_atoms, 1, 1, attrs=attrs) return top diff --git a/package/MDAnalysis/topology/GROParser.py b/package/MDAnalysis/topology/GROParser.py index 368b7d5daf0..dab8ef691cd 100644 --- a/package/MDAnalysis/topology/GROParser.py +++ b/package/MDAnalysis/topology/GROParser.py @@ -78,7 +78,8 @@ class GROParser(TopologyReaderBase): through universe.guess_TopologyAttrs() API). """ - format = 'GRO' + + format = "GRO" def parse(self, **kwargs): """Return the *Topology* object for this file""" @@ -104,14 +105,16 @@ def parse(self, **kwargs): indices[i] = int(line[15:20]) except (ValueError, TypeError): errmsg = ( - f"Couldn't read the following line of the .gro file:\n" - f"{line}") + f"Couldn't read the following line of the .gro file:\n" + f"{line}" + ) raise IOError(errmsg) from None # Check all lines had names if not np.all(names): - missing = np.where(names == '') - raise IOError("Missing atom name on line: {0}" - "".format(missing[0][0] + 3)) # 2 header, 1 based + missing = np.where(names == "") + raise IOError( + "Missing atom name on line: {0}" "".format(missing[0][0] + 3) + ) # 2 header, 1 based # Fix wrapping of resids (if we ever saw a wrap) if np.any(resids == 0): @@ -132,9 +135,9 @@ def parse(self, **kwargs): for s in starts: resids[s:] += 100000 - residx, (new_resids, new_resnames) = change_squash( - (resids, resnames), (resids, resnames)) + (resids, resnames), (resids, resnames) + ) # new_resids is len(residues) # so resindex 0 has resid new_resids[0] @@ -144,12 +147,16 @@ def parse(self, **kwargs): Resids(new_resids), Resnums(new_resids.copy()), Resnames(new_resnames), - Segids(np.array(['SYSTEM'], dtype=object)) + Segids(np.array(["SYSTEM"], dtype=object)), ] - top = Topology(n_atoms=n_atoms, n_res=len(new_resids), n_seg=1, - attrs=attrs, - atom_resindex=residx, - residue_segindex=None) + top = Topology( + n_atoms=n_atoms, + n_res=len(new_resids), + n_seg=1, + attrs=attrs, + atom_resindex=residx, + residue_segindex=None, + ) return top diff --git a/package/MDAnalysis/topology/GSDParser.py b/package/MDAnalysis/topology/GSDParser.py index 64746dd87ef..45f7e570b07 100644 --- a/package/MDAnalysis/topology/GSDParser.py +++ b/package/MDAnalysis/topology/GSDParser.py @@ -104,13 +104,16 @@ class GSDParser(TopologyReaderBase): GSD file. """ - format = 'GSD' + + format = "GSD" def __init__(self, filename): if not HAS_GSD: - errmsg = ("GSDParser: To read a Topology from a Hoomd GSD " - "file, please install gsd") + errmsg = ( + "GSDParser: To read a Topology from a Hoomd GSD " + "file, please install gsd" + ) raise ImportError(errmsg) super(GSDParser, self).__init__(filename) @@ -121,35 +124,39 @@ def parse(self, **kwargs): """ attrs = {} - with gsd.hoomd.open(self.filename, mode='r') as t : + with gsd.hoomd.open(self.filename, mode="r") as t: # Here it is assumed that the particle data does not change in the # trajectory. snap = t[0] natoms = snap.particles.N - ptypes = snap.particles.types atypes = [ptypes[idx] for idx in snap.particles.typeid] if len(atypes) != natoms: raise IOError("Number of types does not equal natoms.") - attrs['types'] = Atomtypes(np.array(atypes, dtype=object)) + attrs["types"] = Atomtypes(np.array(atypes, dtype=object)) # set radii, masses, charges p = snap.particles - attrs['diameter'] = Radii(np.array(p.diameter / 2.,dtype=np.float32)) - attrs['mass'] = Masses(np.array(p.mass,dtype=np.float64)) - attrs['charge'] = Charges(np.array(p.charge,dtype=np.float32)) + attrs["diameter"] = Radii( + np.array(p.diameter / 2.0, dtype=np.float32) + ) + attrs["mass"] = Masses(np.array(p.mass, dtype=np.float64)) + attrs["charge"] = Charges(np.array(p.charge, dtype=np.float32)) # set bonds, angles, dihedrals, impropers - for attrname, attr, in ( - ('bonds', Bonds), - ('angles', Angles), - ('dihedrals', Dihedrals), - ('impropers', Impropers), + for ( + attrname, + attr, + ) in ( + ("bonds", Bonds), + ("angles", Angles), + ("dihedrals", Dihedrals), + ("impropers", Impropers), ): try: - val = getattr(snap,attrname) + val = getattr(snap, attrname) vals = [tuple(b_instance) for b_instance in val.group] except: vals = [] @@ -160,7 +167,7 @@ def parse(self, **kwargs): bodies = np.unique(blist).astype(np.int32) # this fixes the fact that the Topology constructor gets stuck in an # infinite loop if any resid is negative. - if (blist<0).any() : + if (blist < 0).any(): m = blist.min() blist += abs(m) bodies = np.unique(blist).astype(np.int32) @@ -172,9 +179,8 @@ def parse(self, **kwargs): attrs.append(Resids(bodies)) attrs.append(Resnums(bodies)) attrs.append(Resnames(bodies)) - attrs.append(Segids(np.array(['SYSTEM'], dtype=object))) + attrs.append(Segids(np.array(["SYSTEM"], dtype=object))) - top = Topology(natoms, nbodies, 1, - attrs=attrs, atom_resindex=blist) + top = Topology(natoms, nbodies, 1, attrs=attrs, atom_resindex=blist) return top diff --git a/package/MDAnalysis/topology/HoomdXMLParser.py b/package/MDAnalysis/topology/HoomdXMLParser.py index b64e91de2ff..7b57bb59002 100644 --- a/package/MDAnalysis/topology/HoomdXMLParser.py +++ b/package/MDAnalysis/topology/HoomdXMLParser.py @@ -81,7 +81,8 @@ class HoomdXMLParser(TopologyReaderBase): - Masses """ - format = 'XML' + + format = "XML" def parse(self, **kwargs): """Parse Hoomd XML file @@ -102,21 +103,21 @@ def parse(self, **kwargs): with openany(self.filename) as stream: tree = ET.parse(stream) root = tree.getroot() - configuration = root.find('configuration') - natoms = int(configuration.get('natoms')) + configuration = root.find("configuration") + natoms = int(configuration.get("natoms")) attrs = {} - atype = configuration.find('type') - atypes = atype.text.strip().split('\n') + atype = configuration.find("type") + atypes = atype.text.strip().split("\n") if len(atypes) != natoms: raise IOError("Number of types does not equal natoms.") - attrs['types'] = Atomtypes(np.array(atypes, dtype=object)) + attrs["types"] = Atomtypes(np.array(atypes, dtype=object)) for attrname, attr, mapper, dtype in ( - ('diameter', Radii, lambda x: float(x) / 2., np.float32), - ('mass', Masses, float, np.float64), - ('charge', Charges, float, np.float32), + ("diameter", Radii, lambda x: float(x) / 2.0, np.float32), + ("mass", Masses, float, np.float64), + ("charge", Charges, float, np.float32), ): try: val = configuration.find(attrname) @@ -125,34 +126,38 @@ def parse(self, **kwargs): pass else: attrs[attrname] = attr(np.array(vals, dtype=dtype)) - for attrname, attr, in ( - ('bond', Bonds), - ('angle', Angles), - ('dihedral', Dihedrals), - ('improper', Impropers), + for ( + attrname, + attr, + ) in ( + ("bond", Bonds), + ("angle", Angles), + ("dihedral", Dihedrals), + ("improper", Impropers), ): try: val = configuration.find(attrname) - vals = [tuple(int(el) for el in line.split()[1:]) - for line in val.text.strip().split('\n') - if line.strip()] + vals = [ + tuple(int(el) for el in line.split()[1:]) + for line in val.text.strip().split("\n") + if line.strip() + ] except: vals = [] attrs[attrname] = attr(vals) - if 'mass' not in attrs: - attrs['mass'] = Masses(np.zeros(natoms)) - if 'charge' not in attrs: - attrs['charge'] = Charges(np.zeros(natoms, dtype=np.float32)) + if "mass" not in attrs: + attrs["mass"] = Masses(np.zeros(natoms)) + if "charge" not in attrs: + attrs["charge"] = Charges(np.zeros(natoms, dtype=np.float32)) attrs = list(attrs.values()) attrs.append(Atomids(np.arange(natoms) + 1)) attrs.append(Resids(np.array([1]))) attrs.append(Resnums(np.array([1]))) - attrs.append(Segids(np.array(['SYSTEM'], dtype=object))) + attrs.append(Segids(np.array(["SYSTEM"], dtype=object))) - top = Topology(natoms, 1, 1, - attrs=attrs) + top = Topology(natoms, 1, 1, attrs=attrs) return top diff --git a/package/MDAnalysis/topology/ITPParser.py b/package/MDAnalysis/topology/ITPParser.py index 9968f854df7..1aa87b3864b 100644 --- a/package/MDAnalysis/topology/ITPParser.py +++ b/package/MDAnalysis/topology/ITPParser.py @@ -162,8 +162,9 @@ class Chargegroups(AtomAttr): """The charge group for each Atom""" - attrname = 'chargegroups' - singular = 'chargegroup' + + attrname = "chargegroups" + singular = "chargegroup" class GmxTopIterator: @@ -197,17 +198,17 @@ def iter_from_file(self, path): self.file_stack.append(infile) for line in self.clean_file_lines(infile): - if line.startswith('#include'): + if line.startswith("#include"): inc = line.split(None, 1)[1][1:-1] for line in self.iter_from_file(inc): yield line - elif line.startswith('#define'): + elif line.startswith("#define"): self.define(line) - elif line.startswith('#if'): + elif line.startswith("#if"): self.do_if(line, infile) - elif line.startswith('#else'): + elif line.startswith("#else"): self.skip_until_endif(infile) - elif line.startswith('#'): # ignore #if and others + elif line.startswith("#"): # ignore #if and others pass elif line: line = self.substitute_defined(line) @@ -230,42 +231,42 @@ def substitute_defined(self, line): for k, v in self.defines.items(): if k in split: split[split.index(k)] = str(v) - line = ' '.join(split) + line = " ".join(split) return line def clean_file_lines(self, infile): for line in infile: - line = line.split(';')[0].strip() # ; is for comments + line = line.split(";")[0].strip() # ; is for comments yield line def do_if(self, line, infile): ifdef, variable = line.split() - if ifdef == '#ifdef': + if ifdef == "#ifdef": if self.defines.get(variable) in (False, None): self.skip_until_else(infile) - elif ifdef == '#ifndef': + elif ifdef == "#ifndef": if self.defines.get(variable) not in (False, None): self.skip_until_else(infile) def skip_until_else(self, infile): """Skip lines until #if condition ends""" for line in self.clean_file_lines(infile): - if line.startswith('#if'): + if line.startswith("#if"): self.skip_until_endif(infile) - elif line.startswith('#endif') or line.startswith('#else'): + elif line.startswith("#endif") or line.startswith("#else"): break else: - raise IOError('Missing #endif in {}'.format(self.current_file)) + 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): - if line.startswith('#if'): + if line.startswith("#if"): self.skip_until_endif(infile) - elif line.startswith('#endif'): + elif line.startswith("#endif"): break else: - raise IOError('Missing #endif in {}'.format(self.current_file)) + raise IOError("Missing #endif in {}".format(self.current_file)) def find_path(self, path): try: @@ -285,7 +286,7 @@ def find_path(self, path): include_path = os.path.join(self.include_dir, path) if os.path.exists(include_path): return include_path - raise IOError('Could not find {}'.format(path)) + raise IOError("Could not find {}".format(path)) class Molecule: @@ -308,58 +309,78 @@ def __init__(self, name): self.impropers = defaultdict(list) self.parsers = { - 'atoms': self.parse_atoms, - 'bonds': self.parse_bonds, - 'angles': self.parse_angles, - 'dihedrals': self.parse_dihedrals, - 'constraints': self.parse_constraints, - 'settles': self.parse_settles + "atoms": self.parse_atoms, + "bonds": self.parse_bonds, + "angles": self.parse_angles, + "dihedrals": self.parse_dihedrals, + "constraints": self.parse_constraints, + "settles": self.parse_settles, } self.resolved_residue_attrs = False @property def atom_order(self): - return [self.ids, self.types, self.resids, self.resnames, - self.names, self.chargegroups, self.charges, - self.masses] + return [ + self.ids, + self.types, + self.resids, + self.resnames, + self.names, + self.chargegroups, + self.charges, + self.masses, + ] @property def params(self): return [self.bonds, self.angles, self.dihedrals, self.impropers] - + def parse_atoms(self, line): values = line.split() for lst in self.atom_order: try: lst.append(values.pop(0)) except IndexError: # ran out of values - lst.append('') - + lst.append("") + def parse_bonds(self, line): - self.add_param(line, self.bonds, n_funct=2, - funct_values=(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + 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)) - + self.add_param( + line, + self.angles, + n_funct=3, + funct_values=(1, 2, 3, 4, 5, 6, 8, 10), + ) + def parse_dihedrals(self, line): - dih = self.add_param(line, self.dihedrals, n_funct=4, - funct_values=(1, 3, 5, 8, 9, 10, 11)) + dih = self.add_param( + line, + self.dihedrals, + n_funct=4, + funct_values=(1, 3, 5, 8, 9, 10, 11), + ) if not dih: - self.add_param(line, self.impropers, n_funct=4, - funct_values=(2, 4)) + self.add_param( + line, self.impropers, n_funct=4, funct_values=(2, 4) + ) def parse_constraints(self, line): self.add_param(line, self.bonds, n_funct=2, funct_values=(1, 2)) def parse_settles(self, line): - # [ settles ] is a triangular constraint for + # [ settles ] is a triangular constraint for # 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, + # In ITP files this is defined with only the + # oxygen atom index. The next two atoms are + # 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 @@ -370,21 +391,25 @@ def parse_settles(self, line): except ValueError: pass else: - self.bonds[(base, base+1)].append("settles") - self.bonds[(base, base+2)].append("settles") + self.bonds[(base, base + 1)].append("settles") + self.bonds[(base, base + 2)].append("settles") def resolve_residue_attrs(self): """Figure out residue borders and assign moltypes and molnums""" resids = np.array(self.resids, dtype=np.int32) resnames = np.array(self.resnames, dtype=object) - self.residx, (self.resids, resnames) = change_squash((resids,), (resids, resnames)) + self.residx, (self.resids, resnames) = change_squash( + (resids,), (resids, resnames) + ) self.resnames = list(resnames) self.moltypes = [self.name] * len(self.resids) self.molnums = np.array([1] * len(self.resids)) self.resolved_residue_attrs = True - def shift_indices(self, atomid=0, resid=0, molnum=0, cgnr=0, n_res=0, n_atoms=0): + def shift_indices( + self, atomid=0, resid=0, molnum=0, cgnr=0, n_res=0, n_atoms=0 + ): """ Get attributes ready for adding onto a larger topology. @@ -405,26 +430,33 @@ def shift_indices(self, atomid=0, resid=0, molnum=0, cgnr=0, n_res=0, n_atoms=0) if not self.resolved_residue_attrs: self.resolve_residue_attrs() - resids = list(np.array(self.resids)+resid) - residx = list(np.array(self.residx)+n_res) + resids = list(np.array(self.resids) + resid) + residx = list(np.array(self.residx) + n_res) molnums = list(np.array(self.molnums) + molnum) ids = list(np.array(self.ids, dtype=int) + atomid) try: cg = np.array(self.chargegroups, dtype=int) except ValueError: - cg = np.arange(1, len(self.chargegroups)+1) - chargegroups = list(cg+cgnr) - - atom_order = [ids, self.types, resids, self.resnames, - self.names, chargegroups, self.charges, - self.masses] + cg = np.arange(1, len(self.chargegroups) + 1) + chargegroups = list(cg + cgnr) + + atom_order = [ + ids, + self.types, + resids, + self.resnames, + self.names, + chargegroups, + self.charges, + self.masses, + ] new_params = [] for p in self.params: new = {} for indices, values in p.items(): - new[tuple(np.array(indices)+n_atoms)] = values + new[tuple(np.array(indices) + n_atoms)] = values new_params.append(new) return atom_order, new_params, molnums, self.moltypes, residx @@ -491,11 +523,15 @@ class ITPParser(TopologyReaderBase): mass guessing behavior """ - format = 'ITP' - def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', - infer_system=True, - **kwargs): + format = "ITP" + + def parse( + self, + include_dir="/usr/local/gromacs/share/gromacs/top/", + infer_system=True, + **kwargs, + ): """Parse ITP file into Topology Parameters @@ -503,13 +539,13 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', include_dir: str, optional A directory in which to look for other files included from the original file, if the files are not first found - in the current directory. + in the current directory. Default: "/usr/local/gromacs/share/gromacs/top/" infer_system: bool, optional (default True) If a ``[ molecules ]`` directive is not found within the the - topology file, create a Topology with one of every - ``[ moleculetype ]`` defined. If a ``[ molecules ]`` directive is + topology file, create a Topology with one of every + ``[ moleculetype ]`` defined. If a ``[ molecules ]`` directive is found, this keyword is ignored. Returns @@ -522,30 +558,32 @@ 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: self.lines = GmxTopIterator(itpfile, include_dir, kwargs) for line in self.lines: - if '[' in line and ']' in line: - section = line.split('[')[1].split(']')[0].strip() + if "[" in line and "]" in line: + section = line.split("[")[1].split("]")[0].strip() - if section == 'atomtypes': + if section == "atomtypes": self.parser = self.parse_atomtypes - elif section == 'moleculetype': + elif section == "moleculetype": self.parser = self.parse_moleculetype - elif section == 'molecules': + elif section == "molecules": self.parser = self.parse_molecules - + elif self.current_mol: - self.parser = self.current_mol.parsers.get(section, self._pass) + self.parser = self.current_mol.parsers.get( + section, self._pass + ) else: self.parser = self._pass - + else: self.parser(line) @@ -559,23 +597,23 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', self.masses = np.array(self.masses, dtype=object) if not all(self.charges): - empty = self.charges == '' + empty = self.charges == "" self.charges[empty] = [ ( self.atomtypes.get(x)["charge"] if x in self.atomtypes.keys() - else '' + else "" ) for x in self.types[empty] ] if not all(self.masses): - empty = self.masses == '' + empty = self.masses == "" self.masses[empty] = [ ( self.atomtypes.get(x)["mass"] if x in self.atomtypes.keys() - else '' + else "" ) for x in self.types[empty] ] @@ -593,16 +631,18 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', attrs.append(Attr(np.array(vals, dtype=dtype))) if not all(self.masses): - empty = self.masses == '' + empty = self.masses == "" self.masses[empty] = Masses.missing_value_label - attrs.append(Masses(np.array(self.masses, dtype=np.float64), - guessed=False)) + 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)) + 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 " @@ -610,13 +650,15 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', "to the new guessing API. " "This behavior will be removed in release 3.0. " "Please see issue #4698 for more information. ", - DeprecationWarning + DeprecationWarning, ) else: - 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']).") + 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) @@ -627,24 +669,28 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', attrs.append(Resnames(resnames)) attrs.append(Moltypes(np.array(self.moltypes, dtype=object))) attrs.append(Molnums(molnums)) - + n_atoms = len(self.ids) n_residues = len(self.resids) n_segments = len(self.system_molecules) attrs.append(Segids(np.array(self.system_molecules, dtype=object))) - segidx = molnums-1 + segidx = molnums - 1 - top = Topology(n_atoms, n_residues, n_segments, - attrs=attrs, - atom_resindex=self.residx, - residue_segindex=segidx) + top = Topology( + n_atoms, + n_residues, + n_segments, + attrs=attrs, + atom_resindex=self.residx, + residue_segindex=segidx, + ) # connectivity stuff for dct, Attr, attrname in ( - (self.bonds, Bonds, 'bonds'), - (self.angles, Angles, 'angles'), - (self.dihedrals, Dihedrals, 'dihedrals'), - (self.impropers, Impropers, 'impropers') + (self.bonds, Bonds, "bonds"), + (self.angles, Angles, "angles"), + (self.dihedrals, Dihedrals, "dihedrals"), + (self.impropers, Impropers, "impropers"), ): if dct: indices, types = zip(*list(dct.items())) @@ -652,7 +698,7 @@ def parse(self, include_dir='/usr/local/gromacs/share/gromacs/top/', indices, types = [], [] types = [reduce_singular(t) for t in types] - + tattr = Attr(indices, types=types) top.add_TopologyAttr(tattr) @@ -662,17 +708,17 @@ def _pass(self, line): pass def parse_atomtypes(self, line): - keys = ['type_bonded', 'atomic_number', 'mass', 'charge', 'p_type'] + keys = ["type_bonded", "atomic_number", "mass", "charge", "p_type"] fields = line.split() if len(fields[5]) == 1 and fields[5].isalpha(): values = fields[1:6] elif len(fields[3]) == 1 and fields[3].isalpha(): - values = '', '', fields[1], fields[2], fields[3] + values = "", "", fields[1], fields[2], fields[3] elif len(fields[4]) == 1 and fields[4].isalpha(): if fields[1][0].isalpha(): - values = fields[1], '', fields[2], fields[3], fields[4] + values = fields[1], "", fields[2], fields[3], fields[4] else: - values = '', fields[1], fields[2], fields[3], fields[4] + values = "", fields[1], fields[2], fields[3], fields[4] self.atomtypes[fields[0]] = dict(zip(keys, values)) def parse_moleculetype(self, line): @@ -682,7 +728,7 @@ def parse_moleculetype(self, line): def parse_molecules(self, line): name, n_mol = line.split() - self.system_molecules.extend([name]*int(n_mol)) + self.system_molecules.extend([name] * int(n_mol)) def build_system(self): self.ids = [] @@ -697,9 +743,16 @@ def build_system(self): self.molnums = [] self.residx = [] - self.atom_order = [self.ids, self.types, self.resids, self.resnames, - self.names, self.chargegroups, self.charges, - self.masses] + self.atom_order = [ + self.ids, + self.types, + self.resids, + self.resnames, + self.names, + self.chargegroups, + self.charges, + self.masses, + ] self.bonds = defaultdict(list) self.angles = defaultdict(list) @@ -717,9 +770,14 @@ def build_system(self): n_res = len(self.resids) n_atoms = len(self.ids) - shifted = mol.shift_indices(atomid=atomid, resid=resid, - n_res=n_res, cgnr=cgnr, molnum=i, - n_atoms=n_atoms) + shifted = mol.shift_indices( + atomid=atomid, + resid=resid, + n_res=n_res, + cgnr=cgnr, + molnum=i, + n_atoms=n_atoms, + ) atom_order, params, molnums, moltypes, residx = shifted for system_attr, mol_attr in zip(self.atom_order, atom_order): diff --git a/package/MDAnalysis/topology/LAMMPSParser.py b/package/MDAnalysis/topology/LAMMPSParser.py index 2f2ef6ac94a..c7cdcea30b1 100644 --- a/package/MDAnalysis/topology/LAMMPSParser.py +++ b/package/MDAnalysis/topology/LAMMPSParser.py @@ -110,34 +110,36 @@ # Sections will all start with one of these words # and run until the next section title -SECTIONS = set([ - 'Atoms', # Molecular topology sections - 'Velocities', - 'Masses', - 'Ellipsoids', - 'Lines', - 'Triangles', - 'Bodies', - 'Bonds', # Forcefield sections - 'Angles', - 'Dihedrals', - 'Impropers', - 'Pair', - 'Pair LJCoeffs', - 'PairIJ Coeffs', - 'Bond Coeffs', - 'Angle Coeffs', - 'Dihedral Coeffs', - 'Improper Coeffs', - 'BondBond Coeffs', # Class 2 FF sections - 'BondAngle Coeffs', - 'MiddleBondTorsion Coeffs', - 'EndBondTorsion Coeffs', - 'AngleTorsion Coeffs', - 'AngleAngleTorsion Coeffs', - 'BondBond13 Coeffs', - 'AngleAngle Coeffs', -]) +SECTIONS = set( + [ + "Atoms", # Molecular topology sections + "Velocities", + "Masses", + "Ellipsoids", + "Lines", + "Triangles", + "Bodies", + "Bonds", # Forcefield sections + "Angles", + "Dihedrals", + "Impropers", + "Pair", + "Pair LJCoeffs", + "PairIJ Coeffs", + "Bond Coeffs", + "Angle Coeffs", + "Dihedral Coeffs", + "Improper Coeffs", + "BondBond Coeffs", # Class 2 FF sections + "BondAngle Coeffs", + "MiddleBondTorsion Coeffs", + "EndBondTorsion Coeffs", + "AngleTorsion Coeffs", + "AngleAngleTorsion Coeffs", + "BondBond13 Coeffs", + "AngleAngle Coeffs", + ] +) # We usually check by splitting around whitespace, so check # if any SECTION keywords will trip up on this # and add them @@ -146,31 +148,33 @@ SECTIONS.add(val.split()[0]) -HEADERS = set([ - 'atoms', - 'bonds', - 'angles', - 'dihedrals', - 'impropers', - 'atom types', - 'bond types', - 'angle types', - 'dihedral types', - 'improper types', - 'extra bond per atom', - 'extra angle per atom', - 'extra dihedral per atom', - 'extra improper per atom', - 'extra special per atom', - 'ellipsoids', - 'lines', - 'triangles', - 'bodies', - 'xlo xhi', - 'ylo yhi', - 'zlo zhi', - 'xy xz yz', -]) +HEADERS = set( + [ + "atoms", + "bonds", + "angles", + "dihedrals", + "impropers", + "atom types", + "bond types", + "angle types", + "dihedral types", + "improper types", + "extra bond per atom", + "extra angle per atom", + "extra dihedral per atom", + "extra improper per atom", + "extra special per atom", + "ellipsoids", + "lines", + "triangles", + "bodies", + "xlo xhi", + "ylo yhi", + "zlo zhi", + "xy xz yz", + ] +) class DATAParser(TopologyReaderBase): @@ -192,12 +196,13 @@ class as the topology and coordinate reader share many common through universe.guess_TopologyAttrs() API). """ - format = 'DATA' + + format = "DATA" def iterdata(self): with openany(self.filename) as f: for line in f: - line = line.partition('#')[0].strip() + line = line.partition("#")[0].strip() if line: yield line @@ -211,19 +216,19 @@ def grab_datafile(self): """ f = list(self.iterdata()) - starts = [i for i, line in enumerate(f) - if line.split()[0] in SECTIONS] + starts = [i for i, line in enumerate(f) if line.split()[0] in SECTIONS] starts += [None] header = {} - for line in f[:starts[0]]: + for line in f[: starts[0]]: for token in HEADERS: if line.endswith(token): header[token] = line.split(token)[0] continue - sects = {f[l]:f[l+1:starts[i+1]] - for i, l in enumerate(starts[:-1])} + sects = { + f[l]: f[l + 1 : starts[i + 1]] for i, l in enumerate(starts[:-1]) + } return header, sects @@ -248,7 +253,7 @@ def _interpret_atom_style(atom_style): atom_style = atom_style.split() - for attr in ['id', 'type', 'resid', 'charge', 'x', 'y', 'z']: + for attr in ["id", "type", "resid", "charge", "x", "y", "z"]: try: location = atom_style.index(attr) except ValueError: @@ -256,11 +261,13 @@ def _interpret_atom_style(atom_style): else: style_dict[attr] = location - reqd_attrs = ['id', 'type', 'x', 'y', 'z'] + reqd_attrs = ["id", "type", "x", "y", "z"] missing_attrs = [attr for attr in reqd_attrs if attr not in style_dict] if missing_attrs: - raise ValueError("atom_style string missing required field(s): {}" - "".format(', '.join(missing_attrs))) + raise ValueError( + "atom_style string missing required field(s): {}" + "".format(", ".join(missing_attrs)) + ) return style_dict @@ -273,40 +280,43 @@ def parse(self, **kwargs): """ # Can pass atom_style to help parsing try: - self.style_dict = self._interpret_atom_style(kwargs['atom_style']) + self.style_dict = self._interpret_atom_style(kwargs["atom_style"]) except KeyError: self.style_dict = None head, sects = self.grab_datafile() try: - masses = self._parse_masses(sects['Masses']) + masses = self._parse_masses(sects["Masses"]) except KeyError: masses = None - if 'Atoms' not in sects: + if "Atoms" not in sects: raise ValueError("Data file was missing Atoms section") try: - top = self._parse_atoms(sects['Atoms'], masses) + top = self._parse_atoms(sects["Atoms"], masses) except Exception: errmsg = ( "Failed to parse atoms section. You can supply a description " "of the atom_style as a keyword argument, " - "eg mda.Universe(..., atom_style='id resid x y z')") + "eg mda.Universe(..., atom_style='id resid x y z')" + ) raise ValueError(errmsg) from None # create mapping of id to index (ie atom id 10 might be the 0th atom) mapping = {atom_id: i for i, atom_id in enumerate(top.ids.values)} for attr, L, nentries in [ - (Bonds, 'Bonds', 2), - (Angles, 'Angles', 3), - (Dihedrals, 'Dihedrals', 4), - (Impropers, 'Impropers', 4) + (Bonds, "Bonds", 2), + (Angles, "Angles", 3), + (Dihedrals, "Dihedrals", 4), + (Impropers, "Impropers", 4), ]: try: - type, sect = self._parse_bond_section(sects[L], nentries, mapping) + type, sect = self._parse_bond_section( + sects[L], nentries, mapping + ) except KeyError: type, sect = [], [] @@ -314,8 +324,9 @@ def parse(self, **kwargs): return top - def read_DATA_timestep(self, n_atoms, TS_class, TS_kwargs, - atom_style=None): + def read_DATA_timestep( + self, n_atoms, TS_class, TS_kwargs, atom_style=None + ): """Read a DATA file and try and extract x, v, box. - positions @@ -338,19 +349,19 @@ def read_DATA_timestep(self, n_atoms, TS_class, TS_kwargs, unitcell = self._parse_box(header) try: - positions, ordering = self._parse_pos(sects['Atoms']) + positions, ordering = self._parse_pos(sects["Atoms"]) except KeyError as err: errmsg = f"Position information not found: {err}" raise IOError(errmsg) from None - if 'Velocities' in sects: - velocities = self._parse_vel(sects['Velocities'], ordering) + if "Velocities" in sects: + velocities = self._parse_vel(sects["Velocities"], ordering) else: velocities = None - ts = TS_class.from_coordinates(positions, - velocities=velocities, - **TS_kwargs) + ts = TS_class.from_coordinates( + positions, velocities=velocities, **TS_kwargs + ) ts.dimensions = unitcell return ts @@ -365,20 +376,22 @@ def _parse_pos(self, datalines): if self.style_dict is None: if len(datalines[0].split()) in (7, 10): - style_dict = {'id': 0, 'x': 4, 'y': 5, 'z': 6} + style_dict = {"id": 0, "x": 4, "y": 5, "z": 6} else: - style_dict = {'id': 0, 'x': 3, 'y': 4, 'z': 5} + 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() - ids[i] = line[style_dict['id']] + ids[i] = line[style_dict["id"]] - pos[i, :] = [line[style_dict['x']], - line[style_dict['y']], - line[style_dict['z']]] + pos[i, :] = [ + line[style_dict["x"]], + line[style_dict["y"]], + line[style_dict["z"]], + ] order = np.argsort(ids) pos = pos[order] @@ -435,7 +448,9 @@ def _parse_bond_section(self, datalines, nentries, mapping): for line in datalines: line = line.split() # map to 0 based int - section.append(tuple([mapping[int(x)] for x in line[2:2 + nentries]])) + section.append( + tuple([mapping[int(x)] for x in line[2 : 2 + nentries]]) + ) type.append(line[1]) return tuple(type), tuple(section) @@ -471,18 +486,16 @@ def _parse_atoms(self, datalines, massdict=None): n_atoms = len(datalines) if self.style_dict is None: - sd = {'id': 0, - 'resid': 1, - 'type': 2} + sd = {"id": 0, "resid": 1, "type": 2} # Fields per line n = len(datalines[0].split()) if n in (7, 10): - sd['charge'] = 3 + sd["charge"] = 3 else: sd = self.style_dict - has_charge = 'charge' in sd - has_resid = 'resid' in sd + has_charge = "charge" in sd + has_resid = "resid" in sd # atom ids aren't necessarily sequential atom_ids = np.zeros(n_atoms, dtype=np.int32) @@ -500,12 +513,12 @@ def _parse_atoms(self, datalines, massdict=None): # these numpy array are already typed correctly, # so just pass the raw strings # and let numpy handle the conversion - atom_ids[i] = line[sd['id']] + atom_ids[i] = line[sd["id"]] if has_resid: - resids[i] = line[sd['resid']] - types[i] = line[sd['type']] + resids[i] = line[sd["resid"]] + types[i] = line[sd["type"]] if has_charge: - charges[i] = line[sd['charge']] + charges[i] = line[sd["charge"]] # at this point, we've read the atoms section, # but it's still (potentially) unordered @@ -536,11 +549,11 @@ def _parse_atoms(self, datalines, massdict=None): attrs.append(Atomids(atom_ids)) attrs.append(Resids(resids)) attrs.append(Resnums(resids.copy())) - attrs.append(Segids(np.array(['SYSTEM'], dtype=object))) + attrs.append(Segids(np.array(["SYSTEM"], dtype=object))) - top = Topology(n_atoms, n_residues, 1, - attrs=attrs, - atom_resindex=residx) + top = Topology( + n_atoms, n_residues, 1, attrs=attrs, atom_resindex=residx + ) return top @@ -559,18 +572,18 @@ def _parse_masses(self, datalines): return masses def _parse_box(self, header): - x1, x2 = np.float32(header['xlo xhi'].split()) + x1, x2 = np.float32(header["xlo xhi"].split()) x = x2 - x1 - y1, y2 = np.float32(header['ylo yhi'].split()) + y1, y2 = np.float32(header["ylo yhi"].split()) y = y2 - y1 - z1, z2 = np.float32(header['zlo zhi'].split()) + z1, z2 = np.float32(header["zlo zhi"].split()) z = z2 - z1 - if 'xy xz yz' in header: + if "xy xz yz" in header: # Triclinic unitcell = np.zeros((3, 3), dtype=np.float32) - xy, xz, yz = np.float32(header['xy xz yz'].split()) + xy, xz, yz = np.float32(header["xy xz yz"].split()) unitcell[0][0] = x unitcell[1][0] = xy @@ -584,7 +597,7 @@ def _parse_box(self, header): # Orthogonal unitcell = np.zeros(6, dtype=np.float32) unitcell[:3] = x, y, z - unitcell[3:] = 90., 90., 90. + unitcell[3:] = 90.0, 90.0, 90.0 return unitcell @@ -598,7 +611,8 @@ class LammpsDumpParser(TopologyReaderBase): .. versionchanged:: 2.0.0 .. versionadded:: 0.19.0 """ - format = 'LAMMPSDUMP' + + format = "LAMMPSDUMP" def parse(self, **kwargs): with openany(self.filename) as fin: @@ -634,17 +648,25 @@ def parse(self, **kwargs): attrs.append(Atomids(indices)) attrs.append(Atomtypes(types)) attrs.append(Masses(np.ones(natoms, dtype=np.float64), guessed=True)) - warnings.warn('Guessed all Masses to 1.0') + warnings.warn("Guessed all Masses to 1.0") attrs.append(Resids(np.array([1], dtype=int))) attrs.append(Resnums(np.array([1], dtype=int))) - attrs.append(Segids(np.array(['SYSTEM'], dtype=object))) + attrs.append(Segids(np.array(["SYSTEM"], dtype=object))) return Topology(natoms, 1, 1, attrs=attrs) @functools.total_ordering class LAMMPSAtom(object): # pragma: no cover - __slots__ = ("index", "name", "type", "chainid", "charge", "mass", "_positions") + __slots__ = ( + "index", + "name", + "type", + "chainid", + "charge", + "mass", + "_positions", + ) def __init__(self, index, name, type, chain_id, charge=0, mass=1): self.index = index @@ -655,8 +677,15 @@ def __init__(self, index, name, type, chain_id, charge=0, mass=1): self.mass = mass def __repr__(self): - return "" + return ( + "" + ) def __lt__(self, other): return self.index < other.index @@ -668,12 +697,22 @@ def __hash__(self): return hash(self.index) def __getattr__(self, attr): - if attr == 'pos': + if attr == "pos": return self._positions[self.index] else: super(LAMMPSAtom, self).__getattribute__(attr) def __iter__(self): pos = self.pos - return iter((self.index + 1, self.chainid, self.type, self.charge, - self.mass, pos[0], pos[1], pos[2])) + return iter( + ( + self.index + 1, + self.chainid, + self.type, + self.charge, + self.mass, + pos[0], + pos[1], + pos[2], + ) + ) diff --git a/package/MDAnalysis/topology/MMTFParser.py b/package/MDAnalysis/topology/MMTFParser.py index 3abc6a281cb..90af2f46d13 100644 --- a/package/MDAnalysis/topology/MMTFParser.py +++ b/package/MDAnalysis/topology/MMTFParser.py @@ -106,15 +106,15 @@ def _parse_mmtf(fn): - if fn.endswith('gz'): + if fn.endswith("gz"): return mmtf.parse_gzip(fn) else: return mmtf.parse(fn) class Models(SegmentAttr): - attrname = 'models' - singular = 'model' + attrname = "models" + singular = "model" transplants = defaultdict(list) def models(self): @@ -130,16 +130,16 @@ def models(self): """ model_ids = np.unique(self.segments.models) - return [self.select_atoms('model {}'.format(i)) - for i in model_ids] + return [self.select_atoms("model {}".format(i)) for i in model_ids] - transplants['Universe'].append( - ('models', property(models, None, None, models.__doc__))) + transplants["Universe"].append( + ("models", property(models, None, None, models.__doc__)) + ) class ModelSelection(RangeSelection): - token = 'model' - field = 'models' + token = "model" + field = "models" def apply(self, group): mask = np.zeros(len(group), dtype=bool) @@ -157,7 +157,7 @@ def apply(self, group): class MMTFParser(base.TopologyReaderBase): - format = 'MMTF' + format = "MMTF" @staticmethod def _format_hint(thing): @@ -168,9 +168,9 @@ def _format_hint(thing): return isinstance(thing, mmtf.MMTFDecoder) @due.dcite( - Doi('10.1371/journal.pcbi.1005575'), + Doi("10.1371/journal.pcbi.1005575"), description="MMTF Parser", - path='MDAnalysis.topology.MMTFParser', + path="MDAnalysis.topology.MMTFParser", ) def parse(self, **kwargs): if isinstance(self.filename, mmtf.MMTFDecoder): @@ -191,45 +191,58 @@ def iter_atoms(field): attrs = [] # required - charges = Charges(list(iter_atoms('formalChargeList'))) - names = Atomnames(list(iter_atoms('atomNameList'))) - types = Atomtypes(list(iter_atoms('elementList'))) + charges = Charges(list(iter_atoms("formalChargeList"))) + names = Atomnames(list(iter_atoms("atomNameList"))) + types = Atomtypes(list(iter_atoms("elementList"))) attrs.extend([charges, names, types]) - #optional are empty list if empty, sometimes arrays + # optional are empty list if empty, sometimes arrays if len(mtop.atom_id_list): attrs.append(Atomids(mtop.atom_id_list)) else: # must have this attribute for MDA attrs.append(Atomids(np.arange(natoms), guessed=True)) if mtop.alt_loc_list: - attrs.append(AltLocs([val.replace('\x00', '').strip() - for val in mtop.alt_loc_list])) + attrs.append( + AltLocs( + [ + val.replace("\x00", "").strip() + for val in mtop.alt_loc_list + ] + ) + ) else: - attrs.append(AltLocs(['']*natoms)) + attrs.append(AltLocs([""] * natoms)) if len(mtop.b_factor_list): attrs.append(Tempfactors(mtop.b_factor_list)) else: - attrs.append(Tempfactors([0]*natoms)) + attrs.append(Tempfactors([0] * natoms)) if len(mtop.occupancy_list): attrs.append(Occupancies(mtop.occupancy_list)) else: - attrs.append(Occupancies([1]*natoms)) + attrs.append(Occupancies([1] * natoms)) # Residue things # required resids = Resids(mtop.group_id_list) resnums = Resnums(resids.values.copy()) - resnames = Resnames([mtop.group_list[i]['groupName'] - for i in mtop.group_type_list]) + resnames = Resnames( + [mtop.group_list[i]["groupName"] for i in mtop.group_type_list] + ) attrs.extend([resids, resnums, resnames]) # optional # mmtf empty icode is '\x00' rather than '' if mtop.ins_code_list: - attrs.append(ICodes([val.replace('\x00', '').strip() - for val in mtop.ins_code_list])) + attrs.append( + ICodes( + [ + val.replace("\x00", "").strip() + for val in mtop.ins_code_list + ] + ) + ) else: - attrs.append(ICodes(['']*nresidues)) + attrs.append(ICodes([""] * nresidues)) # Segment things # optional @@ -237,18 +250,21 @@ def iter_atoms(field): attrs.append(Segids(mtop.chain_name_list)) else: # required for MDAnalysis - attrs.append(Segids(['SYSTEM'] * nsegments, guessed=True)) + attrs.append(Segids(["SYSTEM"] * nsegments, guessed=True)) mods = np.repeat(np.arange(mtop.num_models), mtop.chains_per_model) attrs.append(Models(mods)) - #attrs.append(chainids) + # attrs.append(chainids) # number of atoms in a given group id - groupID_2_natoms = {i:len(g['atomNameList']) - for i, g in enumerate(mtop.group_list)} + groupID_2_natoms = { + i: len(g["atomNameList"]) for i, g in enumerate(mtop.group_list) + } # mapping of atoms to residues - resindex = np.repeat(np.arange(nresidues), - [groupID_2_natoms[i] for i in mtop.group_type_list]) + resindex = np.repeat( + np.arange(nresidues), + [groupID_2_natoms[i] for i in mtop.group_type_list], + ) # mapping of residues to segments segindex = np.repeat(np.arange(nsegments), mtop.groups_per_chain) @@ -260,7 +276,7 @@ def iter_atoms(field): bonds = [] for gtype in mtop.group_type_list: g = mtop.group_list[gtype] - bondlist = g['bondAtomList'] + bondlist = g["bondAtomList"] for x, y in zip(bondlist[1::2], bondlist[::2]): if x > y: @@ -270,16 +286,21 @@ def iter_atoms(field): offset += groupID_2_natoms[gtype] # add inter group bonds if not mtop.bond_atom_list is None: # optional field - for x, y in zip(mtop.bond_atom_list[1::2], - mtop.bond_atom_list[::2]): + for x, y in zip( + mtop.bond_atom_list[1::2], mtop.bond_atom_list[::2] + ): if x > y: x, y = y, x bonds.append((x, y)) attrs.append(Bonds(bonds)) - top = Topology(natoms, nresidues, nsegments, - atom_resindex=resindex, - residue_segindex=segindex, - attrs=attrs) + top = Topology( + natoms, + nresidues, + nsegments, + atom_resindex=resindex, + residue_segindex=segindex, + attrs=attrs, + ) return top diff --git a/package/MDAnalysis/topology/MOL2Parser.py b/package/MDAnalysis/topology/MOL2Parser.py index 5c81e7346c6..114633c0c41 100644 --- a/package/MDAnalysis/topology/MOL2Parser.py +++ b/package/MDAnalysis/topology/MOL2Parser.py @@ -137,7 +137,8 @@ class MOL2Parser(TopologyReaderBase): through universe.guess_TopologyAttrs() API). """ - format = 'MOL2' + + format = "MOL2" def parse(self, **kwargs): """Parse MOL2 file *filename* and return the dict `structure`. @@ -159,8 +160,10 @@ def parse(self, **kwargs): blocks[-1]["lines"].append(line) if not len(blocks): - raise ValueError("The mol2 file '{0}' needs to have at least one" - " @MOLECULE block".format(self.filename)) + raise ValueError( + "The mol2 file '{0}' needs to have at least one" + " @MOLECULE block".format(self.filename) + ) block = blocks[0] sections = {} @@ -178,8 +181,11 @@ def parse(self, **kwargs): atom_lines, bond_lines = sections["atom"], sections.get("bond") if not len(atom_lines): - raise ValueError("The mol2 block ({0}:{1}) has no atoms".format( - os.path.basename(self.filename), block["start_line"])) + raise ValueError( + "The mol2 block ({0}:{1}) has no atoms".format( + os.path.basename(self.filename), block["start_line"] + ) + ) ids = [] names = [] @@ -187,36 +193,43 @@ def parse(self, **kwargs): resids = [] resnames = [] charges = [] - has_charges = sections['molecule'][3].strip() != 'NO_CHARGES' + has_charges = sections["molecule"][3].strip() != "NO_CHARGES" for a in atom_lines: columns = a.split() if len(columns) >= 9: - aid, name, x, y, z, atom_type, \ - resid, resname, charge = columns[:9] + aid, name, x, y, z, atom_type, resid, resname, charge = ( + columns[:9] + ) elif len(columns) < 6: - raise ValueError(f"The @ATOM block in mol2 file" - f" {os.path.basename(self.filename)}" - f" should have at least 6 fields to be" - f" unpacked: atom_id atom_name x y z" - f" atom_type [subst_id[subst_name" - f" [charge [status_bit]]]]") + raise ValueError( + f"The @ATOM block in mol2 file" + f" {os.path.basename(self.filename)}" + f" should have at least 6 fields to be" + f" unpacked: atom_id atom_name x y z" + f" atom_type [subst_id[subst_name" + f" [charge [status_bit]]]]" + ) else: aid, name, x, y, z, atom_type = columns[:6] id_name_charge = [1, None, None] for i in range(6, len(columns)): - id_name_charge[i-6] = columns[i] + id_name_charge[i - 6] = columns[i] resid, resname, charge = id_name_charge if has_charges: if charge is None: - raise ValueError(f"The mol2 file {self.filename}" - f" indicates a charge model" - f"{sections['molecule'][3]}, but" - f" no charge provided in line: {a}") + raise ValueError( + f"The mol2 file {self.filename}" + f" indicates a charge model" + f"{sections['molecule'][3]}, but" + f" no charge provided in line: {a}" + ) else: if charge is not None: - raise ValueError(f"The mol2 file {self.filename}" - f" indicates no charges, but charge" - f" {charge} provided in line: {a}.") + raise ValueError( + f"The mol2 file {self.filename}" + f" indicates no charges, but charge" + f" {charge} provided in line: {a}." + ) ids.append(aid) names.append(name) @@ -234,13 +247,15 @@ def parse(self, **kwargs): validated_elements[i] = SYBYL2SYMB[at] else: invalid_elements.add(at) - validated_elements[i] = '' + validated_elements[i] = "" # Print single warning for all unknown elements, if any if invalid_elements: - warnings.warn("Unknown elements found for some " - f"atoms: {invalid_elements}. " - "These have been given an empty element record.") + warnings.warn( + "Unknown elements found for some " + f"atoms: {invalid_elements}. " + "These have been given an empty element record." + ) attrs = [] attrs.append(Atomids(np.array(ids, dtype=np.int32))) @@ -249,29 +264,32 @@ def parse(self, **kwargs): if has_charges: attrs.append(Charges(np.array(charges, dtype=np.float32))) - if not np.all(validated_elements == ''): + if not np.all(validated_elements == ""): attrs.append(Elements(validated_elements)) resids = np.array(resids, dtype=np.int32) resnames = np.array(resnames, dtype=object) if np.all(resnames): - residx, resids, (resnames,) = squash_by( - resids, resnames) + residx, resids, (resnames,) = squash_by(resids, resnames) n_residues = len(resids) attrs.append(Resids(resids)) attrs.append(Resnums(resids.copy())) attrs.append(Resnames(resnames)) elif not np.any(resnames): - residx, resids, _ = squash_by(resids,) + residx, resids, _ = squash_by( + resids, + ) n_residues = len(resids) attrs.append(Resids(resids)) attrs.append(Resnums(resids.copy())) else: - raise ValueError(f"Some atoms in the mol2 file {self.filename}" - f" have subst_name while some do not.") + raise ValueError( + f"Some atoms in the mol2 file {self.filename}" + f" have subst_name while some do not." + ) - attrs.append(Segids(np.array(['SYSTEM'], dtype=object))) + attrs.append(Segids(np.array(["SYSTEM"], dtype=object))) # don't add Bonds if there are none (Issue #3057) if bond_lines: @@ -287,8 +305,8 @@ def parse(self, **kwargs): bonds.append(bond) attrs.append(Bonds(bonds, order=bondorder)) - top = Topology(n_atoms, n_residues, 1, - attrs=attrs, - atom_resindex=residx) + top = Topology( + n_atoms, n_residues, 1, attrs=attrs, atom_resindex=residx + ) return top diff --git a/package/MDAnalysis/topology/MinimalParser.py b/package/MDAnalysis/topology/MinimalParser.py index ce0598bf3e7..39f25f66ef7 100644 --- a/package/MDAnalysis/topology/MinimalParser.py +++ b/package/MDAnalysis/topology/MinimalParser.py @@ -56,12 +56,13 @@ class MinimalParser(TopologyReaderBase): This requires that the coordinate format has """ - format = 'MINIMAL' + + format = "MINIMAL" def parse(self, **kwargs): """Return the minimal *Topology* object""" try: - n_atoms = kwargs['n_atoms'] + n_atoms = kwargs["n_atoms"] except KeyError: reader = get_reader_for(self.filename) n_atoms = reader.parse_n_atoms(self.filename, **kwargs) diff --git a/package/MDAnalysis/topology/PDBQTParser.py b/package/MDAnalysis/topology/PDBQTParser.py index 88c3fe3ba40..f3b5c5fa6c6 100644 --- a/package/MDAnalysis/topology/PDBQTParser.py +++ b/package/MDAnalysis/topology/PDBQTParser.py @@ -111,10 +111,11 @@ class PDBQTParser(TopologyReaderBase): Removed mass guessing (attributes guessing takes place now through universe.guess_TopologyAttrs() API). - .. _reference: + .. _reference: https://autodock.scripps.edu/wp-content/uploads/sites/56/2021/10/AutoDock4.2.6_UserGuide.pdf """ - format = 'PDBQT' + + format = "PDBQT" def parse(self, **kwargs): """Parse atom information from PDBQT file *filename*. @@ -139,7 +140,7 @@ def parse(self, **kwargs): with util.openany(self.filename) as f: for line in f: line = line.strip() - if not line.startswith(('ATOM', 'HETATM')): + if not line.startswith(("ATOM", "HETATM")): continue record_types.append(line[:6].strip()) serials.append(int(line[6:11])) @@ -158,14 +159,14 @@ def parse(self, **kwargs): attrs = [] for attrlist, Attr, dtype in ( - (record_types, RecordTypes, object), - (serials, Atomids, np.int32), - (names, Atomnames, object), - (altlocs, AltLocs, object), - (occupancies, Occupancies, np.float32), - (tempfactors, Tempfactors, np.float32), - (charges, Charges, np.float32), - (atomtypes, Atomtypes, object), + (record_types, RecordTypes, object), + (serials, Atomids, np.int32), + (names, Atomnames, object), + (altlocs, AltLocs, object), + (occupancies, Occupancies, np.float32), + (tempfactors, Tempfactors, np.float32), + (charges, Charges, np.float32), + (atomtypes, Atomtypes, object), ): attrs.append(Attr(np.array(attrlist, dtype=dtype))) @@ -177,7 +178,8 @@ def parse(self, **kwargs): attrs.append(ChainIDs(chainids)) residx, (resids, icodes, resnames, chainids) = change_squash( - (resids, icodes), (resids, icodes, resnames, chainids)) + (resids, icodes), (resids, icodes, resnames, chainids) + ) n_residues = len(resids) attrs.append(Resids(resids)) attrs.append(Resnums(resids.copy())) @@ -188,9 +190,13 @@ def parse(self, **kwargs): n_segments = len(segids) attrs.append(Segids(segids)) - top = Topology(n_atoms, n_residues, n_segments, - attrs=attrs, - atom_resindex=residx, - residue_segindex=segidx) + top = Topology( + n_atoms, + n_residues, + n_segments, + attrs=attrs, + atom_resindex=residx, + residue_segindex=segidx, + ) return top diff --git a/package/MDAnalysis/topology/PQRParser.py b/package/MDAnalysis/topology/PQRParser.py index 65c98a70d6e..496ddfb1412 100644 --- a/package/MDAnalysis/topology/PQRParser.py +++ b/package/MDAnalysis/topology/PQRParser.py @@ -103,7 +103,8 @@ class PQRParser(TopologyReaderBase): through universe.guess_TopologyAttrs() API). """ - format = 'PQR' + + format = "PQR" @staticmethod def guess_flavour(line): @@ -126,11 +127,11 @@ def guess_flavour(line): try: float(fields[-1]) except ValueError: - flavour = 'GROMACS' + flavour = "GROMACS" else: - flavour = 'ORIGINAL' + flavour = "ORIGINAL" else: - flavour = 'NO_CHAINID' + flavour = "NO_CHAINID" return flavour def parse(self, **kwargs): @@ -161,20 +162,50 @@ def parse(self, **kwargs): if flavour is None: flavour = self.guess_flavour(line) - if flavour == 'ORIGINAL': - (recordName, serial, name, resName, - chainID, resSeq, x, y, z, charge, - radius) = fields - elif flavour == 'GROMACS': - (recordName, serial, name, resName, - resSeq, x, y, z, charge, - radius, element) = fields + if flavour == "ORIGINAL": + ( + recordName, + serial, + name, + resName, + chainID, + resSeq, + x, + y, + z, + charge, + radius, + ) = fields + elif flavour == "GROMACS": + ( + recordName, + serial, + name, + resName, + resSeq, + x, + y, + z, + charge, + radius, + element, + ) = fields chainID = "SYSTEM" elements.append(element) - elif flavour == 'NO_CHAINID': + elif flavour == "NO_CHAINID": # files without the chainID - (recordName, serial, name, resName, - resSeq, x, y, z, charge, radius) = fields + ( + recordName, + serial, + name, + resName, + resSeq, + x, + y, + z, + charge, + radius, + ) = fields chainID = "SYSTEM" try: @@ -184,7 +215,7 @@ def parse(self, **kwargs): resid = int(resSeq[:-1]) icode = resSeq[-1] else: - icode = '' + icode = "" record_types.append(recordName) serials.append(serial) @@ -216,7 +247,8 @@ def parse(self, **kwargs): residx, (resids, resnames, icodes, chainIDs) = change_squash( (resids, resnames, icodes, chainIDs), - (resids, resnames, icodes, chainIDs)) + (resids, resnames, icodes, chainIDs), + ) n_residues = len(resids) attrs.append(Resids(resids)) @@ -229,9 +261,13 @@ def parse(self, **kwargs): n_segments = len(chainIDs) attrs.append(Segids(chainIDs)) - top = Topology(n_atoms, n_residues, n_segments, - attrs=attrs, - atom_resindex=residx, - residue_segindex=segidx) + top = Topology( + n_atoms, + n_residues, + n_segments, + attrs=attrs, + atom_resindex=residx, + residue_segindex=segidx, + ) return top diff --git a/package/MDAnalysis/topology/PSFParser.py b/package/MDAnalysis/topology/PSFParser.py index f247544262d..531c2ae592d 100644 --- a/package/MDAnalysis/topology/PSFParser.py +++ b/package/MDAnalysis/topology/PSFParser.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 @@ -64,7 +64,7 @@ Bonds, Angles, Dihedrals, - Impropers + Impropers, ) from ..core.topology import Topology @@ -94,7 +94,8 @@ class PSFParser(TopologyReaderBase): .. versionchanged:: 2.8.0 PSFParser now reads string resids and converts them to integers. """ - format = 'PSF' + + format = "PSF" def parse(self, **kwargs): """Parse PSF file into Topology @@ -107,18 +108,19 @@ def parse(self, **kwargs): with openany(self.filename) as psffile: header = next(psffile) if not header.startswith("PSF"): - err = ("{0} is not valid PSF file (header = {1})" - "".format(self.filename, header)) + err = "{0} is not valid PSF file (header = {1})" "".format( + self.filename, header + ) logger.error(err) raise ValueError(err) header_flags = header[3:].split() if "NAMD" in header_flags: - self._format = "NAMD" # NAMD/VMD + self._format = "NAMD" # NAMD/VMD elif "EXT" in header_flags: - self._format = "EXTENDED" # CHARMM + self._format = "EXTENDED" # CHARMM else: - self._format = "STANDARD" # CHARMM + self._format = "STANDARD" # CHARMM next(psffile) title = next(psffile).split() @@ -129,28 +131,28 @@ def parse(self, **kwargs): # psfremarks = [psffile.next() for i in range(int(title[0]))] for _ in range(int(title[0])): next(psffile) - logger.debug("PSF file {0}: format {1}" - "".format(self.filename, self._format)) + logger.debug( + "PSF file {0}: format {1}" + "".format(self.filename, self._format) + ) # Atoms first and mandatory - top = self._parse_sec( - psffile, ('NATOM', 1, 1, self._parseatoms)) + top = self._parse_sec(psffile, ("NATOM", 1, 1, self._parseatoms)) # Then possibly other sections sections = ( - #("atoms", ("NATOM", 1, 1, self._parseatoms)), + # ("atoms", ("NATOM", 1, 1, self._parseatoms)), (Bonds, ("NBOND", 2, 4, self._parsesection)), (Angles, ("NTHETA", 3, 3, self._parsesection)), (Dihedrals, ("NPHI", 4, 2, self._parsesection)), (Impropers, ("NIMPHI", 4, 2, self._parsesection)), - #("donors", ("NDON", 2, 4, self._parsesection)), - #("acceptors", ("NACC", 2, 4, self._parsesection)) + # ("donors", ("NDON", 2, 4, self._parsesection)), + # ("acceptors", ("NACC", 2, 4, self._parsesection)) ) try: for attr, info in sections: next(psffile) - top.add_TopologyAttr( - attr(self._parse_sec(psffile, info))) + top.add_TopologyAttr(attr(self._parse_sec(psffile, info))) except StopIteration: # Reached the end of the file before we expected for attr in (Bonds, Angles, Dihedrals, Impropers): @@ -173,15 +175,16 @@ def _parse_sec(self, psffile, section_info): header = header.split() # Get the number num = float(header[0]) - sect_type = header[1].strip('!:') + sect_type = header[1].strip("!:") # Make sure the section type matches the desc if not sect_type == desc: - err = ("Expected section {0} but found {1}" - "".format(desc, sect_type)) + err = "Expected section {0} but found {1}" "".format( + desc, sect_type + ) logger.error(err) raise ValueError(err) # Now figure out how many lines to read - numlines = int(ceil(num/per_line)) + numlines = int(ceil(num / per_line)) psffile_next = functools.partial(next, psffile) return parsefunc(psffile_next, atoms_per, numlines) @@ -238,23 +241,42 @@ def _parseatoms(self, lines, atoms_per, numlines): """ # how to partition the line into the individual atom components atom_parsers = { - 'STANDARD': lambda l: - (l[:8], l[9:13].strip() or "SYSTEM", l[14:18], - l[19:23].strip(), l[24:28].strip(), - l[29:33].strip(), l[34:48], l[48:62]), + "STANDARD": lambda l: ( + l[:8], + l[9:13].strip() or "SYSTEM", + l[14:18], + l[19:23].strip(), + l[24:28].strip(), + l[29:33].strip(), + l[34:48], + l[48:62], + ), # l[62:70], l[70:84], l[84:98] ignore IMOVE, ECH and EHA, - 'EXTENDED': lambda l: - (l[:10], l[11:19].strip() or "SYSTEM", l[20:28], - l[29:37].strip(), l[38:46].strip(), - l[47:51].strip(), l[52:66], l[66:70]), + "EXTENDED": lambda l: ( + l[:10], + l[11:19].strip() or "SYSTEM", + l[20:28], + l[29:37].strip(), + l[38:46].strip(), + l[47:51].strip(), + l[52:66], + l[66:70], + ), # l[70:78], l[78:84], l[84:98] ignore IMOVE, ECH and EHA, - 'NAMD': lambda l: l.split()[:8], + "NAMD": lambda l: l.split()[:8], } atom_parser = atom_parsers[self._format] # once partitioned, assigned each component the correct type - set_type = lambda x: (int(x[0]) - 1, x[1] or "SYSTEM", - atoi(x[2]), x[3], - x[4], x[5], float(x[6]), float(x[7])) + set_type = lambda x: ( + int(x[0]) - 1, + x[1] or "SYSTEM", + atoi(x[2]), + x[3], + x[4], + x[5], + float(x[6]), + float(x[7]), + ) # Oli: I don't think that this is the correct OUTPUT format: # psf_atom_format = " %5d %4s %4d %4s %-4s %-4s %10.6f %7.4f%s\n" @@ -289,13 +311,17 @@ def _parseatoms(self, lines, atoms_per, numlines): except ValueError: # last ditch attempt: this *might* be a NAMD/VMD # space-separated "PSF" file from VMD version < 1.9.1 - atom_parser = atom_parsers['NAMD'] + atom_parser = atom_parsers["NAMD"] vals = set_type(atom_parser(line)) - logger.warning("Guessing that this is actually a" - " NAMD-type PSF file..." - " continuing with fingers crossed!") - logger.debug("First NAMD-type line: {0}: {1}" - "".format(i, line.rstrip())) + logger.warning( + "Guessing that this is actually a" + " NAMD-type PSF file..." + " continuing with fingers crossed!" + ) + logger.debug( + "First NAMD-type line: {0}: {1}" + "".format(i, line.rstrip()) + ) atomids[i] = vals[0] segids[i] = vals[1] @@ -316,8 +342,8 @@ def _parseatoms(self, lines, atoms_per, numlines): # Residue # resids, resnames residx, (new_resids, new_resnames, perres_segids) = change_squash( - (resids, resnames, segids), - (resids, resnames, segids)) + (resids, resnames, segids), (resids, resnames, segids) + ) # transform from atom:Rid to atom:Rix residueids = Resids(new_resids) residuenums = Resnums(new_resids.copy()) @@ -327,13 +353,24 @@ def _parseatoms(self, lines, atoms_per, numlines): segidx, perseg_segids = squash_by(perres_segids)[:2] segids = Segids(perseg_segids) - top = Topology(len(atomids), len(new_resids), len(segids), - attrs=[atomids, atomnames, atomtypes, - charges, masses, - residueids, residuenums, residuenames, - segids], - atom_resindex=residx, - residue_segindex=segidx) + top = Topology( + len(atomids), + len(new_resids), + len(segids), + attrs=[ + atomids, + atomnames, + atomtypes, + charges, + masses, + residueids, + residuenums, + residuenames, + segids, + ], + atom_resindex=residx, + residue_segindex=segidx, + ) return top @@ -344,5 +381,5 @@ def _parsesection(self, lines, atoms_per, numlines): # Subtract 1 from each number to ensure zero-indexing for the atoms fields = np.int64(lines().split()) - 1 for j in range(0, len(fields), atoms_per): - section.append(tuple(fields[j:j+atoms_per])) + section.append(tuple(fields[j : j + atoms_per])) return section diff --git a/package/MDAnalysis/topology/ParmEdParser.py b/package/MDAnalysis/topology/ParmEdParser.py index 2cfc0df0dae..b7f9331e4ae 100644 --- a/package/MDAnalysis/topology/ParmEdParser.py +++ b/package/MDAnalysis/topology/ParmEdParser.py @@ -27,5 +27,5 @@ "This module is deprecated as of MDAnalysis version 2.0.0." "It will be removed in MDAnalysis version 3.0.0." "Please import the ParmEd classes from MDAnalysis.converters instead.", - category=DeprecationWarning + category=DeprecationWarning, ) diff --git a/package/MDAnalysis/topology/TOPParser.py b/package/MDAnalysis/topology/TOPParser.py index 4f2ce631fc6..ba61bea0b06 100644 --- a/package/MDAnalysis/topology/TOPParser.py +++ b/package/MDAnalysis/topology/TOPParser.py @@ -111,20 +111,21 @@ Bonds, Angles, Dihedrals, - Impropers + Impropers, ) import warnings import logging -logger = logging.getLogger('MDAnalysis.topology.TOPParser') +logger = logging.getLogger("MDAnalysis.topology.TOPParser") class TypeIndices(AtomAttr): """Numerical type of each Atom""" - attrname = 'type_indices' - singular = 'type_index' - level = 'atom' + + attrname = "type_indices" + singular = "type_index" + level = "atom" class TOPParser(TopologyReaderBase): @@ -173,7 +174,8 @@ class TOPParser(TopologyReaderBase): .. versionchanged:: 2.7.0 gets Segments and chainIDs from flag RESIDUE_CHAINID, when present """ - format = ['TOP', 'PRMTOP', 'PARM7'] + + format = ["TOP", "PRMTOP", "PARM7"] def parse(self, **kwargs): """Parse Amber PRMTOP topology file *filename*. @@ -188,8 +190,13 @@ def parse(self, **kwargs): "CHARGE": (1, 5, self.parse_charges, "charge", 0), "ATOMIC_NUMBER": (1, 10, self.parse_elements, "elements", 0), "MASS": (1, 5, self.parse_masses, "mass", 0), - "ATOM_TYPE_INDEX": (1, 10, self.parse_type_indices, "type_indices", - 0), + "ATOM_TYPE_INDEX": ( + 1, + 10, + self.parse_type_indices, + "type_indices", + 0, + ), "AMBER_ATOM_TYPE": (1, 20, self.parse_types, "types", 0), "RESIDUE_LABEL": (1, 20, self.parse_resnames, "resname", 11), "RESIDUE_POINTER": (1, 10, self.parse_residx, "respoint", 11), @@ -198,7 +205,13 @@ def parse(self, **kwargs): "ANGLES_INC_HYDROGEN": (4, 10, self.parse_bonded, "angh", 4), "ANGLES_WITHOUT_HYDROGEN": (4, 10, self.parse_bonded, "anga", 5), "DIHEDRALS_INC_HYDROGEN": (5, 10, self.parse_bonded, "dihh", 6), - "DIHEDRALS_WITHOUT_HYDROGEN": (5, 10, self.parse_bonded, "diha", 7), + "DIHEDRALS_WITHOUT_HYDROGEN": ( + 5, + 10, + self.parse_bonded, + "diha", + 7, + ), "RESIDUE_CHAINID": (1, 20, self.parse_chainids, "segids", 11), } @@ -211,21 +224,26 @@ def parse(self, **kwargs): if not header.startswith("%VE"): raise ValueError( "{0} is not a valid TOP file. %VE Missing in header" - "".format(self.filename)) + "".format(self.filename) + ) title = next(self.topfile).split() if not (title[1] == "TITLE"): # Raise a separate warning if Chamber-style TOP is detected if title[1] == "CTITLE": - emsg = ("{0} is detected as a Chamber-style TOP file. " - "At this time MDAnalysis does not support such " - "topologies".format(self.filename)) + emsg = ( + "{0} is detected as a Chamber-style TOP file. " + "At this time MDAnalysis does not support such " + "topologies".format(self.filename) + ) else: - emsg = ("{0} is not a valid TOP file. " - "'TITLE' missing in header".format(self.filename)) + emsg = ( + "{0} is not a valid TOP file. " + "'TITLE' missing in header".format(self.filename) + ) raise ValueError(emsg) - while not header.startswith('%FLAG POINTERS'): + while not header.startswith("%FLAG POINTERS"): header = next(self.topfile) next(self.topfile) @@ -238,14 +256,17 @@ def parse(self, **kwargs): while next_section is not None: try: - (num_per_record, per_line, - func, name, sect_num) = sections[next_section] + (num_per_record, per_line, func, name, sect_num) = ( + sections[next_section] + ) except KeyError: + def next_getter(): return self.skipper() + else: num = sys_info[sect_num] * num_per_record - numlines = (num // per_line) + numlines = num // per_line if num % per_line != 0: numlines += 1 @@ -265,56 +286,72 @@ def next_getter(): try: next_section = line.split("%FLAG")[1].strip() except IndexError: - errmsg = (f"%FLAG section not found, formatting error " - f"for PARM7 file {self.filename} ") + errmsg = ( + f"%FLAG section not found, formatting error " + f"for PARM7 file {self.filename} " + ) raise IndexError(errmsg) from None # strip out a few values to play with them - n_atoms = len(attrs['name']) + n_atoms = len(attrs["name"]) - resptrs = attrs.pop('respoint') + resptrs = attrs.pop("respoint") resptrs.append(n_atoms) residx = np.zeros(n_atoms, dtype=np.int32) for i, (x, y) in enumerate(zip(resptrs[:-1], resptrs[1:])): residx[x:y] = i - n_res = len(attrs['resname']) + n_res = len(attrs["resname"]) # Deal with recreating bonds and angle records here - attrs['bonds'] = Bonds([i for i in itertools.chain( - attrs.pop('bonda'), attrs.pop('bondh'))]) + attrs["bonds"] = Bonds( + [ + i + for i in itertools.chain( + attrs.pop("bonda"), attrs.pop("bondh") + ) + ] + ) - attrs['angles'] = Angles([i for i in itertools.chain( - attrs.pop('anga'), attrs.pop('angh'))]) + attrs["angles"] = Angles( + [i for i in itertools.chain(attrs.pop("anga"), attrs.pop("angh"))] + ) - attrs['dihedrals'], attrs['impropers'] = self.parse_dihedrals( - attrs.pop('diha'), attrs.pop('dihh')) + attrs["dihedrals"], attrs["impropers"] = self.parse_dihedrals( + attrs.pop("diha"), attrs.pop("dihh") + ) # Warn user if elements not in topology - if 'elements' not in attrs: - msg = ("ATOMIC_NUMBER record not found, elements attribute will " - "not be populated. If needed these can be guessed using " - "universe.guess_TopologyAttrs(to_guess=['elements']).") + if "elements" not in attrs: + msg = ( + "ATOMIC_NUMBER record not found, elements attribute will " + "not be populated. If needed these can be guessed using " + "universe.guess_TopologyAttrs(to_guess=['elements'])." + ) logger.warning(msg) warnings.warn(msg) - elif np.any(attrs['elements'].values == ""): + 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 " - "universe.guess_TopologyAttrs(to_guess=['elements']).") + msg = ( + "Unknown ATOMIC_NUMBER value found for some atoms, these " + "have been given an empty element record. If needed these " + "can be guessed using " + "universe.guess_TopologyAttrs(to_guess=['elements'])." + ) logger.warning(msg) warnings.warn(msg) # atom ids are mandatory - attrs['atomids'] = Atomids(np.arange(n_atoms) + 1) - attrs['resids'] = Resids(np.arange(n_res) + 1) - attrs['resnums'] = Resnums(np.arange(n_res) + 1) + attrs["atomids"] = Atomids(np.arange(n_atoms) + 1) + attrs["resids"] = Resids(np.arange(n_res) + 1) + attrs["resnums"] = Resnums(np.arange(n_res) + 1) # Amber's 'RESIDUE_CHAINID' is a by-residue attribute, turn it into # a by-atom attribute when present. See PR #4007. if "segids" in attrs and len(attrs["segids"]) == n_res: - segidx, (segids,) = change_squash((attrs["segids"],), (attrs["segids"],)) + segidx, (segids,) = change_squash( + (attrs["segids"],), (attrs["segids"],) + ) chainids = [attrs["segids"][r] for r in residx] attrs["segids"] = Segids(segids) @@ -467,8 +504,8 @@ def parse_elements(self, num_per_record, numlines): """ vals = self.parsesection_mapper( - numlines, - lambda x: Z2SYMB[int(x)] if int(x) > 0 else "") + numlines, lambda x: Z2SYMB[int(x)] if int(x) > 0 else "" + ) attr = Elements(np.array(vals, dtype=object)) return attr @@ -556,8 +593,10 @@ def parse_chunks(self, data, chunksize): Therefore, to extract the required information, we split out the list into chunks of size num_per_record, and only extract the atom ids. """ - vals = [tuple(data[x:x+chunksize-1]) - for x in range(0, len(data), chunksize)] + vals = [ + tuple(data[x : x + chunksize - 1]) + for x in range(0, len(data), chunksize) + ] return vals def parse_bonded(self, num_per_record, numlines): @@ -603,7 +642,7 @@ def parsesection_mapper(self, numlines, mapper): section = [] def get_fmt(file): - """ Skips '%COMMENT' lines until it gets the FORMAT specification + """Skips '%COMMENT' lines until it gets the FORMAT specification for the section.""" line = next(file) if line[:7] == "%FORMAT": @@ -622,7 +661,7 @@ def get_fmt(file): for i in range(numlines): l = next(self.topfile) for j in range(len(x.entries)): - val = l[x.entries[j].start:x.entries[j].stop].strip() + val = l[x.entries[j].start : x.entries[j].stop].strip() if val: section.append(mapper(val)) return section @@ -665,7 +704,7 @@ def parse_dihedrals(self, diha, dihh): dihed = [] for i in itertools.chain(diha, dihh): if i[3] < 0: - improp.append(i[:2]+(abs(i[2]),)+(abs(i[3]),)) + improp.append(i[:2] + (abs(i[2]),) + (abs(i[3]),)) elif i[2] < 0: vals = i[:2] + (abs(i[2]),) + i[3:] dihed.append(vals) diff --git a/package/MDAnalysis/topology/TPRParser.py b/package/MDAnalysis/topology/TPRParser.py index 5c648016e87..6adac116e84 100644 --- a/package/MDAnalysis/topology/TPRParser.py +++ b/package/MDAnalysis/topology/TPRParser.py @@ -174,6 +174,7 @@ from ..core.topologyattrs import Resnums import logging + logger = logging.getLogger("MDAnalysis.topology.TPRparser") @@ -183,7 +184,8 @@ class TPRParser(TopologyReaderBase): .. _Gromacs: http://www.gromacs.org .. _TPR file: http://manual.gromacs.org/current/online/tpr.html """ - format = 'TPR' + + format = "TPR" def parse(self, tpr_resid_from_one=True, **kwargs): """Parse a Gromacs TPR file into a MDAnalysis internal topology structure. @@ -206,11 +208,11 @@ def parse(self, tpr_resid_from_one=True, **kwargs): .. versionchanged:: 2.0.0 Changed to ``tpr_resid_from_one=True`` by default. """ - with openany(self.filename, mode='rb') as infile: + with openany(self.filename, mode="rb") as infile: tprf = infile.read() data = tpr_utils.TPXUnpacker(tprf) try: - th = tpr_utils.read_tpxheader(data) # tpxheader + th = tpr_utils.read_tpxheader(data) # tpxheader except (EOFError, ValueError): msg = f"{self.filename}: Invalid tpr file or cannot be recognized" logger.critical(msg) @@ -232,18 +234,21 @@ def parse(self, tpr_resid_from_one=True, **kwargs): raise IOError(msg) data = tpr_utils.TPXUnpacker2020.from_unpacker(data) - state_ngtc = th.ngtc # done init_state() in src/gmxlib/tpxio.c + state_ngtc = th.ngtc # done init_state() in src/gmxlib/tpxio.c if th.bBox: tpr_utils.extract_box_info(data, th.fver) if state_ngtc > 0: - if th.fver < 69: # redundancy due to different versions + if th.fver < 69: # redundancy due to different versions tpr_utils.ndo_real(data, state_ngtc) - tpr_utils.ndo_real(data, state_ngtc) # relevant to Berendsen tcoupl_lambda + tpr_utils.ndo_real( + data, state_ngtc + ) # relevant to Berendsen tcoupl_lambda if th.bTop: - tpr_top = tpr_utils.do_mtop(data, th.fver, - tpr_resid_from_one=tpr_resid_from_one) + tpr_top = tpr_utils.do_mtop( + data, th.fver, tpr_resid_from_one=tpr_resid_from_one + ) else: msg = f"{self.filename}: No topology found in tpr file" logger.critical(msg) @@ -253,7 +258,6 @@ def parse(self, tpr_resid_from_one=True, **kwargs): return tpr_top - def _log_header(self, th): logger.info(f"Gromacs version : {th.ver_str}") logger.info(f"tpx version : {th.fver}") diff --git a/package/MDAnalysis/topology/TXYZParser.py b/package/MDAnalysis/topology/TXYZParser.py index 4b0d248e374..4752f1bf481 100644 --- a/package/MDAnalysis/topology/TXYZParser.py +++ b/package/MDAnalysis/topology/TXYZParser.py @@ -87,7 +87,8 @@ class TXYZParser(TopologyReaderBase): through universe.guess_TopologyAttrs() API). """ - format = ['TXYZ', 'ARC'] + + format = ["TXYZ", "ARC"] def parse(self, **kwargs): """Read the file and return the structure. @@ -97,7 +98,7 @@ def parse(self, **kwargs): MDAnalysis Topology object """ with openany(self.filename) as inf: - #header + # header natoms = int(inf.readline().split()[0]) atomids = np.zeros(natoms, dtype=int) @@ -121,7 +122,7 @@ def parse(self, **kwargs): # Can't infinitely read as XYZ files can be multiframe for i, line in zip(range(natoms), itertools.chain([fline], inf)): line = line.split() - atomids[i]= line[0] + atomids[i] = line[0] names[i] = line[1] types[i] = line[5] bonded_atoms = line[6:] @@ -130,24 +131,26 @@ def parse(self, **kwargs): if i < other_atom: bonds.append((i, other_atom)) - attrs = [Atomnames(names), - Atomids(atomids), - Atomtypes(types), - Bonds(tuple(bonds)), - Resids(np.array([1])), - Resnums(np.array([1])), - Segids(np.array(['SYSTEM'], dtype=object)), - ] + attrs = [ + Atomnames(names), + Atomids(atomids), + Atomtypes(types), + Bonds(tuple(bonds)), + 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 universe.guess_TopologyAttrs(" - "to_guess=['elements']).") - - top = Topology(natoms, 1, 1, - attrs=attrs) + 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'])." + ) + + top = Topology(natoms, 1, 1, attrs=attrs) return top diff --git a/package/MDAnalysis/topology/XYZParser.py b/package/MDAnalysis/topology/XYZParser.py index 956c93567bc..45683a1a714 100644 --- a/package/MDAnalysis/topology/XYZParser.py +++ b/package/MDAnalysis/topology/XYZParser.py @@ -75,7 +75,8 @@ class XYZParser(TopologyReaderBase): through universe.guess_TopologyAttrs() API). """ - format = 'XYZ' + + format = "XYZ" def parse(self, **kwargs): """Read the file and return the structure. @@ -95,15 +96,15 @@ def parse(self, **kwargs): name = inf.readline().split()[0] names[i] = name + attrs = [ + Atomnames(names), + Atomids(np.arange(natoms) + 1), + Resids(np.array([1])), + Resnums(np.array([1])), + Segids(np.array(["SYSTEM"], dtype=object)), + Elements(names), + ] - attrs = [Atomnames(names), - Atomids(np.arange(natoms) + 1), - Resids(np.array([1])), - Resnums(np.array([1])), - Segids(np.array(['SYSTEM'], dtype=object)), - Elements(names)] - - top = Topology(natoms, 1, 1, - attrs=attrs) + top = Topology(natoms, 1, 1, attrs=attrs) return top diff --git a/package/MDAnalysis/topology/core.py b/package/MDAnalysis/topology/core.py index 7ee61219827..a0e28f96431 100644 --- a/package/MDAnalysis/topology/core.py +++ b/package/MDAnalysis/topology/core.py @@ -41,9 +41,15 @@ # 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, + 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 +# tumbleweed diff --git a/package/MDAnalysis/topology/guessers.py b/package/MDAnalysis/topology/guessers.py index d1485bad080..6ae316cd026 100644 --- a/package/MDAnalysis/topology/guessers.py +++ b/package/MDAnalysis/topology/guessers.py @@ -131,7 +131,9 @@ def guess_masses(atom_types): 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) + masses = np.array( + [get_atom_mass(atom_t) for atom_t in atom_types], dtype=np.float64 + ) return masses @@ -158,7 +160,11 @@ def validate_atom_types(atom_types): try: tables.masses[atom_type.upper()] except KeyError: - warnings.warn("Failed to guess the mass for the following atom types: {}".format(atom_type)) + 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) @@ -174,7 +180,9 @@ def guess_types(atom_names): ------- atom_types : np.ndarray dtype object """ - return np.array([guess_atom_element(name) for name in atom_names], dtype=object) + 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) @@ -195,8 +203,8 @@ def guess_atom_type(atomname): return guess_atom_element(atomname) -NUMBERS = re.compile(r'[0-9]') # match numbers -SYMBOLS = re.compile(r'[*+-]') # match *, +, - +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) @@ -216,19 +224,19 @@ def guess_atom_element(atomname): :func:`guess_atom_type` :mod:`MDAnalysis.topology.tables` """ - if atomname == '': - return '' + if atomname == "": + return "" try: return tables.atomelements[atomname.upper()] except KeyError: # strip symbols - no_symbols = re.sub(SYMBOLS, '', atomname) + 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 '' + 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 '' + name = no_numbers[0].upper() if no_numbers else "" # just in case if name in tables.atomelements: @@ -317,10 +325,10 @@ def guess_bonds(atoms, coords, box=None, **kwargs): if len(atoms) != len(coords): raise ValueError("'atoms' and 'coord' must be the same length") - fudge_factor = kwargs.get('fudge_factor', 0.55) + 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) + user_vdwradii = kwargs.get("vdwradii", None) if user_vdwradii: # this should make algo use their values over defaults vdwradii.update(user_vdwradii) @@ -329,13 +337,16 @@ def guess_bonds(atoms, coords, box=None, **kwargs): # 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'")) + 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) + lower_bound = kwargs.get("lower_bound", 0.1) if box is not None: box = np.asarray(box) @@ -347,13 +358,12 @@ def guess_bonds(atoms, coords, box=None, **kwargs): bonds = [] - pairs, dist = distances.self_capped_distance(coords, - max_cutoff=2.0*max_vdw, - min_cutoff=lower_bound, - box=box) + 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): + d = (vdwradii[atomtypes[i]] + vdwradii[atomtypes[j]]) * fudge_factor + if dist[idx] < d: bonds.append((atoms[i].index, atoms[j].index)) return tuple(bonds) @@ -416,8 +426,9 @@ def guess_dihedrals(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 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) @@ -548,7 +559,9 @@ def guess_gasteiger_charges(atomgroup): """ 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) + 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 6be8b40b746..93c5a83ee75 100644 --- a/package/MDAnalysis/topology/tpr/obj.py +++ b/package/MDAnalysis/topology/tpr/obj.py @@ -35,24 +35,59 @@ from ...guesser.tables import Z2SYMB TpxHeader = namedtuple( - "TpxHeader", [ - "ver_str", "precision", - "fver", "fgen", "file_tag", "natoms", "ngtc", "fep_state", "lamb", - "bIr", "bTop", "bX", "bV", "bF", "bBox", "sizeOfTprBody"]) + "TpxHeader", + [ + "ver_str", + "precision", + "fver", + "fgen", + "file_tag", + "natoms", + "ngtc", + "fep_state", + "lamb", + "bIr", + "bTop", + "bX", + "bV", + "bF", + "bBox", + "sizeOfTprBody", + ], +) Box = namedtuple("Box", "size rel v") Mtop = namedtuple("Mtop", "nmoltype moltypes nmolblock") Params = namedtuple("Params", "atnr ntypes functype reppow fudgeQQ") -Atom = namedtuple("Atom", ["m", "q", "mB", "qB", "tp", "typeB", "ptype", "resind", "atomnumber"]) +Atom = namedtuple( + "Atom", + ["m", "q", "mB", "qB", "tp", "typeB", "ptype", "resind", "atomnumber"], +) Atoms = namedtuple("Atoms", "atoms nr nres type typeB atomnames resnames") Ilist = namedtuple("Ilist", "nr ik, iatoms") -Molblock = namedtuple("Molblock", [ - "molb_type", "molb_nmol", "molb_natoms_mol", - "molb_nposres_xA", "molb_nposres_xB"]) +Molblock = namedtuple( + "Molblock", + [ + "molb_type", + "molb_nmol", + "molb_natoms_mol", + "molb_nposres_xA", + "molb_nposres_xB", + ], +) class MoleculeKind(object): - def __init__(self, name, atomkinds, bonds=None, angles=None, - dihe=None, impr=None, donors=None, acceptors=None): + def __init__( + self, + name, + atomkinds, + bonds=None, + angles=None, + dihe=None, + impr=None, + donors=None, + acceptors=None, + ): self.name = name # name of the molecule self.atomkinds = atomkinds self.bonds = bonds @@ -105,7 +140,8 @@ def remap_impr(self, atom_start_ndx): class AtomKind(object): def __init__( - self, id, name, type, resid, resname, mass, charge, atomic_number): + self, id, name, type, resid, resname, mass, charge, atomic_number + ): # id is only within the scope of a single molecule, not the whole system self.id = id self.name = name @@ -125,7 +161,7 @@ def element_symbol(self): is not recognized, which happens if a particle is not really an atom (e.g a coarse-grained particle), an empty string is returned. """ - return Z2SYMB.get(self.atomic_number, '') + return Z2SYMB.get(self.atomic_number, "") def __repr__(self): return ( @@ -152,4 +188,4 @@ def process(self, atom_ndx): # The format for all record is (type, atom1, atom2, ...) # but we are only interested in the atoms. for cursor in range(0, len(atom_ndx), self.natoms + 1): - yield atom_ndx[cursor + 1: cursor + 1 + self.natoms] + yield atom_ndx[cursor + 1 : cursor + 1 + self.natoms] diff --git a/package/MDAnalysis/topology/tpr/setting.py b/package/MDAnalysis/topology/tpr/setting.py index ebb749d588d..1fd0a682835 100644 --- a/package/MDAnalysis/topology/tpr/setting.py +++ b/package/MDAnalysis/topology/tpr/setting.py @@ -38,8 +38,22 @@ """ #: Gromacs TPR file format versions that can be read by the TPRParser. -SUPPORTED_VERSIONS = (58, 73, 83, 100, 103, 110, 112, - 116, 119, 122, 127, 129, 133, 134) +SUPPORTED_VERSIONS = ( + 58, + 73, + 83, + 100, + 103, + 110, + 112, + 116, + 119, + 122, + 127, + 129, + 133, + 134, +) # Some constants STRLEN = 4096 @@ -50,7 +64,7 @@ NR_FOURDIHS = 4 # /src/gromacs/topology/idef.h egcNR = 10 # include/types/topolog.h TPX_TAG_RELEASE = "release" # /src/gromacs/fileio/tpxio.c -tpx_version = 103 # /src/gromacs/fileio/tpxio.c +tpx_version = 103 # /src/gromacs/fileio/tpxio.c tpx_generation = 27 # /src/gromacs/fileio/tpxio.c tpxv_RestrictedBendingAndCombinedAngleTorsionPotentials = 98 tpxv_GenericInternalParameters = 117 @@ -61,6 +75,7 @@ #: Function types from ``/include/types/idef.h`` +# fmt: off ( F_BONDS, F_G96BONDS, F_MORSE, F_CUBICBONDS, F_CONNBONDS, F_HARMONIC, F_FENEBONDS, F_TABBONDS, @@ -83,9 +98,12 @@ F_ETOT, F_ECONSERVED, F_TEMP, F_VTEMP_NOLONGERUSED, F_PDISPCORR, F_PRES, F_DHDL_CON, F_DVDL, F_DKDL, F_DVDL_COUL, F_DVDL_VDW, F_DVDL_BONDED, - F_DVDL_RESTRAINT, F_DVDL_TEMPERATURE, F_NRE) = list(range(95)) + F_DVDL_RESTRAINT, F_DVDL_TEMPERATURE, F_NRE +) = list(range(95)) +# fmt: on #: Function types from ``/src/gmxlib/tpxio.c`` +# fmt: off ftupd = [ (20, F_CUBICBONDS), (20, F_CONNBONDS), (20, F_HARMONIC), (34, F_FENEBONDS), (43, F_TABBONDS), (43, F_TABBONDSNC), (70, F_RESTRBONDS), @@ -110,6 +128,7 @@ (tpxv_VSite1, F_VSITE1), (tpxv_VSite2FD, F_VSITE2FD), ] +# fmt: on #: Interaction types from ``/gmxlib/ifunc.c`` interaction_types = [ @@ -206,5 +225,5 @@ ("DVV/DL", "dVvdw/dl", None), ("DVB/DL", "dVbonded/dl", None), ("DVR/DL", "dVrestraint/dl", None), - ("DVT/DL", "dVtemperature/dl", None) + ("DVT/DL", "dVtemperature/dl", None), ] diff --git a/package/pyproject.toml b/package/pyproject.toml index ae0d34422dc..8be16c7f61a 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -135,6 +135,7 @@ tables\.py | MDAnalysis/visualization/.*\.py | MDAnalysis/lib/.*\.py^ | MDAnalysis/transformations/.*\.py +| MDAnalysis/topology/.*\.py | MDAnalysis/analysis/.*\.py | MDAnalysis/guesser/.*\.py | MDAnalysis/converters/.*\.py diff --git a/testsuite/MDAnalysisTests/topology/base.py b/testsuite/MDAnalysisTests/topology/base.py index 6527ab8ae34..e7649d65f86 100644 --- a/testsuite/MDAnalysisTests/topology/base.py +++ b/testsuite/MDAnalysisTests/topology/base.py @@ -25,7 +25,7 @@ import MDAnalysis as mda from MDAnalysis.core.topology import Topology -mandatory_attrs = ['ids', 'resids', 'resnums', 'segids'] +mandatory_attrs = ["ids", "resids", "resnums", "segids"] class ParserBase(object): @@ -57,34 +57,56 @@ def test_mandatory_attributes(self, top): # attributes required as part of the API # ALL parsers must provide these for attr in mandatory_attrs: - assert hasattr(top, attr), 'Missing required attribute: {}'.format(attr) + assert hasattr(top, attr), "Missing required attribute: {}".format( + attr + ) def test_expected_attributes(self, top): # Extra attributes as declared in specific implementations for attr in self.expected_attrs: - assert hasattr(top, attr), 'Missing expected attribute: {}'.format(attr) + assert hasattr(top, attr), "Missing expected attribute: {}".format( + attr + ) def test_no_unexpected_attributes(self, top): - attrs = set(self.expected_attrs - + mandatory_attrs - + ['indices', 'resindices', 'segindices'] + self.guessed_attrs) + attrs = set( + self.expected_attrs + + mandatory_attrs + + ["indices", "resindices", "segindices"] + + self.guessed_attrs + ) for attr in top.attrs: - assert attr.attrname in attrs, 'Unexpected attribute: {}'.format(attr.attrname) + assert attr.attrname in attrs, "Unexpected attribute: {}".format( + attr.attrname + ) 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( - top.n_atoms, self.expected_n_atoms, self.__class__.__name__) - - assert top.n_residues == self.expected_n_residues, '{} residues read, {} expected in {}'.format( - top.n_residues, self.expected_n_residues, self.__class__.__name__) - - assert top.n_segments == self.expected_n_segments, '{} segment read, {} expected in {}'.format( - top.n_segments, self.expected_n_segments, self.__class__.__name__) + assert ( + top.n_atoms == self.expected_n_atoms + ), "{} atoms read, {} expected in {}".format( + top.n_atoms, self.expected_n_atoms, self.__class__.__name__ + ) + + assert ( + top.n_residues == self.expected_n_residues + ), "{} residues read, {} expected in {}".format( + top.n_residues, self.expected_n_residues, self.__class__.__name__ + ) + + assert ( + top.n_segments == self.expected_n_segments + ), "{} segment read, {} expected in {}".format( + top.n_segments, self.expected_n_segments, self.__class__.__name__ + ) def test_tt_size(self, top): """Check that the transtable is appropriately sized""" - assert top.tt.size == (self.expected_n_atoms, self.expected_n_residues, self.expected_n_segments) + assert top.tt.size == ( + self.expected_n_atoms, + self.expected_n_residues, + self.expected_n_segments, + ) def test_creates_universe(self, filename): """Check that Universe works with this Parser""" @@ -95,8 +117,9 @@ 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] + 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_altloc.py b/testsuite/MDAnalysisTests/topology/test_altloc.py index 1007c3e0673..63b43c32139 100644 --- a/testsuite/MDAnalysisTests/topology/test_altloc.py +++ b/testsuite/MDAnalysisTests/topology/test_altloc.py @@ -50,7 +50,7 @@ def test_bonds(u): def test_write_read(u, tmpdir): - outfile = str(tmpdir.join('test.pdb')) + outfile = str(tmpdir.join("test.pdb")) u.select_atoms("all").write(outfile) u2 = Universe(outfile) assert len(u.atoms) == len(u2.atoms) diff --git a/testsuite/MDAnalysisTests/topology/test_crd.py b/testsuite/MDAnalysisTests/topology/test_crd.py index 7c9b0a72419..d7a8794c97b 100644 --- a/testsuite/MDAnalysisTests/topology/test_crd.py +++ b/testsuite/MDAnalysisTests/topology/test_crd.py @@ -33,10 +33,16 @@ class TestCRDParser(ParserBase): parser = mda.topology.CRDParser.CRDParser ref_filename = CRD - expected_attrs = ['ids', 'names', 'tempfactors', - 'resids', 'resnames', 'resnums', - 'segids'] - guessed_attrs = ['masses', 'types'] + expected_attrs = [ + "ids", + "names", + "tempfactors", + "resids", + "resnames", + "resnums", + "segids", + ] + guessed_attrs = ["masses", "types"] expected_n_atoms = 3341 expected_n_residues = 214 @@ -44,10 +50,10 @@ class TestCRDParser(ParserBase): 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] + 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'] + 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 da1e871dcdd..9ab731d439a 100644 --- a/testsuite/MDAnalysisTests/topology/test_dlpoly.py +++ b/testsuite/MDAnalysisTests/topology/test_dlpoly.py @@ -34,7 +34,7 @@ DLP_HISTORY_order, DLP_HISTORY_minimal, DLP_HISTORY_minimal_cell, - DLP_HISTORY_classic + DLP_HISTORY_classic, ) @@ -50,8 +50,8 @@ def test_guessed_attributes(self, filename): class DLPBase2(DLPUniverse): - expected_attrs = ['ids', 'names'] - guessed_attrs = ['masses', 'types'] + expected_attrs = ["ids", "names"] + guessed_attrs = ["masses", "types"] expected_n_atoms = 216 expected_n_residues = 1 @@ -64,73 +64,72 @@ def test_guesssed_masses(self, filename): 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' + 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-' + assert top.names.values[0] == "K+" + assert top.names.values[4] == "Cl-" class TestDLPHistoryParser(DLPBase2): parser = mda.topology.DLPolyParser.HistoryParser ref_filename = DLP_HISTORY - format = 'HISTORY' + format = "HISTORY" class TestDLPConfigParser(DLPBase2): parser = mda.topology.DLPolyParser.ConfigParser ref_filename = DLP_CONFIG - format = 'CONFIG' + format = "CONFIG" class DLPBase(DLPUniverse): - expected_attrs = ['ids', 'names'] + expected_attrs = ["ids", "names"] expected_n_atoms = 3 expected_n_residues = 1 expected_n_segments = 1 def test_dlp_names(self, top): - assert_equal(top.names.values, - ['C', 'B', 'A']) + assert_equal(top.names.values, ["C", "B", "A"]) class TestDLPConfigMinimal(DLPBase): parser = mda.topology.DLPolyParser.ConfigParser ref_filename = DLP_CONFIG_minimal - format = 'CONFIG' + format = "CONFIG" class TestDLPConfigOrder(DLPBase): parser = mda.topology.DLPolyParser.ConfigParser ref_filename = DLP_CONFIG_order - format = 'CONFIG' + format = "CONFIG" class TestDLPHistoryMinimal(DLPBase): parser = mda.topology.DLPolyParser.HistoryParser ref_filename = DLP_HISTORY_minimal - format = 'HISTORY' + format = "HISTORY" class TestDLPHistoryMinimal(DLPBase): parser = mda.topology.DLPolyParser.HistoryParser ref_filename = DLP_HISTORY_minimal_cell - format = 'HISTORY' + format = "HISTORY" class TestDLPHistoryOrder(DLPBase): parser = mda.topology.DLPolyParser.HistoryParser ref_filename = DLP_HISTORY_order - format = 'HISTORY' + format = "HISTORY" class TestDLPHistoryClassic(DLPBase): parser = mda.topology.DLPolyParser.HistoryParser ref_filename = DLP_HISTORY_classic - format = 'HISTORY' + format = "HISTORY" def test_HISTORY_EOFError(): with pytest.raises(EOFError): - mda.Universe(DLP_CONFIG, topology_format='HISTORY') + mda.Universe(DLP_CONFIG, topology_format="HISTORY") diff --git a/testsuite/MDAnalysisTests/topology/test_dms.py b/testsuite/MDAnalysisTests/topology/test_dms.py index b1eb8d77b34..0465eb1de5c 100644 --- a/testsuite/MDAnalysisTests/topology/test_dms.py +++ b/testsuite/MDAnalysisTests/topology/test_dms.py @@ -28,10 +28,19 @@ class TestDMSParser(ParserBase): parser = mda.topology.DMSParser.DMSParser ref_filename = DMS_DOMAINS - expected_attrs = ['ids', 'names', 'bonds', 'charges', - 'masses', 'resids', 'resnames', 'segids', - 'chainIDs', 'atomnums'] - guessed_attrs = ['types'] + expected_attrs = [ + "ids", + "names", + "bonds", + "charges", + "masses", + "resids", + "resnames", + "segids", + "chainIDs", + "atomnums", + ] + guessed_attrs = ["types"] expected_n_atoms = 3341 expected_n_residues = 214 expected_n_segments = 3 @@ -62,9 +71,9 @@ def test_atomsels(self, filename): 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() + u = mda.Universe(filename) + expected = ["N", "H", "H", "H", "C", "H", "C"] + assert (u.atoms.types[:7] == expected).all() class TestDMSParserNoSegid(TestDMSParser): diff --git a/testsuite/MDAnalysisTests/topology/test_fhiaims.py b/testsuite/MDAnalysisTests/topology/test_fhiaims.py index b8bbc29e46e..4086c107dff 100644 --- a/testsuite/MDAnalysisTests/topology/test_fhiaims.py +++ b/testsuite/MDAnalysisTests/topology/test_fhiaims.py @@ -30,28 +30,25 @@ class TestFHIAIMS(ParserBase): parser = mda.topology.FHIAIMSParser.FHIAIMSParser - expected_attrs = ['names', 'elements'] - guessed_attrs = ['masses', 'types'] + expected_attrs = ["names", "elements"] + guessed_attrs = ["masses", "types"] expected_n_residues = 1 expected_n_segments = 1 expected_n_atoms = 6 ref_filename = FHIAIMS def test_names(self, top): - assert_equal(top.names.values, - ['O', 'H', 'H', 'O', 'H', 'H']) + assert_equal(top.names.values, ["O", "H", "H", "O", "H", "H"]) def test_guessed_types(self, filename): u = mda.Universe(filename) - assert_equal(u.atoms.types, - ['O', 'H', 'H', 'O', 'H', 'H']) + 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]) + 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']) + assert_equal(top.elements.values, ["O", "H", "H", "O", "H", "H"]) diff --git a/testsuite/MDAnalysisTests/topology/test_gms.py b/testsuite/MDAnalysisTests/topology/test_gms.py index 65935c14baf..8becdfda6f8 100644 --- a/testsuite/MDAnalysisTests/topology/test_gms.py +++ b/testsuite/MDAnalysisTests/topology/test_gms.py @@ -34,8 +34,8 @@ class GMSBase(ParserBase): parser = mda.topology.GMSParser.GMSParser - expected_attrs = ['names', 'atomiccharges'] - guessed_attrs = ['masses', 'types'] + expected_attrs = ["names", "atomiccharges"] + guessed_attrs = ["masses", "types"] expected_n_residues = 1 expected_n_segments = 1 @@ -45,12 +45,10 @@ class TestGMSASYMOPT(GMSBase): ref_filename = GMS_ASYMOPT def test_names(self, top): - assert_equal(top.names.values, - ['O', 'H', 'H', 'O', 'H', 'H']) + assert_equal(top.names.values, ["O", "H", "H", "O", "H", "H"]) def test_types(self, top): - assert_equal(top.atomiccharges.values, - [8, 1, 1, 8, 1, 1]) + assert_equal(top.atomiccharges.values, [8, 1, 1, 8, 1, 1]) def test_guessed_masses(self, filename): u = mda.Universe(filename) @@ -59,7 +57,7 @@ def test_guessed_masses(self, filename): def test_guessed_types(self, filename): u = mda.Universe(filename) - expected = ['O', 'H', 'H', 'O', 'H', 'H'] + expected = ["O", "H", "H", "O", "H", "H"] assert (u.atoms.types == expected).all() @@ -68,12 +66,12 @@ class TestGMSSYMOPT(GMSBase): ref_filename = GMS_SYMOPT def test_names(self, top): - assert_equal(top.names.values, - ['CARBON', 'CARBON', 'HYDROGEN', 'HYDROGEN']) + assert_equal( + top.names.values, ["CARBON", "CARBON", "HYDROGEN", "HYDROGEN"] + ) def test_types(self, top): - assert_equal(top.atomiccharges.values, - [6, 6, 1, 1]) + assert_equal(top.atomiccharges.values, [6, 6, 1, 1]) class TestGMSASYMSURF(TestGMSASYMOPT): diff --git a/testsuite/MDAnalysisTests/topology/test_gro.py b/testsuite/MDAnalysisTests/topology/test_gro.py index f9d506fdba5..3bd266b0087 100644 --- a/testsuite/MDAnalysisTests/topology/test_gro.py +++ b/testsuite/MDAnalysisTests/topology/test_gro.py @@ -40,8 +40,8 @@ class TestGROParser(ParserBase): parser = mda.topology.GROParser.GROParser ref_filename = GRO - expected_attrs = ['ids', 'names', 'resids', 'resnames'] - guessed_attrs = ['masses', 'types'] + expected_attrs = ["ids", "names", "resids", "resnames"] + guessed_attrs = ["masses", "types"] expected_n_atoms = 47681 expected_n_residues = 11302 expected_n_segments = 1 @@ -54,17 +54,18 @@ def test_attr_size(self, top): 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] + 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'] + expected = ["N", "H", "H", "H", "C", "H", "C"] assert_equal(u.atoms.types[:7], expected) class TestGROWideBox(object): """Tests for Issue #548""" + def test_atoms(self): parser = mda.topology.GROParser.GROParser with parser(two_water_gro_widebox) as p: @@ -89,16 +90,23 @@ def test_parse_missing_atomname_IOerror(): class TestGroResidWrapping(object): # resid is 5 digit field, so is limited to 100k # check that parser recognises when resids have wrapped - names = ['MET', 'ARG', 'ILE', 'ILE', 'LEU', 'LEU', 'GLY'] + names = ["MET", "ARG", "ILE", "ILE", "LEU", "LEU", "GLY"] lengths = [19, 24, 19, 19, 19, 19, 7] parser = mda.topology.GROParser.GROParser - @pytest.mark.parametrize('parser, resids', ( - (GRO_residwrap, [1, 99999, 100000, 100001, 199999, 200000, 200001]), - (GRO_residwrap_0base, [0, 99999, 100000, 100001, 199999, 200000, - 200001]) - - )) + @pytest.mark.parametrize( + "parser, resids", + ( + ( + GRO_residwrap, + [1, 99999, 100000, 100001, 199999, 200000, 200001], + ), + ( + GRO_residwrap_0base, + [0, 99999, 100000, 100001, 199999, 200000, 200001], + ), + ), + ) def test_wrapping_resids(self, parser, resids): with self.parser(parser) as p: top = p.parse() @@ -116,7 +124,7 @@ def test_sameresid_diffresname(): with parser(GRO_sameresid_diffresname) as p: top = p.parse() resids = [9, 9] - resnames = ['GLN', 'POPC'] + resnames = ["GLN", "POPC"] for i, (resid, resname) in enumerate(zip(resids, resnames)): assert top.resids.values[i] == resid assert top.resnames.values[i] == resname diff --git a/testsuite/MDAnalysisTests/topology/test_gsd.py b/testsuite/MDAnalysisTests/topology/test_gsd.py index d183642013c..54dbe49288a 100644 --- a/testsuite/MDAnalysisTests/topology/test_gsd.py +++ b/testsuite/MDAnalysisTests/topology/test_gsd.py @@ -31,12 +31,23 @@ import os -@pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') +@pytest.mark.skipif(not HAS_GSD, reason="gsd not installed") class GSDBase(ParserBase): parser = mda.topology.GSDParser.GSDParser - expected_attrs = ['ids', 'names', 'resids', 'resnames', 'masses', - 'charges', 'radii', 'types', - 'bonds', 'angles', 'dihedrals', 'impropers'] + expected_attrs = [ + "ids", + "names", + "resids", + "resnames", + "masses", + "charges", + "radii", + "types", + "bonds", + "angles", + "dihedrals", + "impropers", + ] expected_n_bonds = 0 expected_n_angles = 0 expected_n_dihedrals = 0 @@ -80,7 +91,7 @@ def test_impropers(self, top): assert top.impropers.values == [] -@pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') +@pytest.mark.skipif(not HAS_GSD, reason="gsd not installed") class TestGSDParser(GSDBase): ref_filename = GSD expected_n_atoms = 5832 @@ -88,7 +99,7 @@ class TestGSDParser(GSDBase): expected_n_segments = 1 -@pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') +@pytest.mark.skipif(not HAS_GSD, reason="gsd not installed") class TestGSDParserBonds(GSDBase): ref_filename = GSD_bonds expected_n_atoms = 490 @@ -102,16 +113,16 @@ def test_bonds_identity(self, top): vals = top.bonds.values for b in ((0, 1), (1, 2), (2, 3), (3, 4)): assert (b in vals) or (b[::-1] in vals) - assert ((0, 450) not in vals) + assert (0, 450) not in vals def test_angles_identity(self, top): vals = top.angles.values for b in ((0, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5)): assert (b in vals) or (b[::-1] in vals) - assert ((0, 350, 450) not in vals) + assert (0, 350, 450) not in vals def test_dihedrals_identity(self, top): vals = top.dihedrals.values for b in ((0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5), (3, 4, 5, 6)): assert (b in vals) or (b[::-1] in vals) - assert ((0, 250, 350, 450) not in vals) + assert (0, 250, 350, 450) not in vals diff --git a/testsuite/MDAnalysisTests/topology/test_guessers.py b/testsuite/MDAnalysisTests/topology/test_guessers.py index 46581c6c999..69b1046f826 100644 --- a/testsuite/MDAnalysisTests/topology/test_guessers.py +++ b/testsuite/MDAnalysisTests/topology/test_guessers.py @@ -42,31 +42,38 @@ except ImportError: pass -requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), - reason="requires RDKit") +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']) + 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, match='Failed to guess the mass'): - guessers.guess_masses(['X']) + with pytest.warns(UserWarning, match="Failed to guess the mass"): + guessers.guess_masses(["X"]) def test_guess_masses_miss(self): - out = guessers.guess_masses(['X', 'Z']) + 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), )) + @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 + assert guessers.guess_atom_mass("1H") == 1.008 class TestGuessTypes(object): @@ -74,50 +81,53 @@ class TestGuessTypes(object): # guess_atom_type # guess_atom_element def test_guess_types(self): - out = guessers.guess_types(['MG2+', 'C12']) + out = guessers.guess_types(["MG2+", "C12"]) assert isinstance(out, np.ndarray) - assert_equal(out, np.array(['MG', 'C'], dtype=object)) + assert_equal(out, np.array(["MG", "C"], dtype=object)) def test_guess_atom_element(self): - assert guessers.guess_atom_element('MG2+') == 'MG' + assert guessers.guess_atom_element("MG2+") == "MG" def test_guess_atom_element_empty(self): - assert guessers.guess_atom_element('') == '' + assert guessers.guess_atom_element("") == "" def test_guess_atom_element_singledigit(self): - assert guessers.guess_atom_element('1') == '1' + 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') - )) + 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 + assert guessers.guess_atom_charge("this") == 0.0 def test_guess_bonds_Error(): @@ -141,42 +151,45 @@ 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: + 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))) + 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)) + 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", -]) + 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) @@ -187,20 +200,25 @@ def test_guess_aromaticities(smi): assert_equal(values, expected) -@pytest.mark.parametrize("smi", [ - "c1ccccc1", - "C1=CC=CC=C1", - "CCO", - "c1ccccc1Cc1ccccc1", - "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", -]) +@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) + 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) @@ -208,21 +226,24 @@ def test_guess_gasteiger_charges(smi): class TestDeprecationWarning: wmsg = ( - "MDAnalysis.topology.guessers is deprecated in favour of " - "the new Guessers API. " - "See MDAnalysis.guesser.default_guesser for more details." + "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'], - ]) + @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) @@ -251,7 +272,7 @@ def test_angles_dihedral_deprecations(self): @requires_rdkit def test_rdkit_guessers_deprecations(self): - mol = Chem.MolFromSmiles('c1ccccc1') + mol = Chem.MolFromSmiles("c1ccccc1") mol = Chem.AddHs(mol) u = mda.Universe(mol) diff --git a/testsuite/MDAnalysisTests/topology/test_hoomdxml.py b/testsuite/MDAnalysisTests/topology/test_hoomdxml.py index 759a2aae78d..091662abef4 100644 --- a/testsuite/MDAnalysisTests/topology/test_hoomdxml.py +++ b/testsuite/MDAnalysisTests/topology/test_hoomdxml.py @@ -31,7 +31,14 @@ class TestHoomdXMLParser(ParserBase): parser = mda.topology.HoomdXMLParser.HoomdXMLParser ref_filename = HoomdXMLdata expected_attrs = [ - 'types', 'masses', 'charges', 'radii', 'bonds', 'angles', 'dihedrals', 'impropers' + "types", + "masses", + "charges", + "radii", + "bonds", + "angles", + "dihedrals", + "impropers", ] expected_n_atoms = 769 @@ -62,19 +69,19 @@ def test_bonds_identity(self, top): vals = top.bonds.values for b in ((0, 1), (1, 2), (2, 3), (3, 4)): assert (b in vals) or (b[::-1] in vals) - assert ((0, 450) not in vals) + assert (0, 450) not in vals def test_angles_identity(self, top): vals = top.angles.values for b in ((0, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5)): assert (b in vals) or (b[::-1] in vals) - assert ((0, 350, 450) not in vals) + assert (0, 350, 450) not in vals def test_dihedrals_identity(self, top): vals = top.dihedrals.values for b in ((0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5), (3, 4, 5, 6)): assert (b in vals) or (b[::-1] in vals) - assert ((0, 250, 350, 450) not in vals) + assert (0, 250, 350, 450) not in vals def test_read_masses(self, top): assert_almost_equal(top.masses.values, 1.0) diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index 8711ac072a3..5b3cf93411a 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -45,13 +45,27 @@ class BaseITP(ParserBase): parser = mda.topology.ITPParser.ITPParser - expected_attrs = ['ids', 'names', 'types', 'masses', - 'charges', 'chargegroups', - 'resids', 'resnames', - 'segids', 'moltypes', 'molnums', - 'bonds', 'angles', 'dihedrals', 'impropers'] - - guessed_attrs = ['elements', ] + expected_attrs = [ + "ids", + "names", + "types", + "masses", + "charges", + "chargegroups", + "resids", + "resnames", + "segids", + "moltypes", + "molnums", + "bonds", + "angles", + "dihedrals", + "impropers", + ] + + guessed_attrs = [ + "elements", + ] expected_n_atoms = 63 expected_n_residues = 10 @@ -146,12 +160,26 @@ def test_impropers_type(self, universe): class TestITPNoMass(ParserBase): parser = mda.topology.ITPParser.ITPParser ref_filename = ITP_nomass - expected_attrs = ['ids', 'names', 'types', - 'charges', 'chargegroups', - 'resids', 'resnames', - 'segids', 'moltypes', 'molnums', - 'bonds', 'angles', 'dihedrals', 'impropers', 'masses', ] - guessed_attrs = ['elements', ] + expected_attrs = [ + "ids", + "names", + "types", + "charges", + "chargegroups", + "resids", + "resnames", + "segids", + "moltypes", + "molnums", + "bonds", + "angles", + "dihedrals", + "impropers", + "masses", + ] + guessed_attrs = [ + "elements", + ] expected_n_atoms = 60 expected_n_residues = 1 @@ -168,11 +196,23 @@ def test_mass_guess(self, universe): class TestITPAtomtypes(ParserBase): parser = mda.topology.ITPParser.ITPParser ref_filename = ITP_atomtypes - expected_attrs = ['ids', 'names', 'types', - 'charges', 'chargegroups', - 'resids', 'resnames', 'masses', - 'segids', 'moltypes', 'molnums', - 'bonds', 'angles', 'dihedrals', 'impropers'] + expected_attrs = [ + "ids", + "names", + "types", + "charges", + "chargegroups", + "resids", + "resnames", + "masses", + "segids", + "moltypes", + "molnums", + "bonds", + "angles", + "dihedrals", + "impropers", + ] expected_n_atoms = 4 expected_n_residues = 1 @@ -186,7 +226,7 @@ def test_charge_parse(self, universe): assert_allclose(universe.atoms[0].charge, 4) assert_allclose(universe.atoms[1].charge, 1.1) assert_allclose(universe.atoms[2].charge, -3.000) - assert_allclose(universe.atoms[3].charge, 1.) + assert_allclose(universe.atoms[3].charge, 1.0) def test_mass_parse_or_guess(self, universe): # read from [ atoms ] section @@ -202,12 +242,26 @@ def test_mass_parse_or_guess(self, universe): class TestITPCharges(ParserBase): parser = mda.topology.ITPParser.ITPParser ref_filename = ITP_charges - expected_attrs = ['ids', 'names', 'types', 'masses', - 'charges', 'chargegroups', - 'resids', 'resnames', - 'segids', 'moltypes', 'molnums', - 'bonds', 'angles', 'dihedrals', 'impropers'] - guessed_attrs = ['elements', ] + expected_attrs = [ + "ids", + "names", + "types", + "masses", + "charges", + "chargegroups", + "resids", + "resnames", + "segids", + "moltypes", + "molnums", + "bonds", + "angles", + "dihedrals", + "impropers", + ] + guessed_attrs = [ + "elements", + ] expected_n_atoms = 9 expected_n_residues = 3 @@ -221,7 +275,7 @@ def test_charge_parse(self, universe): assert_allclose(universe.atoms[0].charge, -1.0) assert_allclose(universe.atoms[1].charge, 0) assert_allclose(universe.atoms[2].charge, 0) - assert_allclose(universe.atoms[3].charge, -1.) + assert_allclose(universe.atoms[3].charge, -1.0) def test_masses_are_read(self, universe): assert_allclose(universe.atoms.masses, [100] * 9) @@ -252,12 +306,27 @@ 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', ] + 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. @@ -285,7 +354,7 @@ class TestITPNoKeywords(BaseITP): def test_whether_settles_types(self, universe): for param in list(universe.bonds) + list(universe.angles): - assert param.type == 'settles' + assert param.type == "settles" def test_bonds_values(self, top): vals = top.bonds.values @@ -298,8 +367,9 @@ def test_defines(self, top): 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]) + assert_allclose( + u.atoms.masses, [15.999, 15.999, 15.999, 15.999, 15.999] + ) class TestITPKeywords(TestITPNoKeywords): @@ -313,14 +383,20 @@ class TestITPKeywords(TestITPNoKeywords): @pytest.fixture def universe(self, filename): - return mda.Universe(filename, FLEXIBLE=True, EXTRA_ATOMS=True, - HW1_CHARGE=1, HW2_CHARGE=3) + 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, - HW1_CHARGE=1, HW2_CHARGE=3) + yield p.parse( + FLEXIBLE=True, EXTRA_ATOMS=True, HW1_CHARGE=1, HW2_CHARGE=3 + ) def test_whether_settles_types(self, universe): for param in list(universe.bonds) + list(universe.angles): @@ -340,6 +416,7 @@ class TestNestedIfs(BaseITP): """ Test reading ITP files with nested ifdef/ifndef conditions. """ + ref_filename = ITP_spce expected_n_atoms = 7 expected_n_residues = 1 @@ -352,7 +429,9 @@ class TestNestedIfs(BaseITP): @pytest.fixture def universe(self, filename): - return mda.Universe(filename, HEAVY_H=True, EXTRA_ATOMS=True, HEAVY_SIX=True) + return mda.Universe( + filename, HEAVY_H=True, EXTRA_ATOMS=True, HEAVY_SIX=True + ) @pytest.fixture() def top(self, filename): @@ -386,7 +465,9 @@ def top(self, filename): @pytest.fixture() def universe(self, filename): - return mda.Universe(filename, topology_format='ITP', include_dir=GMX_DIR) + return mda.Universe( + filename, topology_format="ITP", include_dir=GMX_DIR + ) def test_output(self, filename): """Testing the call signature""" @@ -395,19 +476,21 @@ def test_output(self, filename): def test_creates_universe(self, filename): """Check that Universe works with this Parser""" - u = mda.Universe(filename, topology_format='ITP', include_dir=GMX_DIR) + 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) + 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) - assert_equal(universe.residues.resindices, np.arange(self.expected_n_residues)) + assert_equal( + universe.residues.resindices, np.arange(self.expected_n_residues) + ) assert_equal(universe.atoms.chargegroups[-1], 63) @@ -442,7 +525,7 @@ def test_relstring(self, tmpdir): p2 = tmpdir.mkdir("sub2") p2.chdir() with p2.as_cwd() as pchange: - u = mda.Universe(str("../sub1/test.itp"), format='ITP') + u = mda.Universe(str("../sub1/test.itp"), format="ITP") def test_relpath(self, tmpdir): content = """ @@ -455,7 +538,7 @@ def test_relpath(self, tmpdir): p2.chdir() with p2.as_cwd() as pchange: relpath = Path("../sub1/test.itp") - u = mda.Universe(relpath, format='ITP') + u = mda.Universe(relpath, format="ITP") def test_relative_path(self, tmpdir): test_itp_content = '#include "../atoms.itp"' @@ -485,8 +568,10 @@ def test_missing_elements_no_attribute(): 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 ") + 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): diff --git a/testsuite/MDAnalysisTests/topology/test_lammpsdata.py b/testsuite/MDAnalysisTests/topology/test_lammpsdata.py index 7e76c2e7a3d..c5f087b89b2 100644 --- a/testsuite/MDAnalysisTests/topology/test_lammpsdata.py +++ b/testsuite/MDAnalysisTests/topology/test_lammpsdata.py @@ -43,8 +43,16 @@ class LammpsBase(ParserBase): parser = mda.topology.LAMMPSParser.DATAParser expected_n_segments = 1 - expected_attrs = ['types', 'resids', 'masses', 'charges', - 'bonds', 'angles', 'dihedrals', 'impropers'] + expected_attrs = [ + "types", + "resids", + "masses", + "charges", + "bonds", + "angles", + "dihedrals", + "impropers", + ] def test_n_atom_types(self, top): assert_equal(len(set(top.types.values)), self.expected_n_atom_types) @@ -53,7 +61,7 @@ def test_n_bonds(self, top): if self.ref_n_bonds: assert_equal(len(top.bonds.values), self.ref_n_bonds) else: - assert not hasattr(top, 'bonds') + assert not hasattr(top, "bonds") def test_bond_member(self, top): if self.ref_n_bonds: @@ -63,7 +71,7 @@ def test_n_angles(self, top): if self.ref_n_angles: assert_equal(len(top.angles.values), self.ref_n_angles) else: - assert not hasattr(self.top, 'angles') + assert not hasattr(self.top, "angles") def test_angle_member(self, top): if self.ref_n_angles: @@ -73,7 +81,7 @@ def test_n_dihedrals(self, top): if self.ref_n_dihedrals: assert_equal(len(top.dihedrals.values), self.ref_n_dihedrals) else: - assert not hasattr(self.top, 'dihedrals') + assert not hasattr(self.top, "dihedrals") def test_dihedral_member(self, top): if self.ref_n_dihedrals: @@ -83,17 +91,17 @@ def test_n_impropers(self, top): if self.ref_n_impropers: assert_equal(len(top.impropers.values), self.ref_n_impropers) else: - assert not hasattr(self.top, 'impropers') + assert not hasattr(self.top, "impropers") def test_improper_member(self, top): if self.ref_n_impropers: assert self.ref_improper in top.impropers.values def test_creates_universe(self, filename): - u = mda.Universe(filename, format='DATA') + u = mda.Universe(filename, format="DATA") def test_guessed_attributes(self, filename): - u = mda.Universe(filename, format='DATA') + u = mda.Universe(filename, format="DATA") for attr in self.guessed_attrs: assert hasattr(u.atoms, attr) @@ -104,6 +112,7 @@ class TestLammpsData(LammpsBase): The reading of coords and velocities is done separately in test_coordinates """ + ref_filename = LAMMPSdata expected_n_atoms = 18364 expected_n_atom_types = 10 @@ -165,24 +174,32 @@ class TestLAMMPSDeletedAtoms(LammpsBase): def test_atom_ids(self, filename): u = mda.Universe(filename) - assert_equal(u.atoms.ids, - [1, 10, 1002, 2003, 2004, 2005, 2006, 2007, 2008, 2009]) + assert_equal( + u.atoms.ids, + [1, 10, 1002, 2003, 2004, 2005, 2006, 2007, 2008, 2009], + ) def test_traj(self, filename): u = mda.Universe(filename) - assert_equal(u.atoms.positions, - np.array([[11.8998565674, 48.4455718994, 19.0971984863], - [14.5285415649, 50.6892776489, 19.9419136047], - [12.8466796875, 48.1473007202, 18.6461906433], - [11.0093536377, 48.7145767212, 18.5247917175], - [12.4033203125, 49.2582168579, 20.2825050354], - [13.0947723389, 48.8437194824, 21.0175533295], - [11.540184021, 49.6138534546, 20.8459072113], - [13.0085144043, 50.6062469482, 19.9141769409], - [12.9834518433, 51.1562423706, 18.9713554382], - [12.6588821411, 51.4160842896, 20.5548400879]], - dtype=np.float32)) + assert_equal( + u.atoms.positions, + np.array( + [ + [11.8998565674, 48.4455718994, 19.0971984863], + [14.5285415649, 50.6892776489, 19.9419136047], + [12.8466796875, 48.1473007202, 18.6461906433], + [11.0093536377, 48.7145767212, 18.5247917175], + [12.4033203125, 49.2582168579, 20.2825050354], + [13.0947723389, 48.8437194824, 21.0175533295], + [11.540184021, 49.6138534546, 20.8459072113], + [13.0085144043, 50.6062469482, 19.9141769409], + [12.9834518433, 51.1562423706, 18.9713554382], + [12.6588821411, 51.4160842896, 20.5548400879], + ], + dtype=np.float32, + ), + ) class TestLammpsDataPairIJ(LammpsBase): @@ -190,8 +207,15 @@ class TestLammpsDataPairIJ(LammpsBase): PairIJ Coeffs section """ - expected_attrs = ['types', 'resids', 'masses', - 'bonds', 'angles', 'dihedrals', 'impropers'] + expected_attrs = [ + "types", + "resids", + "masses", + "bonds", + "angles", + "dihedrals", + "impropers", + ] ref_filename = LAMMPSdata_PairIJ expected_n_atoms = 800 expected_n_atom_types = 2 @@ -228,47 +252,62 @@ class TestLammpsDataPairIJ(LammpsBase): 1 1 3.7151744275286681e+01 1.8684434743140471e+01 1.9285127961842125e+01 0 0 0 """ + def test_noresid(): - u = mda.Universe(StringIO(LAMMPS_NORESID), format='data', - atom_style='id type x y z') + u = mda.Universe( + StringIO(LAMMPS_NORESID), format="data", atom_style="id type x y z" + ) assert len(u.atoms) == 1 assert_equal(u.atoms[0].mass, 28.0) - assert_equal(u.atoms.positions, - np.array([[3.7151744275286681e+01, - 1.8684434743140471e+01, - 1.9285127961842125e+01]], dtype=np.float32)) + assert_equal( + u.atoms.positions, + np.array( + [ + [ + 3.7151744275286681e01, + 1.8684434743140471e01, + 1.9285127961842125e01, + ] + ], + dtype=np.float32, + ), + ) + def test_noresid_failure(): with pytest.raises( - ValueError, - match='.+?You can supply a description of the atom_style.+?', + ValueError, + match=".+?You can supply a description of the atom_style.+?", ): - u = mda.Universe(StringIO(LAMMPS_NORESID), format='data') + u = mda.Universe(StringIO(LAMMPS_NORESID), format="data") def test_interpret_atom_style(): style = mda.topology.LAMMPSParser.DATAParser._interpret_atom_style( - 'id charge type z y x') + "id charge type z y x" + ) assert isinstance(style, dict) - assert style['id'] == 0 - assert style['type'] == 2 - assert style['charge'] == 1 - assert style['x'] == 5 - assert style['y'] == 4 - assert style['z'] == 3 + assert style["id"] == 0 + assert style["type"] == 2 + assert style["charge"] == 1 + assert style["x"] == 5 + assert style["y"] == 4 + assert style["z"] == 3 def test_interpret_atom_style_missing(): - with pytest.raises(ValueError, - match='atom_style string missing required.+?'): + with pytest.raises( + ValueError, match="atom_style string missing required.+?" + ): style = mda.topology.LAMMPSParser.DATAParser._interpret_atom_style( - 'id charge z y x') + "id charge z y x" + ) class TestDumpParser(ParserBase): - expected_attrs = ['types', 'masses'] + expected_attrs = ["types", "masses"] expected_n_atoms = 24 expected_n_residues = 1 expected_n_segments = 1 @@ -277,7 +316,7 @@ class TestDumpParser(ParserBase): ref_filename = LAMMPSDUMP def test_creates_universe(self): - u = mda.Universe(self.ref_filename, format='LAMMPSDUMP') + u = mda.Universe(self.ref_filename, format="LAMMPSDUMP") assert isinstance(u, mda.Universe) assert len(u.atoms) == 24 @@ -286,30 +325,31 @@ def test_masses_warning(self): # masses are mandatory, but badly guessed # check that user is alerted with self.parser(self.ref_filename) as p: - with pytest.warns(UserWarning, match='Guessed all Masses to 1.0'): + 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') + 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') + 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.] + u = mda.Universe(filename, format="LAMMPSDUMP") + expected = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] 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'] + 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 1a1cee1d6a0..2d0e1ffd87d 100644 --- a/testsuite/MDAnalysisTests/topology/test_minimal.py +++ b/testsuite/MDAnalysisTests/topology/test_minimal.py @@ -41,7 +41,8 @@ working_readers = pytest.mark.parametrize( - 'filename,expected_n_atoms', [ + "filename,expected_n_atoms", + [ (DCD, 3341), (INPCRD, 5), (LAMMPSdcd2, 12421), @@ -49,7 +50,9 @@ (TRR, 47681), (XTC, 47681), (np.zeros((1, 10, 3)), 10), # memory reader default - ]) + ], +) + @working_readers def test_minimal_parser(filename, expected_n_atoms): @@ -65,14 +68,18 @@ def test_universe_with_minimal(filename, expected_n_atoms): assert len(u.atoms) == expected_n_atoms -nonworking_readers = pytest.mark.parametrize('filename,n_atoms', [ - (TRJ, 252), - (TRJncdf, 2661), - (TRZ, 8184), -]) +nonworking_readers = pytest.mark.parametrize( + "filename,n_atoms", + [ + (TRJ, 252), + (TRJncdf, 2661), + (TRZ, 8184), + ], +) + @nonworking_readers -def test_minimal_parser_fail(filename,n_atoms): +def test_minimal_parser_fail(filename, n_atoms): with MinimalParser(filename) as p: with pytest.raises(NotImplementedError): p.parse() @@ -89,15 +96,18 @@ def test_minimal_n_atoms_kwarg(filename, n_atoms): def memory_possibilities(): # iterate over all possible shapes for a MemoryReader array # number of frames, atoms and coordinates - n = {'f': 1, 'a': 10, 'c': 3} - for permutation in itertools.permutations('fac', 3): - order = ''.join(permutation) + n = {"f": 1, "a": 10, "c": 3} + for permutation in itertools.permutations("fac", 3): + order = "".join(permutation) array = np.zeros([n[val] for val in permutation]) yield array, order -memory_reader = pytest.mark.parametrize('array,order', list(memory_possibilities())) +memory_reader = pytest.mark.parametrize( + "array,order", list(memory_possibilities()) +) + @memory_reader def test_memory_minimal_parser(array, order): @@ -105,6 +115,7 @@ def test_memory_minimal_parser(array, order): top = p.parse(order=order) assert top.n_atoms == 10 + @memory_reader def test_memory_universe(array, order): u = mda.Universe(array, order=order, to_guess=()) diff --git a/testsuite/MDAnalysisTests/topology/test_mmtf.py b/testsuite/MDAnalysisTests/topology/test_mmtf.py index 9e2a85f7784..7125d23f2d3 100644 --- a/testsuite/MDAnalysisTests/topology/test_mmtf.py +++ b/testsuite/MDAnalysisTests/topology/test_mmtf.py @@ -12,16 +12,28 @@ class MMTFBase(ParserBase): expected_attrs = [ - 'ids', 'names', 'types', 'altLocs', 'tempfactors', 'occupancies', - 'charges', 'names', 'resnames', 'resids', 'resnums', 'icodes', - 'segids', 'bonds', 'models' + "ids", + "names", + "types", + "altLocs", + "tempfactors", + "occupancies", + "charges", + "names", + "resnames", + "resids", + "resnums", + "icodes", + "segids", + "bonds", + "models", ] class TestMMTFParser(MMTFBase): parser = mda.topology.MMTFParser.MMTFParser ref_filename = MMTF - guessed_attrs = ['masses'] + guessed_attrs = ["masses"] expected_n_atoms = 512 expected_n_residues = 124 expected_n_segments = 8 @@ -40,7 +52,7 @@ class TestMMTFSkinny(MMTFBase): # for all attributes often in MMTF, # check that we get expected error on access # (sort so pytest gets reliable order) - guessed_attrs = ['ids', 'masses', 'segids'] + guessed_attrs = ["ids", "masses", "segids"] expected_n_atoms = 660 expected_n_residues = 134 expected_n_segments = 2 @@ -49,7 +61,7 @@ class TestMMTFSkinny(MMTFBase): class TestMMTFSkinny2(MMTFBase): parser = mda.topology.MMTFParser.MMTFParser ref_filename = MMTF_skinny2 - guessed_attrs = ['ids', 'masses', 'segids'] + guessed_attrs = ["ids", "masses", "segids"] expected_n_atoms = 169 expected_n_residues = 44 expected_n_segments = 2 @@ -70,10 +82,10 @@ def test_names(self, u): assert_equal(u.atoms.names[:3], ["O5'", "C5'", "C4'"]) def test_resnames(self, u): - assert_equal(u.residues.resnames[:3], ['DG', 'DA', 'DA']) + assert_equal(u.residues.resnames[:3], ["DG", "DA", "DA"]) def test_segids(self, u): - assert_equal(u.segments[:3].segids, ['A', 'B', 'C']) + assert_equal(u.segments[:3].segids, ["A", "B", "C"]) def test_resids(self, u): assert_equal(u.residues.resids[-3:], [2008, 2009, 2010]) @@ -86,16 +98,16 @@ def test_bfactors(self, u): assert_equal(u.atoms.bfactors[:3], [9.48, 10.88, 10.88]) def test_types(self, u): - assert_equal(u.atoms.types[:3], ['O', 'C', 'C']) + assert_equal(u.atoms.types[:3], ["O", "C", "C"]) def test_models(self, u): assert all(u.atoms.models == 0) def test_icodes(self, u): - assert all(u.atoms.icodes == '') + assert all(u.atoms.icodes == "") def test_altlocs(self, u): - assert all(u.atoms.altLocs[:3] == '') + 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] @@ -143,33 +155,33 @@ def u(self): return mda.Universe(MMTF_gz) def test_model_selection(self, u): - m1 = u.select_atoms('model 0') - m2 = u.select_atoms('model 1') + m1 = u.select_atoms("model 0") + m2 = u.select_atoms("model 1") assert len(m1) == 570 assert len(m2) == 570 def test_model_multiple(self, u): - m2plus = u.select_atoms('model 1-10') + m2plus = u.select_atoms("model 1-10") assert len(m2plus) == 570 def test_model_multiple_2(self, u): - m2plus = u.select_atoms('model 1:10') + m2plus = u.select_atoms("model 1:10") assert len(m2plus) == 570 def test_model_multiple_3(self, u): - m1and2 = u.select_atoms('model 0-1') + m1and2 = u.select_atoms("model 0-1") assert len(m1and2) == 1140 def test_model_multiple_4(self, u): - m1and2 = u.select_atoms('model 0:1') + m1and2 = u.select_atoms("model 0:1") assert len(m1and2) == 1140 def test_model_multiple_5(self, u): - m1and2 = u.select_atoms('model 0 1') + m1and2 = u.select_atoms("model 0 1") assert len(m1and2) == 1140 diff --git a/testsuite/MDAnalysisTests/topology/test_mol2.py b/testsuite/MDAnalysisTests/topology/test_mol2.py index 604fbe63628..4157e8273a1 100644 --- a/testsuite/MDAnalysisTests/topology/test_mol2.py +++ b/testsuite/MDAnalysisTests/topology/test_mol2.py @@ -176,11 +176,17 @@ class TestMOL2Base(ParserBase): parser = mda.topology.MOL2Parser.MOL2Parser expected_attrs = [ - 'ids', 'names', 'types', 'charges', 'resids', 'resnames', 'bonds', - 'elements', + "ids", + "names", + "types", + "charges", + "resids", + "resnames", + "bonds", + "elements", ] - guessed_attrs = ['masses'] + guessed_attrs = ["masses"] expected_n_atoms = 49 expected_n_residues = 1 expected_n_segments = 1 @@ -204,10 +210,12 @@ def filename(self, request): def test_bond_orders(): - ref_orders = ('am 1 1 2 1 2 1 1 am 1 1 am 2 2 ' - '1 1 1 1 1 1 1 1 1 1 1 1 1 1 ' - 'ar ar ar 1 ar 1 ar 1 ar 1 1 1 ' - '2 1 1 1 1 2 1 1 2 1 1').split() + ref_orders = ( + "am 1 1 2 1 2 1 1 am 1 1 am 2 2 " + "1 1 1 1 1 1 1 1 1 1 1 1 1 1 " + "ar ar ar 1 ar 1 ar 1 ar 1 1 1 " + "2 1 1 1 1 2 1 1 2 1 1" + ).split() u = mda.Universe(mol2_molecule) orders = [bond.order for bond in u.atoms.bonds] assert_equal(orders, ref_orders) @@ -217,8 +225,7 @@ def test_elements(): u = mda.Universe(mol2_molecule) assert_equal( - u.atoms.elements[:5], - np.array(["N", "S", "N", "N", "O"], dtype="U3") + u.atoms.elements[:5], np.array(["N", "S", "N", "N", "O"], dtype="U3") ) @@ -227,22 +234,19 @@ def test_elements_selection(): u = mda.Universe(mol2_molecule) ag = u.select_atoms("element S") - assert_equal( - ag.elements, - np.array(["S", "S"], dtype="U3") - ) + assert_equal(ag.elements, np.array(["S", "S"], dtype="U3")) def test_wrong_elements_warnings(): - with pytest.warns(UserWarning, match='Unknown elements found') as record: - u = mda.Universe(StringIO(mol2_wrong_element), format='MOL2') + 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 masses PendingDeprecationWarning assert len(record) == 3 - expected_elements = np.array(['N', '', ''], dtype=object) + 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']) + gussed_types = np.array(["N.am", "X.o2", "XX.am"]) assert_equal(u.atoms.elements, expected_elements) assert_equal(u.atoms.types, gussed_types) @@ -250,29 +254,40 @@ def test_wrong_elements_warnings(): def test_all_wrong_elements_warnings(): - with pytest.warns(UserWarning, match='Unknown elements found'): - u = mda.Universe(StringIO(mol2_all_wrong_elements), format='MOL2') + with pytest.warns(UserWarning, match="Unknown elements found"): + u = mda.Universe(StringIO(mol2_all_wrong_elements), format="MOL2") - with pytest.raises(mda.exceptions.NoDataError, - match='This Universe does not contain element ' - 'information'): + with pytest.raises( + mda.exceptions.NoDataError, + match="This Universe does not contain element " "information", + ): u.atoms.elements def test_all_elements(): - with pytest.warns(UserWarning, match='Unknown elements found'): - u = mda.Universe(StringIO(mol2_fake), format='MOL2') - - expected = ["H"] * 2 + [""] + ["C"] * 5 + [""] + ["N"] * 4 + ["O"] * 5 + \ - ["S"] * 6 + ["P"] + ["Cr"] * 2 + ["Co"] + with pytest.warns(UserWarning, match="Unknown elements found"): + u = mda.Universe(StringIO(mol2_fake), format="MOL2") + + expected = ( + ["H"] * 2 + + [""] + + ["C"] * 5 + + [""] + + ["N"] * 4 + + ["O"] * 5 + + ["S"] * 6 + + ["P"] + + ["Cr"] * 2 + + ["Co"] + ) expected = np.array(expected, dtype=object) assert_equal(u.atoms.elements, expected) # Test for Issue #3385 / PR #3598 def test_wo_optional_columns(): - u = mda.Universe(StringIO(mol2_wo_opt_col), format='MOL2') + u = mda.Universe(StringIO(mol2_wo_opt_col), format="MOL2") assert_equal(u.atoms.resids, np.array([1, 1])) with pytest.raises(mda.exceptions.NoDataError): u.atoms.resnames @@ -281,7 +296,7 @@ def test_wo_optional_columns(): def test_partial_optional_columns(): - u = mda.Universe(StringIO(mol2_partial_opt_col), format='MOL2') + u = mda.Universe(StringIO(mol2_partial_opt_col), format="MOL2") assert_equal(u.atoms.resids, np.array([1, 2])) with pytest.raises(mda.exceptions.NoDataError): u.atoms.resnames @@ -290,27 +305,27 @@ def test_partial_optional_columns(): def test_mol2_wo_required_columns(): - with pytest.raises(ValueError, - match='The @ATOM block in mol2 file'): - u = mda.Universe(StringIO(mol2_wo_required_col), format='MOL2') + with pytest.raises( + ValueError, match="The @ATOM block in mol2 file" + ): + u = mda.Universe(StringIO(mol2_wo_required_col), format="MOL2") def test_mol2_no_charges(): - with pytest.raises(ValueError, - match='indicates no charges'): - u = mda.Universe(StringIO(mol2_no_charge_error1), format='MOL2') - with pytest.raises(ValueError, - match='indicates a charge model'): - u = mda.Universe(StringIO(mol2_no_charge_error2), format='MOL2') + with pytest.raises(ValueError, match="indicates no charges"): + u = mda.Universe(StringIO(mol2_no_charge_error1), format="MOL2") + with pytest.raises(ValueError, match="indicates a charge model"): + u = mda.Universe(StringIO(mol2_no_charge_error2), format="MOL2") def test_unformat(): - with pytest.raises(ValueError, - match='Some atoms in the mol2 file'): - u = mda.Universe(StringIO(mol2_resname_unformat), format='MOL2') + 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]) + 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 51822e96710..146462969af 100644 --- a/testsuite/MDAnalysisTests/topology/test_pdb.py +++ b/testsuite/MDAnalysisTests/topology/test_pdb.py @@ -22,28 +22,28 @@ # from io import StringIO -import pytest -import numpy as np -from numpy.testing import assert_equal, assert_allclose import MDAnalysis as mda +import numpy as np +import pytest +from MDAnalysis import NoDataError +from MDAnalysis.guesser import tables +from MDAnalysis.topology.PDBParser import PDBParser +from numpy.testing import assert_allclose, assert_equal -from MDAnalysisTests.topology.base import ParserBase from MDAnalysisTests.datafiles import ( PDB, PDB_HOLE, - PDB_small, + PDB_chainidnewres, + PDB_charges, PDB_conect, PDB_conect2TER, - PDB_singleconect, - PDB_chainidnewres, - PDB_sameresid_diffresname, - PDB_helix, PDB_elements, - PDB_charges, + PDB_helix, + PDB_sameresid_diffresname, + PDB_singleconect, + PDB_small, ) -from MDAnalysis.topology.PDBParser import PDBParser -from MDAnalysis import NoDataError -from MDAnalysis.guesser import tables +from MDAnalysisTests.topology.base import ParserBase _PDBPARSER = mda.topology.PDBParser.PDBParser @@ -64,24 +64,34 @@ (" 24", 24), (" 645", 645), (" 4951", 4951), - ("10267", 10267) + ("10267", 10267), ] -@pytest.mark.parametrize('hybrid, integer', hybrid36) +@pytest.mark.parametrize("hybrid, integer", hybrid36) def test_hy36decode(hybrid, integer): assert mda.topology.PDBParser.hy36decode(5, hybrid) == integer class PDBBase(ParserBase): - expected_attrs = ['ids', 'names', 'record_types', 'resids', - 'resnames', 'altLocs', 'icodes', 'occupancies', - 'tempfactors', 'chainIDs'] - guessed_attrs = ['types', 'masses'] + expected_attrs = [ + "ids", + "names", + "record_types", + "resids", + "resnames", + "altLocs", + "icodes", + "occupancies", + "tempfactors", + "chainIDs", + ] + guessed_attrs = ["types", "masses"] class TestPDBParser(PDBBase): """This one has neither chainids or segids""" + parser = mda.topology.PDBParser.PDBParser ref_filename = PDB expected_n_atoms = 47681 @@ -91,6 +101,7 @@ class TestPDBParser(PDBBase): class TestPDBParserSegids(PDBBase): """Has segids""" + parser = mda.topology.PDBParser.PDBParser ref_filename = PDB_small expected_n_atoms = 3341 @@ -102,20 +113,24 @@ class TestPDBConect(object): """Testing PDB topology parsing (PDB)""" def test_conect_parser(self): - lines = ("CONECT1233212331", - "CONECT123331233112334", - "CONECT123341233312335", - "CONECT123351233412336", - "CONECT12336123271233012335", - "CONECT12337 7718 84081234012344", - "CONECT1233812339123401234112345") - results = ((12332, [12331]), - (12333, [12331, 12334]), - (12334, [12333, 12335]), - (12335, [12334, 12336]), - (12336, [12327, 12330, 12335]), - (12337, [7718, 8408, 12340, 12344]), - (12338, [12339, 12340, 12341, 12345])) + lines = ( + "CONECT1233212331", + "CONECT123331233112334", + "CONECT123341233312335", + "CONECT123351233412336", + "CONECT12336123271233012335", + "CONECT12337 7718 84081234012344", + "CONECT1233812339123401234112345", + ) + results = ( + (12332, [12331]), + (12333, [12331, 12334]), + (12334, [12333, 12335]), + (12335, [12334, 12336]), + (12336, [12327, 12330, 12335]), + (12337, [7718, 8408, 12340, 12344]), + (12338, [12339, 12340, 12341, 12345]), + ) for line, res in zip(lines, results): bonds = mda.topology.PDBParser._parse_conect(line) assert_equal(bonds[0], res[0]) @@ -124,8 +139,9 @@ def test_conect_parser(self): def test_conect_parser_runtime(self): with pytest.raises(RuntimeError): - mda.topology.PDBParser._parse_conect('CONECT12337 7718 ' - '84081234012344123') + mda.topology.PDBParser._parse_conect( + "CONECT12337 7718 " "84081234012344123" + ) def test_conect_topo_parser(self): """Check that the parser works as intended, @@ -145,7 +161,7 @@ def parse(): with pytest.warns(UserWarning): struc = parse() - assert hasattr(struc, 'bonds') + assert hasattr(struc, "bonds") assert len(struc.bonds.values) == 4 @@ -157,7 +173,7 @@ def parse(): with pytest.warns(UserWarning): struc = parse() - assert hasattr(struc, 'bonds') + assert hasattr(struc, "bonds") assert len(struc.bonds.values) == 2 @@ -169,7 +185,7 @@ def test_new_chainid_new_res(): assert len(u.residues) == 4 assert_equal(u.residues.resids, [1, 2, 3, 3]) assert len(u.segments) == 4 - assert_equal(u.segments.segids, ['A', 'B', 'C', 'D']) + assert_equal(u.segments.segids, ["A", "B", "C", "D"]) assert len(u.segments[0].atoms) == 5 assert len(u.segments[1].atoms) == 5 assert len(u.segments[2].atoms) == 5 @@ -180,7 +196,7 @@ def test_sameresid_diffresname(): with _PDBPARSER(PDB_sameresid_diffresname) as p: top = p.parse() resids = [9, 9] - resnames = ['GLN', 'POPC'] + resnames = ["GLN", "POPC"] for i, (resid, resname) in enumerate(zip(resids, resnames)): assert top.resids.values[i] == resid assert top.resnames.values[i] == resname @@ -189,11 +205,11 @@ def test_sameresid_diffresname(): def test_PDB_record_types(): u = mda.Universe(PDB_HOLE) - assert u.atoms[0].record_type == 'ATOM' - assert u.atoms[132].record_type == 'HETATM' + assert u.atoms[0].record_type == "ATOM" + assert u.atoms[132].record_type == "HETATM" - assert_equal(u.atoms[10:20].record_types, 'ATOM') - assert_equal(u.atoms[271:].record_types, 'HETATM') + assert_equal(u.atoms[10:20].record_types, "ATOM") + assert_equal(u.atoms[271:].record_types, "HETATM") PDB_noresid = """\ @@ -210,7 +226,7 @@ def test_PDB_record_types(): def test_PDB_no_resid(): - u = mda.Universe(StringIO(PDB_noresid), format='PDB') + u = mda.Universe(StringIO(PDB_noresid), format="PDB") assert len(u.atoms) == 4 assert len(u.residues) == 1 @@ -242,7 +258,7 @@ def test_PDB_no_resid(): def test_PDB_hex(): - u = mda.Universe(StringIO(PDB_hex), format='PDB') + u = mda.Universe(StringIO(PDB_hex), format="PDB") assert len(u.atoms) == 5 assert u.atoms[0].id == 1 assert u.atoms[1].id == 100000 @@ -253,7 +269,7 @@ def test_PDB_hex(): @pytest.mark.filterwarnings("error:Failed to guess the mass") def test_PDB_metals(): - u = mda.Universe(StringIO(PDB_metals), format='PDB') + u = mda.Universe(StringIO(PDB_metals), format="PDB") assert len(u.atoms) == 4 assert u.atoms[0].mass == pytest.approx(tables.masses["CU"]) @@ -266,11 +282,17 @@ def test_PDB_elements(): """The test checks whether elements attribute are assigned properly given a PDB file with valid elements record. """ - u = mda.Universe(PDB_elements, format='PDB') - element_list = np.array(['N', 'C', 'C', 'O', 'C', 'C', 'O', 'N', 'H', - 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'Cu', 'Fe', - 'Mg', 'Ca', 'S', 'O', 'C', 'C', 'S', 'O', 'C', - 'C'], dtype=object) + u = mda.Universe(PDB_elements, format="PDB") + # fmt: off + element_list = np.array( + [ + 'N', 'C', 'C', 'O', 'C', 'C', 'O', 'N', 'H', 'H', 'H', 'H', 'H', + 'H', 'H', 'H', 'Cu', 'Fe', 'Mg', 'Ca', 'S', 'O', 'C', 'C', 'S', + 'O', 'C', 'C' + ], + dtype=object + ) + # fmt: on assert_equal(u.atoms.elements, element_list) @@ -280,8 +302,10 @@ def test_missing_elements_noattribute(): 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" + ) with pytest.warns(UserWarning, match=wmsg): u = mda.Universe(PDB_small) with pytest.raises(AttributeError): @@ -308,14 +332,21 @@ def test_wrong_elements_warnings(): """The test checks whether there are invalid elements in the elements 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') - - 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) + with pytest.warns(UserWarning, match="Unknown element XX found"): + 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) @@ -324,12 +355,22 @@ def test_wrong_elements_warnings(): def test_guessed_masses_and_types_values(): """Test that guessed masses and types have the expected values for universe - constructed from PDB file. + 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] + 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) @@ -353,9 +394,15 @@ def test_PDB_charges(): properly given a PDB file with a valid formal charges record. """ u = mda.Universe(PDB_charges) - formal_charges = np.array([0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0], dtype=int) + # fmt: off + formal_charges = np.array( + [ + 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], + dtype=int + ) + # fmt: on assert_equal(u.atoms.formalcharges, formal_charges) @@ -377,10 +424,10 @@ def test_PDB_charges(): """ -@pytest.mark.parametrize('infile,entry', [ - [PDB_charges_nosign, r'2'], - [PDB_charges_invertsign, r'\+2'] -]) +@pytest.mark.parametrize( + "infile,entry", + [[PDB_charges_nosign, r"2"], [PDB_charges_invertsign, r"\+2"]], +) def test_PDB_bad_charges(infile, entry): """ Test that checks that a warning is raised and formal charges are not set: @@ -389,5 +436,5 @@ def test_PDB_bad_charges(infile, entry): """ wmsg = f"Unknown entry {entry} encountered in formal charge field." with pytest.warns(UserWarning, match=wmsg): - u = mda.Universe(StringIO(infile), format='PDB') - assert not hasattr(u, 'formalcharges') + u = mda.Universe(StringIO(infile), format="PDB") + assert not hasattr(u, "formalcharges") diff --git a/testsuite/MDAnalysisTests/topology/test_pdbqt.py b/testsuite/MDAnalysisTests/topology/test_pdbqt.py index 9578c1e9483..02b326b5f53 100644 --- a/testsuite/MDAnalysisTests/topology/test_pdbqt.py +++ b/testsuite/MDAnalysisTests/topology/test_pdbqt.py @@ -51,15 +51,16 @@ class TestPDBQT(ParserBase): "tempfactors", ] - guessed_attrs = ['masses'] + 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]) + assert_allclose( + u.atoms.masses[:7], [14.007, 0.0, 0.0, 12.011, 12.011, 0.0, 12.011] + ) def test_footnote(): diff --git a/testsuite/MDAnalysisTests/topology/test_pqr.py b/testsuite/MDAnalysisTests/topology/test_pqr.py index fa35171efe7..df3790b7a75 100644 --- a/testsuite/MDAnalysisTests/topology/test_pqr.py +++ b/testsuite/MDAnalysisTests/topology/test_pqr.py @@ -34,10 +34,18 @@ class TestPQRParser(ParserBase): parser = mda.topology.PQRParser.PQRParser ref_filename = PQR - expected_attrs = ['ids', 'names', 'charges', 'radii', 'record_types', - 'resids', 'resnames', 'icodes', - 'segids'] - guessed_attrs = ['masses', 'types'] + expected_attrs = [ + "ids", + "names", + "charges", + "radii", + "record_types", + "resids", + "resnames", + "icodes", + "segids", + ] + guessed_attrs = ["masses", "types"] expected_n_atoms = 3341 expected_n_residues = 214 expected_n_segments = 1 @@ -51,8 +59,8 @@ 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'] + 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) @@ -69,20 +77,20 @@ class TestPQRParser2(TestPQRParser): 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'] + expected_types = ["N", "C", "C", "O", "C", "C", "C"] def test_record_types(): u = mda.Universe(PQR_icodes) - assert u.atoms[4052].record_type == 'ATOM' - assert u.atoms[4053].record_type == 'HETATM' + assert u.atoms[4052].record_type == "ATOM" + assert u.atoms[4053].record_type == "HETATM" - assert_equal(u.atoms[:10].record_types, 'ATOM') - assert_equal(u.atoms[4060:4070].record_types, 'HETATM') + assert_equal(u.atoms[:10].record_types, "ATOM") + assert_equal(u.atoms[4060:4070].record_types, "HETATM") -GROMACS_PQR = ''' +GROMACS_PQR = """ REMARK The B-factors in this file hold atomic radii REMARK The occupancy in this file hold atomic charges TITLE system @@ -92,17 +100,19 @@ def test_record_types(): ATOM 1 O ZR 1 15.710 17.670 23.340 -0.67 1.48 O TER ENDMDL -''' +""" def test_gromacs_flavour(): - u = mda.Universe(StringIO(GROMACS_PQR), format='PQR') + u = mda.Universe(StringIO(GROMACS_PQR), format="PQR") assert len(u.atoms) == 1 # topology things - assert u.atoms[0].type == 'O' - assert u.atoms[0].segid == 'SYSTEM' + assert u.atoms[0].type == "O" + assert u.atoms[0].segid == "SYSTEM" assert_almost_equal(u.atoms[0].radius, 1.48, decimal=5) assert_almost_equal(u.atoms[0].charge, -0.67, decimal=5) # coordinatey things - assert_almost_equal(u.atoms[0].position, [15.710, 17.670, 23.340], decimal=4) + assert_almost_equal( + u.atoms[0].position, [15.710, 17.670, 23.340], decimal=4 + ) diff --git a/testsuite/MDAnalysisTests/topology/test_psf.py b/testsuite/MDAnalysisTests/topology/test_psf.py index ccfbb0bddd8..c86e6a781c5 100644 --- a/testsuite/MDAnalysisTests/topology/test_psf.py +++ b/testsuite/MDAnalysisTests/topology/test_psf.py @@ -37,34 +37,45 @@ XYZ, ) + class PSFBase(ParserBase): parser = mda.topology.PSFParser.PSFParser - expected_attrs = ['ids', 'names', 'types', 'masses', - 'charges', - 'resids', 'resnames', - 'segids', - 'bonds', 'angles', 'dihedrals', 'impropers'] + expected_attrs = [ + "ids", + "names", + "types", + "masses", + "charges", + "resids", + "resnames", + "segids", + "bonds", + "angles", + "dihedrals", + "impropers", + ] class TestPSFParser(PSFBase): """ Based on small PDB with AdK (:data:`PDB_small`). """ + ref_filename = PSF expected_n_atoms = 3341 expected_n_residues = 214 expected_n_segments = 1 - @pytest.fixture(params=['uncompressed', 'bz2']) + @pytest.fixture(params=["uncompressed", "bz2"]) def filename(self, request, tmpdir): - if request.param == 'uncompressed': + if request.param == "uncompressed": return self.ref_filename else: - fn = str(tmpdir.join('file.psf.bz2')) - with open(self.ref_filename, 'rb') as f: + fn = str(tmpdir.join("file.psf.bz2")) + with open(self.ref_filename, "rb") as f: stuff = f.read() buf = bz2.compress(stuff) - with open(fn, 'wb') as out: + with open(fn, "wb") as out: out.write(buf) return fn @@ -114,6 +125,7 @@ class TestNAMDPSFParser(PSFBase): https://github.com/MDAnalysis/mdanalysis/issues/107 """ + ref_filename = PSF_NAMD expected_n_atoms = 130 expected_n_residues = 6 @@ -134,6 +146,7 @@ def test_as_universe_resids(self): for seg in u.segments: assert_equal(seg.residues.resids[:4], [380, 381, 382, 383]) + class TestPSFParserNoTop(PSFBase): ref_filename = PSF_notop expected_n_atoms = 3341 @@ -152,6 +165,7 @@ def test_dihedrals_total_counts(self, top): def test_impropers_total_counts(self, top): assert len(top.impropers.values) == 0 + def test_psf_nosegid(): """Issue #121""" u = mda.Universe(PSF_nosegid) @@ -159,7 +173,8 @@ def test_psf_nosegid(): assert u.atoms.n_atoms == 98 assert_equal(u.segments.segids, ["SYSTEM"]) + def test_psf_inscode(): """Issue #2053 and #4189""" u = mda.Universe(PSF_inscode) - assert_equal(u.residues.resids[:3], [1, 1, 1]) \ No newline at end of file + assert_equal(u.residues.resids[:3], [1, 1, 1]) diff --git a/testsuite/MDAnalysisTests/topology/test_tables.py b/testsuite/MDAnalysisTests/topology/test_tables.py index 37246ad1864..6b19c715870 100644 --- a/testsuite/MDAnalysisTests/topology/test_tables.py +++ b/testsuite/MDAnalysisTests/topology/test_tables.py @@ -32,4 +32,3 @@ def test_moved_to_guessers_warning(): wmsg = "has been moved to MDAnalysis.guesser.tables" with pytest.warns(DeprecationWarning, match=wmsg): reload(tables) - diff --git a/testsuite/MDAnalysisTests/topology/test_top.py b/testsuite/MDAnalysisTests/topology/test_top.py index 3a8227227c1..a67c9283b77 100644 --- a/testsuite/MDAnalysisTests/topology/test_top.py +++ b/testsuite/MDAnalysisTests/topology/test_top.py @@ -20,37 +20,48 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -import sys import platform +import sys import warnings + import MDAnalysis as mda -import pytest import numpy as np +import pytest from numpy.testing import assert_equal -from MDAnalysisTests.topology.base import ParserBase + +from MDAnalysisTests.datafiles import PRM # ache.prmtop +from MDAnalysisTests.datafiles import PRM7 # tz2.truncoct.parm7.bz2 +from MDAnalysisTests.datafiles import PRM12 # anti.top +from MDAnalysisTests.datafiles import PRM_chainid_bz2 # multi_anche.prmtop.bz2 from MDAnalysisTests.datafiles import ( - PRM, # ache.prmtop - PRM_chainid_bz2, # multi_anche.prmtop.bz2 - PRM12, # anti.top - PRM7, # tz2.truncoct.parm7.bz2 - PRMpbc, + PRM19SBOPC, PRMNCRST, PRMNEGATIVE, + PRM_UreyBradley, PRMErr1, PRMErr2, PRMErr3, PRMErr4, PRMErr5, - PRM_UreyBradley, - PRM19SBOPC, + PRMpbc, ) +from MDAnalysisTests.topology.base import ParserBase class TOPBase(ParserBase): parser = mda.topology.TOPParser.TOPParser expected_attrs = [ - "names", "types", "type_indices", "charges", "masses", "resnames", - "bonds", "angles", "dihedrals", "impropers", "elements" + "names", + "types", + "type_indices", + "charges", + "masses", + "resnames", + "bonds", + "angles", + "dihedrals", + "impropers", + "elements", ] expected_n_segments = 1 @@ -79,14 +90,18 @@ def test_angles_atom_counts(self, filename): def test_dihedrals_atom_counts(self, filename): u = mda.Universe(filename) assert len(u.atoms[[0]].dihedrals) == self.expected_n_zero_dihedrals - assert len(u.atoms[[self.atom_i]].dihedrals) == \ - self.expected_n_i_dihedrals + assert ( + len(u.atoms[[self.atom_i]].dihedrals) + == self.expected_n_i_dihedrals + ) def test_impropers_atom_counts(self, filename): u = mda.Universe(filename) assert len(u.atoms[[0]].impropers) == self.expected_n_zero_impropers - assert len(u.atoms[[self.atom_i]].impropers) == \ - self.expected_n_i_impropers + assert ( + len(u.atoms[[self.atom_i]].impropers) + == self.expected_n_i_impropers + ) def test_bonds_identity(self, top): vals = top.bonds.values @@ -134,8 +149,12 @@ def test_improper_atoms_bonded(self, top): forward = ((imp[0], imp[2]), (imp[1], imp[2]), (imp[2], imp[3])) backward = ((imp[0], imp[1]), (imp[1], imp[2]), (imp[1], imp[3])) for a, b in zip(forward, backward): - assert ((b in vals) or (b[::-1] in vals) or - (a in vals) or (a[::-1] in vals)) + assert ( + (b in vals) + or (b[::-1] in vals) + or (a in vals) + or (a[::-1] in vals) + ) def test_elements(self, top): """Tests elements attribute. @@ -147,18 +166,29 @@ def test_elements(self, top): if self.expected_elems: for erange, evals in zip(self.elems_ranges, self.expected_elems): - assert_equal(top.elements.values[erange[0]:erange[1]], evals, - "unexpected element match") + assert_equal( + top.elements.values[erange[0] : erange[1]], + evals, + "unexpected element match", + ) else: - assert not hasattr(top, 'elements'), 'Unexpected elements attr' + assert not hasattr(top, "elements"), "Unexpected elements attr" class TestPRMParser(TOPBase): ref_filename = PRM # Does not contain an ATOMIC_NUMBER record, so no elements expected_attrs = [ - "names", "types", "type_indices", "charges", "masses", "resnames", - "bonds", "angles", "dihedrals", "impropers" + "names", + "types", + "type_indices", + "charges", + "masses", + "resnames", + "bonds", + "angles", + "dihedrals", + "impropers", ] expected_n_atoms = 252 expected_n_residues = 14 @@ -177,30 +207,68 @@ class TestPRMParser(TOPBase): expected_n_i_impropers = 4 atom_zero_bond_values = ((0, 4), (0, 1), (0, 2), (0, 3)) atom_i_bond_values = ((79, 80), (79, 83), (77, 79)) - atom_zero_angle_values = ((0, 4, 6), (0, 4, 10), (3, 0, 4), - (2, 0, 3), (2, 0, 4), (1, 0, 2), - (1, 0, 3), (1, 0, 4), (0, 4, 5)) - atom_i_angle_values = ((80, 79, 83), (77, 79, 80), (77, 79, 83), - (74, 77, 79), (79, 80, 81), (79, 80, 82), - (79, 83, 84), (79, 83, 85), (78, 77, 79)) - atom_zero_dihedral_values = ((0, 4, 10, 11), (0, 4, 10, 12), - (3, 0, 4, 5), (3, 0, 4, 6), - (3, 0, 4, 10), (2, 0, 4, 5), - (2, 0, 4, 6), (2, 0, 4, 10), - (1, 0, 4, 5), (1, 0, 4, 6), - (1, 0, 4, 10), (0, 4, 6, 7), - (0, 4, 6, 8), (0, 4, 6, 9)) - atom_i_dihedral_values = ((71, 74, 77, 79), (74, 77, 79, 80), - (74, 77, 79, 83), (75, 74, 77, 79), - (76, 74, 77, 79), (77, 79, 80, 81), - (77, 79, 80, 82), (77, 79, 83, 84), - (77, 79, 83, 85), (78, 77, 79, 80), - (78, 77, 79, 83), (80, 79, 83, 84), - (80, 79, 83, 85), (81, 80, 79, 83), - (82, 80, 79, 83)) + atom_zero_angle_values = ( + (0, 4, 6), + (0, 4, 10), + (3, 0, 4), + (2, 0, 3), + (2, 0, 4), + (1, 0, 2), + (1, 0, 3), + (1, 0, 4), + (0, 4, 5), + ) + atom_i_angle_values = ( + (80, 79, 83), + (77, 79, 80), + (77, 79, 83), + (74, 77, 79), + (79, 80, 81), + (79, 80, 82), + (79, 83, 84), + (79, 83, 85), + (78, 77, 79), + ) + atom_zero_dihedral_values = ( + (0, 4, 10, 11), + (0, 4, 10, 12), + (3, 0, 4, 5), + (3, 0, 4, 6), + (3, 0, 4, 10), + (2, 0, 4, 5), + (2, 0, 4, 6), + (2, 0, 4, 10), + (1, 0, 4, 5), + (1, 0, 4, 6), + (1, 0, 4, 10), + (0, 4, 6, 7), + (0, 4, 6, 8), + (0, 4, 6, 9), + ) + atom_i_dihedral_values = ( + (71, 74, 77, 79), + (74, 77, 79, 80), + (74, 77, 79, 83), + (75, 74, 77, 79), + (76, 74, 77, 79), + (77, 79, 80, 81), + (77, 79, 80, 82), + (77, 79, 83, 84), + (77, 79, 83, 85), + (78, 77, 79, 80), + (78, 77, 79, 83), + (80, 79, 83, 84), + (80, 79, 83, 85), + (81, 80, 79, 83), + (82, 80, 79, 83), + ) atom_zero_improper_values = () - atom_i_improper_values = ((74, 79, 77, 78), (77, 80, 79, 83), - (79, 81, 80, 82), (79, 84, 83, 85)) + atom_i_improper_values = ( + (74, 79, 77, 78), + (77, 80, 79, 83), + (79, 81, 80, 82), + (79, 84, 83, 85), + ) expected_elems = None @@ -305,76 +373,25 @@ class TestPRMChainidParser(TOPBase): expected_elems = [ np.array( - [ - "N", - "H", - "H", - "H", - "C", - "H", - "C", - "H", - "H", - ], + ["N", "H", "H", "H", "C", "H", "C", "H", "H"], dtype=object, ), np.array( - [ - "O", - "O", - "N", - "H", - "H", - "H", - "C", - ], + ["O", "O", "N", "H", "H", "H", "C"], dtype=object, ), np.array(["H", "C", "O", "O", "N", "H", "H", "H"], dtype=object), ] + # fmt: off expected_chainIDs = np.array( [ - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "A", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "B", - "C", - "C", - "C", - "C", - "C", - "C", - "C", - "C", - "C", - "C", + "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", + "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", + "C", "C", "C", "C", "C", "C", "C", "C", "C", "C", ] ) + # fmt: on def test_chainIDs(self, filename): """Tests chainIDs attribute. @@ -386,7 +403,9 @@ def test_chainIDs(self, filename): u = mda.Universe(filename) if hasattr(self, "expected_chainIDs"): - reschainIDs = [atomchainIDs[0] for atomchainIDs in u.residues.chainIDs] + reschainIDs = [ + atomchainIDs[0] for atomchainIDs in u.residues.chainIDs + ] assert_equal( reschainIDs, self.expected_chainIDs, "unexpected element match" ) @@ -413,54 +432,94 @@ class TestPRM12Parser(TOPBase): atom_i = 335 ref_proteinatoms = 0 atom_zero_bond_values = ((0, 1),) - atom_i_bond_values = ((335, 337), (335, 354), - (334, 335), (335, 336)) + atom_i_bond_values = ((335, 337), (335, 354), (334, 335), (335, 336)) atom_zero_angle_values = ((0, 1, 2),) - atom_i_angle_values = ((337, 335, 354), (335, 337, 338), - (335, 337, 351), (335, 354, 352), - (334, 335, 337), (334, 335, 354), - (332, 334, 335), (336, 335, 337), - (336, 335, 354), (335, 354, 355), - (335, 354, 356), (334, 335, 336)) - atom_zero_dihedral_values = ((0, 1, 2, 3), (0, 1, 2, 4), - (0, 1, 2, 5)) - atom_i_dihedral_values = ((329, 332, 334, 335), (332, 334, 335, 336), - (332, 334, 335, 337), (332, 334, 335, 354), - (332, 352, 354, 335), (333, 332, 334, 335), - (334, 335, 337, 338), (334, 335, 337, 351), - (334, 335, 354, 352), (334, 335, 354, 355), - (334, 335, 354, 356), (335, 334, 332, 352), - (335, 337, 338, 339), (335, 337, 338, 340), - (335, 337, 351, 341), (335, 337, 351, 350), - (335, 354, 352, 353), (335, 354, 352, 357), - (336, 335, 337, 338), (336, 335, 337, 351), - (336, 335, 354, 352), (336, 335, 354, 355), - (336, 335, 354, 356), (337, 335, 354, 352), - (337, 335, 354, 355), (337, 335, 354, 356), - (338, 337, 335, 354), (351, 337, 335, 354)) + atom_i_angle_values = ( + (337, 335, 354), + (335, 337, 338), + (335, 337, 351), + (335, 354, 352), + (334, 335, 337), + (334, 335, 354), + (332, 334, 335), + (336, 335, 337), + (336, 335, 354), + (335, 354, 355), + (335, 354, 356), + (334, 335, 336), + ) + atom_zero_dihedral_values = ((0, 1, 2, 3), (0, 1, 2, 4), (0, 1, 2, 5)) + atom_i_dihedral_values = ( + (329, 332, 334, 335), + (332, 334, 335, 336), + (332, 334, 335, 337), + (332, 334, 335, 354), + (332, 352, 354, 335), + (333, 332, 334, 335), + (334, 335, 337, 338), + (334, 335, 337, 351), + (334, 335, 354, 352), + (334, 335, 354, 355), + (334, 335, 354, 356), + (335, 334, 332, 352), + (335, 337, 338, 339), + (335, 337, 338, 340), + (335, 337, 351, 341), + (335, 337, 351, 350), + (335, 354, 352, 353), + (335, 354, 352, 357), + (336, 335, 337, 338), + (336, 335, 337, 351), + (336, 335, 354, 352), + (336, 335, 354, 355), + (336, 335, 354, 356), + (337, 335, 354, 352), + (337, 335, 354, 355), + (337, 335, 354, 356), + (338, 337, 335, 354), + (351, 337, 335, 354), + ) atom_zero_improper_values = () atom_i_improper_values = ((335, 337, 338, 351),) elems_ranges = [[0, 36], [351, 403]] - expected_elems = [np.array(["H", "O", "C", "H", "H", "C", "H", "O", "C", - "H", "N", "C", "H", "N", "C", "C", "O", "N", - "H", "C", "N", "H", "H", "N", "C", "C", "H", - "C", "H", "H", "O", "P", "O", "O", "O", "C"], - dtype=object), - np.array(["C", "C", "H", "C", "H", "H", "O", "P", "O", - "O", "O", "C", "H", "H", "C", "H", "O", "C", - "H", "N", "C", "H", "N", "C", "C", "O", "N", - "H", "C", "N", "H", "H", "N", "C", "C", "H", - "C", "H", "H", "O", "H", "Na", "Na", "Na", - "Na", "Na", "Na", "Na", "Na", "O", "H", "H"], - dtype=object)] + # fmt: off + expected_elems = [ + np.array( + [ + "H", "O", "C", "H", "H", "C", "H", "O", "C", "H", "N", "C", + "H", "N", "C", "C", "O", "N", "H", "C", "N", "H", "H", "N", + "C", "C", "H", "C", "H", "H", "O", "P", "O", "O", "O","C", + ], + dtype=object, + ), + np.array( + [ + "C", "C", "H", "C", "H", "H", "O", "P", "O", "O", "O", "C", + "H", "H", "C", "H", "O", "C", "H", "N", "C", "H", "N", "C", + "C", "O", "N", "H", "C", "N", "H", "H", "N", "C", "C", "H", + "C", "H", "H", "O", "H", "Na", "Na", "Na", "Na", "Na", "Na", + "Na", "Na", "O", "H", "H", + ], + dtype=object, + ), + ] + # fmt: on class TestParm7Parser(TOPBase): ref_filename = PRM7 # Does not contain an ATOMIC_NUMBER record, so no elements expected_attrs = [ - "names", "types", "type_indices", "charges", "masses", "resnames", - "bonds", "angles", "dihedrals", "impropers" + "names", + "types", + "type_indices", + "charges", + "masses", + "resnames", + "bonds", + "angles", + "dihedrals", + "impropers", ] expected_n_atoms = 5827 expected_n_residues = 1882 @@ -478,39 +537,78 @@ class TestParm7Parser(TOPBase): expected_n_zero_impropers = 0 expected_n_i_impropers = 2 atom_zero_bond_values = ((0, 4), (0, 1), (0, 2), (0, 3)) - atom_i_bond_values = ((135, 137), (135, 155), (133, 135), - (135, 136)) - atom_zero_angle_values = ((0, 4, 6), (0, 4, 11), (3, 0, 4), - (2, 0, 3), (2, 0, 4), (1, 0, 2), - (1, 0, 3), (1, 0, 4), (0, 4, 5)) - atom_i_angle_values = ((131, 133, 135), (137, 135, 155), - (135, 137, 140), (135, 155, 156), - (135, 155, 157), (133, 135, 137), - (133, 135, 155), (136, 135, 137), - (136, 135, 155), (135, 137, 138), - (135, 137, 139), (134, 133, 135), - (133, 135, 136)) - atom_zero_dihedral_values = ((0, 4, 6, 7), (0, 4, 6, 8), - (0, 4, 6, 9), (0, 4, 11, 12), - (0, 4, 11, 13), (1, 0, 4, 5), - (1, 0, 4, 6), (1, 0, 4, 11), - (2, 0, 4, 5), (2, 0, 4, 6), - (2, 0, 4, 11), (3, 0, 4, 5), - (3, 0, 4, 6), (3, 0, 4, 11)) - atom_i_dihedral_values = ((113, 131, 133, 135), (131, 133, 135, 136), - (131, 133, 135, 137), (131, 133, 135, 155), - (132, 131, 133, 135), (133, 135, 137, 138), - (133, 135, 137, 139), (133, 135, 137, 140), - (133, 135, 155, 156), (133, 135, 155, 157), - (134, 133, 135, 136), (134, 133, 135, 137), - (134, 133, 135, 155), (135, 137, 140, 141), - (135, 137, 140, 154), (135, 155, 157, 158), - (135, 155, 157, 159), (136, 135, 137, 138), - (136, 135, 137, 139), (136, 135, 137, 140), - (136, 135, 155, 156), (136, 135, 155, 157), - (137, 135, 155, 156), (137, 135, 155, 157), - (138, 137, 135, 155), (139, 137, 135, 155), - (140, 137, 135, 155)) + atom_i_bond_values = ((135, 137), (135, 155), (133, 135), (135, 136)) + atom_zero_angle_values = ( + (0, 4, 6), + (0, 4, 11), + (3, 0, 4), + (2, 0, 3), + (2, 0, 4), + (1, 0, 2), + (1, 0, 3), + (1, 0, 4), + (0, 4, 5), + ) + atom_i_angle_values = ( + (131, 133, 135), + (137, 135, 155), + (135, 137, 140), + (135, 155, 156), + (135, 155, 157), + (133, 135, 137), + (133, 135, 155), + (136, 135, 137), + (136, 135, 155), + (135, 137, 138), + (135, 137, 139), + (134, 133, 135), + (133, 135, 136), + ) + atom_zero_dihedral_values = ( + (0, 4, 6, 7), + (0, 4, 6, 8), + (0, 4, 6, 9), + (0, 4, 11, 12), + (0, 4, 11, 13), + (1, 0, 4, 5), + (1, 0, 4, 6), + (1, 0, 4, 11), + (2, 0, 4, 5), + (2, 0, 4, 6), + (2, 0, 4, 11), + (3, 0, 4, 5), + (3, 0, 4, 6), + (3, 0, 4, 11), + ) + atom_i_dihedral_values = ( + (113, 131, 133, 135), + (131, 133, 135, 136), + (131, 133, 135, 137), + (131, 133, 135, 155), + (132, 131, 133, 135), + (133, 135, 137, 138), + (133, 135, 137, 139), + (133, 135, 137, 140), + (133, 135, 155, 156), + (133, 135, 155, 157), + (134, 133, 135, 136), + (134, 133, 135, 137), + (134, 133, 135, 155), + (135, 137, 140, 141), + (135, 137, 140, 154), + (135, 155, 157, 158), + (135, 155, 157, 159), + (136, 135, 137, 138), + (136, 135, 137, 139), + (136, 135, 137, 140), + (136, 135, 155, 156), + (136, 135, 155, 157), + (137, 135, 155, 156), + (137, 135, 155, 157), + (138, 137, 135, 155), + (139, 137, 135, 155), + (140, 137, 135, 155), + ) atom_zero_improper_values = () atom_i_improper_values = ((131, 135, 133, 134), (135, 157, 155, 156)) expected_elems = None @@ -520,8 +618,16 @@ class TestPRM2(TOPBase): ref_filename = PRMpbc # Does not contain an ATOMIC_NUMBER record, so no elements expected_attrs = [ - "names", "types", "type_indices", "charges", "masses", "resnames", - "bonds", "angles", "dihedrals", "impropers" + "names", + "types", + "type_indices", + "charges", + "masses", + "resnames", + "bonds", + "angles", + "dihedrals", + "impropers", ] expected_n_atoms = 5071 expected_n_residues = 1686 @@ -542,19 +648,37 @@ class TestPRM2(TOPBase): atom_zero_bond_values = ((0, 1),) atom_i_bond_values = ((14, 15), (14, 16), (8, 14)) atom_zero_angle_values = ((0, 1, 2), (0, 1, 3), (0, 1, 4)) - atom_i_angle_values = ((15, 14, 16), (14, 16, 18), (10, 8, 14), - (8, 14, 15), (8, 14, 16), (6, 8, 14), - (14, 16, 17), (9, 8, 14)) + atom_i_angle_values = ( + (15, 14, 16), + (14, 16, 18), + (10, 8, 14), + (8, 14, 15), + (8, 14, 16), + (6, 8, 14), + (14, 16, 17), + (9, 8, 14), + ) atom_zero_dihedral_values = ((0, 1, 4, 5), (0, 1, 4, 6)) - atom_i_dihedral_values = ((4, 6, 8, 14), (6, 8, 14, 15), - (6, 8, 14, 16), (7, 6, 8, 14), - (8, 14, 16, 17), (8, 14, 16, 18), - (9, 8, 14, 15), (9, 8, 14, 16), - (10, 8, 14, 15), (10, 8, 14, 16), - (11, 10, 8, 14), (12, 10, 8, 14), - (13, 10, 8, 14), (14, 16, 18, 19), - (14, 16, 18, 20), (14, 16, 18, 21), - (15, 14, 16, 17), (15, 14, 16, 18)) + atom_i_dihedral_values = ( + (4, 6, 8, 14), + (6, 8, 14, 15), + (6, 8, 14, 16), + (7, 6, 8, 14), + (8, 14, 16, 17), + (8, 14, 16, 18), + (9, 8, 14, 15), + (9, 8, 14, 16), + (10, 8, 14, 15), + (10, 8, 14, 16), + (11, 10, 8, 14), + (12, 10, 8, 14), + (13, 10, 8, 14), + (14, 16, 18, 19), + (14, 16, 18, 20), + (14, 16, 18, 21), + (15, 14, 16, 17), + (15, 14, 16, 18), + ) atom_zero_improper_values = () atom_i_improper_values = ((8, 16, 14, 15), (14, 18, 16, 17)) expected_elems = None @@ -587,8 +711,12 @@ class TestPRMNCRST(TOPBase): atom_i_dihedral_values = ((0, 1, 4, 5), (2, 1, 4, 5), (3, 1, 4, 5)) atom_zero_improper_values = () atom_i_improper_values = () - elems_ranges = [[0, 6], ] - expected_elems = [np.array(["H", "C", "H", "H", "C", "O"], dtype=object), ] + elems_ranges = [ + [0, 6], + ] + expected_elems = [ + np.array(["H", "C", "H", "H", "C", "O"], dtype=object), + ] class TestPRMNCRST_negative(TOPBase): @@ -618,8 +746,12 @@ class TestPRMNCRST_negative(TOPBase): atom_i_dihedral_values = ((0, 1, 4, 5), (2, 1, 4, 5), (3, 1, 4, 5)) atom_zero_improper_values = () atom_i_improper_values = () - elems_ranges = [[0, 6], ] - expected_elems = [np.array(["H", "", "H", "H", "C", ""], dtype=object), ] + elems_ranges = [ + [0, 6], + ] + expected_elems = [ + np.array(["H", "", "H", "H", "C", ""], dtype=object), + ] class TestPRMEP(TOPBase): @@ -650,17 +782,15 @@ class TestPRMEP(TOPBase): atom_zero_improper_values = () atom_i_improper_values = () elems_ranges = [[0, 8], [20, 28]] - expected_elems = [np.array(["H", "C", "H", "H", "C", "O", "N", "H"], - dtype=object), - np.array(["H", "H", "O", "H", "H", "", "O", "H"], - dtype=object)] + expected_elems = [ + np.array(["H", "C", "H", "H", "C", "O", "N", "H"], dtype=object), + np.array(["H", "H", "O", "H", "H", "", "O", "H"], dtype=object), + ] class TestErrorsAndWarnings(object): - ATOMIC_NUMBER_MSG = ( - "ATOMIC_NUMBER record not found, elements attribute will not be populated" - ) + ATOMIC_NUMBER_MSG = "ATOMIC_NUMBER record not found, elements attribute will not be populated" MISSING_ELEM_MSG = ( "Unknown ATOMIC_NUMBER value found for some atoms, " "these have been given an empty element record" diff --git a/testsuite/MDAnalysisTests/topology/test_topology_base.py b/testsuite/MDAnalysisTests/topology/test_topology_base.py index 0a6099b17de..33d03258f96 100644 --- a/testsuite/MDAnalysisTests/topology/test_topology_base.py +++ b/testsuite/MDAnalysisTests/topology/test_topology_base.py @@ -6,16 +6,18 @@ class TestSquash(object): atom_resids = np.array([2, 2, 1, 1, 5, 5, 4, 4]) - atom_resnames = np.array(['A', 'A', 'B', 'B', 'C', 'C', 'D', 'D'], - dtype=object) + atom_resnames = np.array( + ["A", "A", "B", "B", "C", "C", "D", "D"], dtype=object + ) def test_squash(self): - atom_residx, resids, (resnames, ) = squash_by(self.atom_resids, - self.atom_resnames) + atom_residx, resids, (resnames,) = squash_by( + self.atom_resids, self.atom_resnames + ) assert_equal(atom_residx, np.array([1, 1, 0, 0, 3, 3, 2, 2])) assert_equal(resids, np.array([1, 2, 4, 5])) - assert_equal(resnames, np.array(['B', 'A', 'D', 'C'])) + assert_equal(resnames, np.array(["B", "A", "D", "C"])) class TestChangeSquash(object): @@ -24,21 +26,22 @@ def test_resid_squash(self): # Residues 1 & 2 are Segid A, Residue 3 is Segid B # Resid 2 is repeated twice! Should be detected as 2 distinct residues resids = np.array([2, 2, 3, 3, 2, 2]) - resnames = np.array(['RsA', 'RsA', 'RsB', 'RsB', 'RsC', 'RsC']) - segids = np.array(['A', 'A', 'A', 'A', 'B', 'B']) + resnames = np.array(["RsA", "RsA", "RsB", "RsB", "RsC", "RsC"]) + segids = np.array(["A", "A", "A", "A", "B", "B"]) residx, (new_resids, new_resnames, new_segids) = change_squash( - (resids, ), (resids, resnames, segids)) + (resids,), (resids, resnames, segids) + ) assert_equal(residx, np.array([0, 0, 1, 1, 2, 2])) assert_equal(new_resids, np.array([2, 3, 2])) - assert_equal(new_resnames, np.array(['RsA', 'RsB', 'RsC'])) - assert_equal(new_segids, np.array(['A', 'A', 'B'])) + assert_equal(new_resnames, np.array(["RsA", "RsB", "RsC"])) + assert_equal(new_segids, np.array(["A", "A", "B"])) def test_segid_squash(self): - segids = np.array(['A', 'A', 'B']) + segids = np.array(["A", "A", "B"]) - segidx, (new_segids, ) = change_squash((segids, ), (segids, )) + segidx, (new_segids,) = change_squash((segids,), (segids,)) assert_equal(segidx, np.array([0, 0, 1])) - assert_equal(new_segids, np.array(['A', 'B'])) + assert_equal(new_segids, np.array(["A", "B"])) diff --git a/testsuite/MDAnalysisTests/topology/test_topology_str_types.py b/testsuite/MDAnalysisTests/topology/test_topology_str_types.py index 66bf89b3e09..2827dc4eec8 100644 --- a/testsuite/MDAnalysisTests/topology/test_topology_str_types.py +++ b/testsuite/MDAnalysisTests/topology/test_topology_str_types.py @@ -43,40 +43,47 @@ HoomdXMLdata, XPDB_small, XYZ_mini, - DLP_HISTORY_minimal, ) + DLP_HISTORY_minimal, +) -@pytest.mark.parametrize('prop', [ - 'name', - 'resname', - 'type', - 'segid', - 'moltype', -]) +@pytest.mark.parametrize( + "prop", + [ + "name", + "resname", + "type", + "segid", + "moltype", + ], +) # topology formats curated from values available in # MDAnalysis._PARSERS -@pytest.mark.parametrize( 'top_format, top', [ - ('CONFIG', DLP_CONFIG_minimal), - ('CRD', CRD), - ('DATA', LAMMPSdata), - ('DMS', DMS), - ('GMS', GMS_SYMOPT), - ('GRO', GRO), - ('HISTORY', DLP_HISTORY_minimal), - ('MMTF', MMTF), - ('MOL2', mol2_molecule), - ('PARM7', PRM7), - ('PDB', PDB_small), - ('PDBQT', PDBQT_input), - ('PQR', PQR), - ('PRMTOP', PRM), - ('PSF', PSF), - ('TOP', PRM12), - ('TPR', TPR), - ('XML', HoomdXMLdata), - ('XPDB', XPDB_small), - ('XYZ', XYZ_mini) -]) +@pytest.mark.parametrize( + "top_format, top", + [ + ("CONFIG", DLP_CONFIG_minimal), + ("CRD", CRD), + ("DATA", LAMMPSdata), + ("DMS", DMS), + ("GMS", GMS_SYMOPT), + ("GRO", GRO), + ("HISTORY", DLP_HISTORY_minimal), + ("MMTF", MMTF), + ("MOL2", mol2_molecule), + ("PARM7", PRM7), + ("PDB", PDB_small), + ("PDBQT", PDBQT_input), + ("PQR", PQR), + ("PRMTOP", PRM), + ("PSF", PSF), + ("TOP", PRM12), + ("TPR", TPR), + ("XML", HoomdXMLdata), + ("XPDB", XPDB_small), + ("XYZ", XYZ_mini), + ], +) def test_str_types(top_format, top, prop): # Python 2/3 topology string type checking # Related to Issue #1336 diff --git a/testsuite/MDAnalysisTests/topology/test_tprparser.py b/testsuite/MDAnalysisTests/topology/test_tprparser.py index 208769bd61d..f2b93e0fcbb 100644 --- a/testsuite/MDAnalysisTests/topology/test_tprparser.py +++ b/testsuite/MDAnalysisTests/topology/test_tprparser.py @@ -20,32 +20,38 @@ # 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 functools +import MDAnalysis as mda +import MDAnalysis.topology.TPRParser import numpy as np +import pytest +# fmt: off +from MDAnalysis.tests.datafiles import (TPR, TPR400, TPR402, TPR403, TPR404, + TPR405, TPR406, TPR407, TPR450, TPR451, + TPR452, TPR453, TPR454, TPR455, TPR460, + TPR461, TPR502, TPR504, TPR505, TPR510, + TPR2016, TPR2018, TPR2019B3, TPR2020, + TPR2020B2, TPR2021, TPR2022RC1, + TPR2023, TPR2024, TPR2024_4, + TPR_EXTRA_407, TPR_EXTRA_2016, + TPR_EXTRA_2018, TPR_EXTRA_2020, + TPR_EXTRA_2021, TPR_EXTRA_2022RC1, + TPR_EXTRA_2023, TPR_EXTRA_2024, + TPR_EXTRA_2024_4, XTC, TPR334_bonded, + TPR455Double, TPR510_bonded, + TPR2016_bonded, TPR2018_bonded, + TPR2019B3_bonded, TPR2020_bonded, + TPR2020_double_bonded, + TPR2020B2_bonded, TPR2020Double, + TPR2021_bonded, TPR2021_double_bonded, + TPR2021Double, TPR2022RC1_bonded, + TPR2023_bonded, TPR2024_4_bonded, + TPR2024_bonded) +from numpy.testing import assert_equal -from MDAnalysis.tests.datafiles import ( - TPR, - TPR400, TPR402, TPR403, TPR404, TPR405, TPR406, TPR407, - TPR450, TPR451, TPR452, TPR453, TPR454, TPR455, TPR455Double, - TPR460, TPR461, TPR502, TPR504, TPR505, TPR510, TPR510_bonded, - TPR2016, TPR2018, TPR2019B3, TPR2020B2, TPR2020, TPR2020Double, - TPR2021, TPR2021Double, TPR2022RC1, TPR2023, TPR2024, TPR2024_4, - TPR2016_bonded, TPR2018_bonded, TPR2019B3_bonded, - TPR2020B2_bonded, TPR2020_bonded, TPR2020_double_bonded, - TPR2021_bonded, TPR2021_double_bonded, TPR334_bonded, - TPR2022RC1_bonded, TPR2023_bonded, TPR2024_bonded, - TPR2024_4_bonded, - TPR_EXTRA_2021, TPR_EXTRA_2020, TPR_EXTRA_2018, - TPR_EXTRA_2016, TPR_EXTRA_407, TPR_EXTRA_2022RC1, - TPR_EXTRA_2023, TPR_EXTRA_2024, TPR_EXTRA_2024_4, - XTC, -) +# fmt: on from MDAnalysisTests.topology.base import ParserBase -import MDAnalysis.topology.TPRParser -import MDAnalysis as mda BONDED_TPRS = ( TPR510_bonded, @@ -103,18 +109,22 @@ def test_molnums(self, top): def test_chainIDs(self, top): if hasattr(self, "ref_chainIDs"): - assert_equal(self.ref_chainIDs, getattr(top, "chainIDs").name_lookup) + assert_equal( + self.ref_chainIDs, getattr(top, "chainIDs").name_lookup + ) class TestTPR(TPRAttrs): """ this test the data/adk_oplsaa.tpr which is of tpx version 58 """ + expected_n_atoms = 47681 expected_n_residues = 11302 expected_n_segments = 3 - ref_moltypes = np.array(['AKeco'] * 214 + ['SOL'] * 11084 + ['NA+'] * 4, - dtype=object) + ref_moltypes = np.array( + ["AKeco"] * 214 + ["SOL"] * 11084 + ["NA+"] * 4, dtype=object + ) ref_molnums = np.array([0] * 214 + list(range(1, 1 + 11084 + 4))) @pytest.fixture() @@ -129,16 +139,19 @@ class TestTPRGromacsVersions(TPRAttrs): expected_n_atoms = 2263 expected_n_residues = 230 expected_n_segments = 2 - ref_moltypes = np.array(['Protein_A'] * 129 + ['SOL'] * 101, dtype=object) + ref_moltypes = np.array(["Protein_A"] * 129 + ["SOL"] * 101, dtype=object) ref_molnums = np.array([0] * 129 + list(range(1, 1 + 101))) ref_chainIDs = ["Protein_A", "SOL"] - @pytest.fixture(params=[TPR400, TPR402, TPR403, TPR404, TPR405, TPR406, - TPR407, TPR450, TPR451, TPR452, TPR453, TPR454, - TPR455, TPR502, TPR504, TPR505, TPR510, TPR2016, - TPR2018, TPR2019B3, TPR2020, TPR2020Double, - TPR2021, TPR2021Double, TPR2022RC1, TPR2023, - TPR2024, TPR2024_4]) + # fmt: off + @pytest.fixture(params=[ + TPR400, TPR402, TPR403, TPR404, TPR405, TPR406, TPR407, TPR450, + TPR451, TPR452, TPR453, TPR454, TPR455, TPR502, TPR504, TPR505, + TPR510, TPR2016, TPR2018, TPR2019B3, TPR2020, TPR2020Double, + TPR2021, TPR2021Double, TPR2022RC1, TPR2023, TPR2024, TPR2024_4 + ] + ) + # fmt: on def filename(self, request): return request.param @@ -147,10 +160,16 @@ class TestTPRDouble(TPRAttrs): expected_n_atoms = 21692 expected_n_residues = 4352 expected_n_segments = 7 - ref_moltypes = np.array(['DOPC'] * 21 + ['DPPC'] * 10 + ['CHOL'] * 3 - + ['DOPC'] * 21 + ['DPPC'] * 10 + ['CHOL'] * 3 - + ['SOL'] * 4284, - dtype=object) + ref_moltypes = np.array( + ["DOPC"] * 21 + + ["DPPC"] * 10 + + ["CHOL"] * 3 + + ["DOPC"] * 21 + + ["DPPC"] * 10 + + ["CHOL"] * 3 + + ["SOL"] * 4284, + dtype=object, + ) ref_molnums = np.arange(4352) @pytest.fixture() @@ -162,13 +181,25 @@ class TestTPR46x(TPRAttrs): expected_n_atoms = 44052 expected_n_residues = 10712 expected_n_segments = 8 - ref_moltypes = np.array(['Protein_A'] * 27 + ['Protein_B'] * 27 - + ['Protein_C'] * 27 + ['Protein_D'] * 27 - + ['Protein_E'] * 27 - + ['SOL'] * 10530 + ['NA+'] * 26 + ['CL-'] * 21, - dtype=object) - ref_molnums = np.array([0] * 27 + [1] * 27 + [2] * 27 + [3] * 27 + [4] * 27 - + list(range(5, 5 + 10530 + 26 + 21))) + ref_moltypes = np.array( + ["Protein_A"] * 27 + + ["Protein_B"] * 27 + + ["Protein_C"] * 27 + + ["Protein_D"] * 27 + + ["Protein_E"] * 27 + + ["SOL"] * 10530 + + ["NA+"] * 26 + + ["CL-"] * 21, + dtype=object, + ) + ref_molnums = np.array( + [0] * 27 + + [1] * 27 + + [2] * 27 + + [3] * 27 + + [4] * 27 + + list(range(5, 5 + 10530 + 26 + 21)) + ) @pytest.fixture(params=[TPR460, TPR461]) def filename(self, request): @@ -180,7 +211,11 @@ def _test_is_in_topology(name, elements, topology_path, topology_section): Test if an interaction appears as expected in the topology """ post_40_potentials = { - 'RESTRAINTPOT', 'RESTRANGLES', 'RESTRDIHS', 'CBTDIHS', 'PIDIHS', + "RESTRAINTPOT", + "RESTRANGLES", + "RESTRDIHS", + "CBTDIHS", + "PIDIHS", } if name in post_40_potentials and topology_path == TPR_EXTRA_407: # The potential is not yet implemented in this version of gromacs @@ -188,85 +223,102 @@ def _test_is_in_topology(name, elements, topology_path, topology_section): parser = MDAnalysis.topology.TPRParser.TPRParser(topology_path) top = parser.parse() for element in elements: - assert element in getattr(top, topology_section).values, \ - 'Interaction type "{}" not found'.format(name) - - -@pytest.mark.parametrize('topology', BONDED_TPRS) -@pytest.mark.parametrize('bond', ( - ('BONDS', [(0, 1)]), - ('G96BONDS', [(1, 2)]), - ('MORSE', [(2, 3)]), - ('CUBICBONDS', [(3, 4)]), - ('CONNBONDS', [(4, 5)]), - ('HARMONIC', [(5, 6)]), - ('FENEBONDS', [(6, 7)]), - ('RESTRAINTPOT', [(7, 8)]), - ('TABBONDS', [(8, 9)]), - ('TABBONDSNC', [(9, 10)]), - ('CONSTR', [(10, 11)]), - ('CONSTRNC', [(11, 12)]), -)) + assert ( + element in getattr(top, topology_section).values + ), 'Interaction type "{}" not found'.format(name) + + +@pytest.mark.parametrize("topology", BONDED_TPRS) +@pytest.mark.parametrize( + "bond", + ( + ("BONDS", [(0, 1)]), + ("G96BONDS", [(1, 2)]), + ("MORSE", [(2, 3)]), + ("CUBICBONDS", [(3, 4)]), + ("CONNBONDS", [(4, 5)]), + ("HARMONIC", [(5, 6)]), + ("FENEBONDS", [(6, 7)]), + ("RESTRAINTPOT", [(7, 8)]), + ("TABBONDS", [(8, 9)]), + ("TABBONDSNC", [(9, 10)]), + ("CONSTR", [(10, 11)]), + ("CONSTRNC", [(11, 12)]), + ), +) def test_all_bonds(topology, bond): """Test that all bond types are parsed as expected""" - bond_type_in_topology = functools.partial(_test_is_in_topology, - topology_section='bonds') + bond_type_in_topology = functools.partial( + _test_is_in_topology, topology_section="bonds" + ) bond_type, elements = bond bond_type_in_topology(bond_type, elements, topology) -@pytest.mark.parametrize('topology', BONDED_TPRS) -@pytest.mark.parametrize('angle', ( - ('ANGLES', [(0, 1, 2)]), - ('G96ANGLES', [(1, 2, 3)]), - ('CROSS_BOND_BOND', [(2, 3, 4)]), - ('CROSS_BOND_ANGLE', [(3, 4, 5)]), - ('UREY_BRADLEY', [(4, 5, 6)]), - ('QANGLES', [(5, 6, 7)]), - ('RESTRANGLES', [(6, 7, 8)]), - ('TABANGLES', [(7, 8, 9)]), -)) +@pytest.mark.parametrize("topology", BONDED_TPRS) +@pytest.mark.parametrize( + "angle", + ( + ("ANGLES", [(0, 1, 2)]), + ("G96ANGLES", [(1, 2, 3)]), + ("CROSS_BOND_BOND", [(2, 3, 4)]), + ("CROSS_BOND_ANGLE", [(3, 4, 5)]), + ("UREY_BRADLEY", [(4, 5, 6)]), + ("QANGLES", [(5, 6, 7)]), + ("RESTRANGLES", [(6, 7, 8)]), + ("TABANGLES", [(7, 8, 9)]), + ), +) def test_all_angles(topology, angle): - angle_type_in_topology = functools.partial(_test_is_in_topology, - topology_section='angles') + angle_type_in_topology = functools.partial( + _test_is_in_topology, topology_section="angles" + ) angle_type, elements = angle angle_type_in_topology(angle_type, elements, topology) -@pytest.mark.parametrize('topology', BONDED_TPRS) -@pytest.mark.parametrize('dih', ( - ('PDIHS', [(0, 1, 2, 3), (1, 2, 3, 4), (7, 8, 9, 10)]), - ('RBDIHS', [(4, 5, 6, 7)]), - ('RESTRDIHS', [(8, 9, 10, 11)]), - ('CBTDIHS', [(9, 10, 11, 12)]), - ('FOURDIHS', [(6, 7, 8, 9)]), - ('TABDIHS', [(10, 11, 12, 13)]), -)) +@pytest.mark.parametrize("topology", BONDED_TPRS) +@pytest.mark.parametrize( + "dih", + ( + ("PDIHS", [(0, 1, 2, 3), (1, 2, 3, 4), (7, 8, 9, 10)]), + ("RBDIHS", [(4, 5, 6, 7)]), + ("RESTRDIHS", [(8, 9, 10, 11)]), + ("CBTDIHS", [(9, 10, 11, 12)]), + ("FOURDIHS", [(6, 7, 8, 9)]), + ("TABDIHS", [(10, 11, 12, 13)]), + ), +) def test_all_dihedrals(topology, dih): - dih_type_in_topology = functools.partial(_test_is_in_topology, - topology_section='dihedrals') + dih_type_in_topology = functools.partial( + _test_is_in_topology, topology_section="dihedrals" + ) dih_type, elements = dih dih_type_in_topology(dih_type, elements, topology) -@pytest.mark.parametrize('topology', BONDED_TPRS) -@pytest.mark.parametrize('impr', ( - ('IDIHS', [(2, 3, 4, 5), (3, 4, 5, 6)]), - ('PIDIHS', [(5, 6, 7, 8)]) -)) +@pytest.mark.parametrize("topology", BONDED_TPRS) +@pytest.mark.parametrize( + "impr", + (("IDIHS", [(2, 3, 4, 5), (3, 4, 5, 6)]), ("PIDIHS", [(5, 6, 7, 8)])), +) def test_all_impropers(topology, impr): - impr_type_in_topology = functools.partial(_test_is_in_topology, - topology_section='impropers') + impr_type_in_topology = functools.partial( + _test_is_in_topology, topology_section="impropers" + ) impr_type, elements = impr impr_type_in_topology(impr_type, elements, topology) +# fmt: off @pytest.fixture(params=( - TPR400, TPR402, TPR403, TPR404, TPR405, TPR406, TPR407, TPR450, TPR451, - TPR452, TPR453, TPR454, TPR502, TPR504, TPR505, TPR510, TPR2016, TPR2018, - TPR2023, TPR2024, TPR2024_4, -)) + TPR400, TPR402, TPR403, TPR404, TPR405, TPR406, TPR407, TPR450, + TPR451, TPR452, TPR453, TPR454, TPR502, TPR504, TPR505, TPR510, + TPR2016, TPR2018, TPR2023, TPR2024, TPR2024_4, + ) +) +# fmt: on def bonds_water(request): parser = MDAnalysis.topology.TPRParser.TPRParser(request.param).parse() # The index of the first water atom is 1960 @@ -286,19 +338,22 @@ def test_settle(bonds_water): assert bonds_water[-1][1] == 2262 -@pytest.mark.parametrize('tpr_path, expected_exception', ( - (TPR2020B2, IOError), # Gromacs 2020 beta see issue #2428 - (TPR2020B2_bonded, IOError), # Gromacs 2020 beta see issue #2428 - (TPR334_bonded, NotImplementedError), # Too old - (XTC, IOError), # Not a TPR file -)) +@pytest.mark.parametrize( + "tpr_path, expected_exception", + ( + (TPR2020B2, IOError), # Gromacs 2020 beta see issue #2428 + (TPR2020B2_bonded, IOError), # Gromacs 2020 beta see issue #2428 + (TPR334_bonded, NotImplementedError), # Too old + (XTC, IOError), # Not a TPR file + ), +) def test_fail_for_unsupported_files(tpr_path, expected_exception): parser = MDAnalysis.topology.TPRParser.TPRParser(tpr_path) with pytest.raises(expected_exception): parser.parse() -@pytest.mark.parametrize('tpr_path', BONDED_TPRS) +@pytest.mark.parametrize("tpr_path", BONDED_TPRS) def test_no_elements(tpr_path): """ If the TPR does not contain element information, the element topology @@ -314,26 +369,40 @@ def test_elements(): tpr_path = TPR parser = MDAnalysis.topology.TPRParser.TPRParser(tpr_path) topology = parser.parse() - reference = np.array(( - 'H,C,H,H,C,H,H,H,C,H,H,H,C,O,N,H,C,H,C,H,H,C,H,C,H,H,H,C,H,H,' - 'H,C,O,N,H,C,H,H,C,O,O,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H' - ',H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,' - 'O,H,H' - ).split(','), dtype=object) + reference = np.array( + ( + "H,C,H,H,C,H,H,H,C,H,H,H,C,O,N,H,C,H,C,H,H,C,H,C,H,H,H,C,H,H," + "H,C,O,N,H,C,H,H,C,O,O,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H" + ",H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,,O,H,H,," + "O,H,H" + ).split(","), + dtype=object, + ) assert_equal(topology.elements.values[3300:3400], reference) - reference = np.array([ - 'O', 'H', 'H', '', 'O', 'H', 'H', '', 'O', 'H', 'H', '', 'O', 'H', - 'H', '', 'Na', 'Na', 'Na', 'Na', - ], dtype=object) + # fmt: off + reference = np.array( + [ + "O", "H", "H", "", "O", "H", "H", "", "O", "H", "H", "", "O", "H", + "H", "", "Na", "Na", "Na", "Na", + ], + dtype=object, + ) + # fmt: on assert_equal(topology.elements.values[-20:], reference) -@pytest.mark.parametrize("resid_from_one,resid_addition", [ - (False, 0), - (True, 1), # status quo for 2.x - ]) +@pytest.mark.parametrize( + "resid_from_one,resid_addition", + [ + (False, 0), + (True, 1), # status quo for 2.x + ], +) def test_resids(resid_from_one, resid_addition): u = mda.Universe(TPR, tpr_resid_from_one=resid_from_one) resids = np.arange(len(u.residues)) + resid_addition - assert_equal(u.residues.resids, resids, - err_msg="tpr_resid_from_one kwarg not switching resids") + assert_equal( + u.residues.resids, + resids, + err_msg="tpr_resid_from_one kwarg not switching resids", + ) diff --git a/testsuite/MDAnalysisTests/topology/test_txyz.py b/testsuite/MDAnalysisTests/topology/test_txyz.py index 06c2e757e0f..5615bdc985a 100644 --- a/testsuite/MDAnalysisTests/topology/test_txyz.py +++ b/testsuite/MDAnalysisTests/topology/test_txyz.py @@ -31,8 +31,8 @@ class TestTXYZParser(ParserBase): parser = mda.topology.TXYZParser.TXYZParser - guessed_attrs = ['masses'] - expected_attrs = ['ids', 'names', 'bonds', 'types', 'elements'] + guessed_attrs = ["masses"] + expected_attrs = ["ids", "names", "bonds", "types", "elements"] expected_n_residues = 1 expected_n_atoms = 9 @@ -55,12 +55,15 @@ def test_atom_type_type(self, top): type_is_str = [isinstance(atom_type, str) for atom_type in types] assert all(type_is_str) + def test_TXYZ_elements(): """The test checks whether elements attribute are assigned properly given a TXYZ file with valid elements record. """ - u = mda.Universe(TXYZ, format='TXYZ') - element_list = np.array(['C', 'H', 'H', 'O', 'H', 'C', 'H', 'H', 'H'], dtype=object) + 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) @@ -70,8 +73,10 @@ def test_missing_elements_noattribute(): 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 ") + 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): @@ -80,6 +85,15 @@ def test_missing_elements_noattribute(): 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] + 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 2be72fb8e7e..ba55580acd4 100644 --- a/testsuite/MDAnalysisTests/topology/test_xpdb.py +++ b/testsuite/MDAnalysisTests/topology/test_xpdb.py @@ -33,10 +33,19 @@ class TestXPDBParser(ParserBase): parser = mda.topology.ExtendedPDBParser.ExtendedPDBParser ref_filename = XPDB_small - expected_attrs = ['ids', 'names', 'record_types', 'resids', - 'resnames', 'altLocs', 'icodes', 'occupancies', - 'tempfactors', 'chainIDs'] - guessed_attrs = ['masses', 'types'] + expected_attrs = [ + "ids", + "names", + "record_types", + "resids", + "resnames", + "altLocs", + "icodes", + "occupancies", + "tempfactors", + "chainIDs", + ] + guessed_attrs = ["masses", "types"] expected_n_atoms = 5 expected_n_residues = 5 expected_n_segments = 1 @@ -48,5 +57,5 @@ def test_guessed_masses(self, filename): def test_guessed_types(self, filename): u = mda.Universe(filename) - expected = ['O', 'O', 'O', 'O', 'O'] + 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 07b0159d6dc..4574e4edb43 100644 --- a/testsuite/MDAnalysisTests/topology/test_xyz.py +++ b/testsuite/MDAnalysisTests/topology/test_xyz.py @@ -37,8 +37,8 @@ class XYZBase(ParserBase): parser = mda.topology.XYZParser.XYZParser expected_n_residues = 1 expected_n_segments = 1 - expected_attrs = ['names', 'elements'] - guessed_attrs = ['masses', 'types'] + expected_attrs = ["names", "elements"] + guessed_attrs = ["masses", "types"] class TestXYZMini(XYZBase): @@ -60,5 +60,5 @@ def test_guessed_masses(self, filename): def test_guessed_types(self, filename): u = mda.Universe(filename) - expected = ['H', 'H', 'H', 'H', 'H', 'H', 'H'] + expected = ["H", "H", "H", "H", "H", "H", "H"] assert_equal(u.atoms.types[:7], expected) diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 4a9a4bf6251..63450efa749 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -162,6 +162,7 @@ setup\.py | MDAnalysisTests/auxiliary/.*\.py | MDAnalysisTests/lib/.*\.py | MDAnalysisTests/transformations/.*\.py +| MDAnalysisTests/topology/.*\.py | MDAnalysisTests/analysis/.*\.py | MDAnalysisTests/guesser/.*\.py | MDAnalysisTests/converters/.*\.py From b8fe34b73c9df9330c1608229b2f8cddc6e275b4 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 31 Dec 2024 05:20:55 +0100 Subject: [PATCH 50/58] [fmt] utils, import, viz tests (#4875) --- .../MDAnalysisTests/import/fork_called.py | 3 +- .../MDAnalysisTests/import/test_import.py | 17 +- .../MDAnalysisTests/utils/test_authors.py | 5 +- .../MDAnalysisTests/utils/test_datafiles.py | 35 +- .../MDAnalysisTests/utils/test_duecredit.py | 104 +++-- .../MDAnalysisTests/utils/test_failure.py | 11 +- .../MDAnalysisTests/utils/test_imports.py | 22 +- testsuite/MDAnalysisTests/utils/test_log.py | 7 +- testsuite/MDAnalysisTests/utils/test_meta.py | 40 +- .../MDAnalysisTests/utils/test_modelling.py | 175 +++++--- .../MDAnalysisTests/utils/test_persistence.py | 27 +- .../MDAnalysisTests/utils/test_pickleio.py | 123 +++--- .../MDAnalysisTests/utils/test_qcprot.py | 113 +++-- .../MDAnalysisTests/utils/test_selections.py | 107 +++-- .../MDAnalysisTests/utils/test_streamio.py | 223 ++++++---- .../utils/test_transformations.py | 391 +++++++++++------- testsuite/MDAnalysisTests/utils/test_units.py | 138 ++++--- .../visualization/test_streamlines.py | 136 +++--- testsuite/pyproject.toml | 3 + 19 files changed, 1068 insertions(+), 612 deletions(-) diff --git a/testsuite/MDAnalysisTests/import/fork_called.py b/testsuite/MDAnalysisTests/import/fork_called.py index 823f122a4e5..5bfb6b390f1 100644 --- a/testsuite/MDAnalysisTests/import/fork_called.py +++ b/testsuite/MDAnalysisTests/import/fork_called.py @@ -26,6 +26,7 @@ """Tests whether os.fork() is called as a side effect when importing MDAnalysis. See PR #1794 for details.""" -with mock.patch('os.fork') as os_dot_fork: +with mock.patch("os.fork") as os_dot_fork: import MDAnalysis + assert not os_dot_fork.called diff --git a/testsuite/MDAnalysisTests/import/test_import.py b/testsuite/MDAnalysisTests/import/test_import.py index d8065f4ac18..9794b716ef0 100644 --- a/testsuite/MDAnalysisTests/import/test_import.py +++ b/testsuite/MDAnalysisTests/import/test_import.py @@ -27,8 +27,10 @@ """Test if importing MDAnalysis has unwanted side effects (PR #1794).""" -@pytest.mark.skipif(os.name == 'nt', - reason="fork-related import checks irrelevant on Windows") + +@pytest.mark.skipif( + os.name == "nt", reason="fork-related import checks irrelevant on Windows" +) class TestMDAImport(object): # Tests concerning importing MDAnalysis. def test_os_dot_fork_not_called(self): @@ -37,7 +39,7 @@ def test_os_dot_fork_not_called(self): # no previously imported modules interfere with it. It is therefore # offloaded to the script "fork_called.py". loc = os.path.dirname(os.path.realpath(__file__)) - script = os.path.join(loc, 'fork_called.py') + script = os.path.join(loc, "fork_called.py") encoding = sys.stdout.encoding if encoding is None: encoding = "utf-8" @@ -47,16 +49,17 @@ def test_os_dot_fork_not_called(self): # CalledProcessError. That error's output member then contains the # failed script's stderr and we can print it: try: - out = subprocess.check_output([sys.executable, script], - stderr=subprocess.STDOUT)\ - .decode(encoding) + out = subprocess.check_output( + [sys.executable, script], stderr=subprocess.STDOUT + ).decode(encoding) except subprocess.CalledProcessError as err: print(err.output) - raise(err) + raise (err) def test_os_dot_fork_not_none(self): # In MDAnalysis.core.universe, os.fork is set to None prior to importing # the uuid module and restored afterwards (see PR #1794 for details). # This tests asserts that os.fork has been restored. import MDAnalysis + assert os.fork is not None diff --git a/testsuite/MDAnalysisTests/utils/test_authors.py b/testsuite/MDAnalysisTests/utils/test_authors.py index 7e1a69960d2..13563dadfdf 100644 --- a/testsuite/MDAnalysisTests/utils/test_authors.py +++ b/testsuite/MDAnalysisTests/utils/test_authors.py @@ -24,5 +24,8 @@ import MDAnalysis + def test_package_authors(): - assert len(MDAnalysis.__authors__) > 0, 'Could not find the list of authors' + assert ( + len(MDAnalysis.__authors__) > 0 + ), "Could not find the list of authors" diff --git a/testsuite/MDAnalysisTests/utils/test_datafiles.py b/testsuite/MDAnalysisTests/utils/test_datafiles.py index 92caf2348f6..7b79bb86404 100644 --- a/testsuite/MDAnalysisTests/utils/test_datafiles.py +++ b/testsuite/MDAnalysisTests/utils/test_datafiles.py @@ -29,10 +29,10 @@ def test_failed_import(monkeypatch): # Putting this test first to avoid datafiles already being loaded errmsg = "MDAnalysisTests package not installed." - monkeypatch.setitem(sys.modules, 'MDAnalysisTests.datafiles', None) + monkeypatch.setitem(sys.modules, "MDAnalysisTests.datafiles", None) - if 'MDAnalysis.tests.datafiles' in sys.modules: - monkeypatch.delitem(sys.modules, 'MDAnalysis.tests.datafiles') + if "MDAnalysis.tests.datafiles" in sys.modules: + monkeypatch.delitem(sys.modules, "MDAnalysis.tests.datafiles") with pytest.raises(ImportError, match=errmsg): import MDAnalysis.tests.datafiles @@ -42,20 +42,35 @@ def test_import(): try: import MDAnalysis.tests.datafiles except ImportError: - pytest.fail("Failed to 'import MDAnalysis.tests.datafiles --- install MDAnalysisTests") + pytest.fail( + "Failed to 'import MDAnalysis.tests.datafiles --- install MDAnalysisTests" + ) def test_all_exports(): import MDAnalysisTests.datafiles - missing = [name for name in dir(MDAnalysisTests.datafiles) - if - not name.startswith('_') and name not in MDAnalysisTests.datafiles.__all__ and name != 'MDAnalysisTests'] + + missing = [ + name + for name in dir(MDAnalysisTests.datafiles) + if not name.startswith("_") + and name not in MDAnalysisTests.datafiles.__all__ + and name != "MDAnalysisTests" + ] assert_equal(missing, [], err_msg="Variables need to be added to __all__.") def test_export_variables(): import MDAnalysisTests.datafiles import MDAnalysis.tests.datafiles - missing = [name for name in MDAnalysisTests.datafiles.__all__ - if name not in dir(MDAnalysis.tests.datafiles)] - assert_equal(missing, [], err_msg="Variables not exported to MDAnalysis.tests.datafiles") + + missing = [ + name + for name in MDAnalysisTests.datafiles.__all__ + if name not in dir(MDAnalysis.tests.datafiles) + ] + assert_equal( + missing, + [], + err_msg="Variables not exported to MDAnalysis.tests.datafiles", + ) diff --git a/testsuite/MDAnalysisTests/utils/test_duecredit.py b/testsuite/MDAnalysisTests/utils/test_duecredit.py index d567d256f5d..f0224a9a73b 100644 --- a/testsuite/MDAnalysisTests/utils/test_duecredit.py +++ b/testsuite/MDAnalysisTests/utils/test_duecredit.py @@ -35,48 +35,68 @@ # duecredit itself is not needed in the name space but this is a # convenient way to skip all tests if duecredit is not installed # (see https://github.com/MDAnalysis/mdanalysis/issues/1906) -pytest.importorskip('duecredit') +pytest.importorskip("duecredit") -@pytest.mark.skipif((os.environ.get('DUECREDIT_ENABLE', 'yes').lower() - in ('no', '0', 'false')), - reason= - "duecredit is explicitly disabled with DUECREDIT_ENABLE=no") + +@pytest.mark.skipif( + ( + os.environ.get("DUECREDIT_ENABLE", "yes").lower() + in ("no", "0", "false") + ), + reason="duecredit is explicitly disabled with DUECREDIT_ENABLE=no", +) class TestDuecredit(object): def test_duecredit_active(self): assert mda.due.active == True - @pytest.mark.parametrize("module,path,citekey", [ - ("MDAnalysis", "MDAnalysis", "10.25080/majora-629e541a-00e"), - ("MDAnalysis", "MDAnalysis", "10.1002/jcc.21787"), - ]) + @pytest.mark.parametrize( + "module,path,citekey", + [ + ("MDAnalysis", "MDAnalysis", "10.25080/majora-629e541a-00e"), + ("MDAnalysis", "MDAnalysis", "10.1002/jcc.21787"), + ], + ) def test_duecredit_collector_primary(self, module, path, citekey): assert mda.due.citations[(path, citekey)].cites_module == True # note: citekeys are *all lower case* - @pytest.mark.parametrize("module,path,citekey", [ - ("MDAnalysis.analysis.psa", - "pathsimanalysis.psa", - "10.1371/journal.pcbi.1004568"), - ("MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel", - "MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel", - "10.1063/1.4922445"), - ("MDAnalysis.analysis.leaflet", - "MDAnalysis.analysis.leaflet", - "10.1002/jcc.21787"), - ("MDAnalysis.lib.qcprot", - "MDAnalysis.lib.qcprot", - "10.1107/s0108767305015266"), - ("MDAnalysis.lib.qcprot", - "MDAnalysis.lib.qcprot", - "qcprot2"), - ("MDAnalysis.analysis.encore", - "MDAnalysis.analysis.encore", - "10.1371/journal.pcbi.1004415"), - ("MDAnalysis.analysis.dssp", - "MDAnalysis.analysis.dssp", - "10.1002/bip.360221211") - ]) + @pytest.mark.parametrize( + "module,path,citekey", + [ + ( + "MDAnalysis.analysis.psa", + "pathsimanalysis.psa", + "10.1371/journal.pcbi.1004568", + ), + ( + "MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel", + "MDAnalysis.analysis.hydrogenbonds.hbond_autocorrel", + "10.1063/1.4922445", + ), + ( + "MDAnalysis.analysis.leaflet", + "MDAnalysis.analysis.leaflet", + "10.1002/jcc.21787", + ), + ( + "MDAnalysis.lib.qcprot", + "MDAnalysis.lib.qcprot", + "10.1107/s0108767305015266", + ), + ("MDAnalysis.lib.qcprot", "MDAnalysis.lib.qcprot", "qcprot2"), + ( + "MDAnalysis.analysis.encore", + "MDAnalysis.analysis.encore", + "10.1371/journal.pcbi.1004415", + ), + ( + "MDAnalysis.analysis.dssp", + "MDAnalysis.analysis.dssp", + "10.1002/bip.360221211", + ), + ], + ) def test_duecredit_collector_analysis_modules(self, module, path, citekey): importlib.import_module(module) assert mda.due.citations[(path, citekey)].cites_module == True @@ -85,17 +105,21 @@ def test_duecredit_mmtf(self): # doesn't trigger on import but on use of either parser or reader u = mda.Universe(MMTF) - assert mda.due.citations[('MDAnalysis.coordinates.MMTF', - '10.1371/journal.pcbi.1005575')].cites_module - assert mda.due.citations[('MDAnalysis.topology.MMTFParser', - '10.1371/journal.pcbi.1005575')].cites_module + assert mda.due.citations[ + ("MDAnalysis.coordinates.MMTF", "10.1371/journal.pcbi.1005575") + ].cites_module + assert mda.due.citations[ + ("MDAnalysis.topology.MMTFParser", "10.1371/journal.pcbi.1005575") + ].cites_module @pytest.mark.skipif(not HAS_H5PY, reason="h5py not installed") def test_duecredit_h5md(self): # doesn't trigger on import but on use of either reader or writer u = mda.Universe(TPR_xvf, H5MD_xvf) - assert mda.due.citations[('MDAnalysis.coordinates.H5MD', - '10.25080/majora-1b6fd038-005')].cites_module - assert mda.due.citations[('MDAnalysis.coordinates.H5MD', - '10.1016/j.cpc.2014.01.018')].cites_module + assert mda.due.citations[ + ("MDAnalysis.coordinates.H5MD", "10.25080/majora-1b6fd038-005") + ].cites_module + assert mda.due.citations[ + ("MDAnalysis.coordinates.H5MD", "10.1016/j.cpc.2014.01.018") + ].cites_module diff --git a/testsuite/MDAnalysisTests/utils/test_failure.py b/testsuite/MDAnalysisTests/utils/test_failure.py index b1ec9f1e869..494e84cdb9f 100644 --- a/testsuite/MDAnalysisTests/utils/test_failure.py +++ b/testsuite/MDAnalysisTests/utils/test_failure.py @@ -24,9 +24,10 @@ def test_failure(): - """Fail if the MDA_FAILURE_TEST environment variable is set. - """ - if u'MDA_FAILURE_TEST' in os.environ: + """Fail if the MDA_FAILURE_TEST environment variable is set.""" + if "MDA_FAILURE_TEST" in os.environ: # Have a file open to trigger an output from the open_files plugin. - f = open('./failure.txt', 'w') - raise AssertionError("the MDA_FAILURE_TEST environment variable is set") + f = open("./failure.txt", "w") + raise AssertionError( + "the MDA_FAILURE_TEST environment variable is set" + ) diff --git a/testsuite/MDAnalysisTests/utils/test_imports.py b/testsuite/MDAnalysisTests/utils/test_imports.py index bd343e2b992..016388e3244 100644 --- a/testsuite/MDAnalysisTests/utils/test_imports.py +++ b/testsuite/MDAnalysisTests/utils/test_imports.py @@ -29,11 +29,11 @@ path_to_testing_modules = MDAnalysisTests.__path__[0] # Exclusion path relative to MDAnalysisTests -exclusions = ['/plugins', '/data'] +exclusions = ["/plugins", "/data"] def is_excluded(path): - leaf = path[len(path_to_testing_modules):] + leaf = path[len(path_to_testing_modules) :] return leaf in exclusions @@ -42,7 +42,7 @@ def get_file_paths(): for dirpath, dirnames, files in os.walk(path_to_testing_modules): if is_excluded(dirpath): continue - for f in filter(lambda x: x.endswith('.py'), files): + for f in filter(lambda x: x.endswith(".py"), files): fpath = os.path.join(dirpath, f) if is_excluded(fpath): continue @@ -50,12 +50,18 @@ def get_file_paths(): return paths -@pytest.mark.parametrize('testing_module', get_file_paths()) +@pytest.mark.parametrize("testing_module", get_file_paths()) def test_relative_import(testing_module): - with open(testing_module, 'r') as test_module_file_object: + with open(testing_module, "r") as test_module_file_object: for lineno, line in enumerate(test_module_file_object, start=1): - if 'from .' in line and 'import' in line \ - and not 'test_imports' in testing_module: + if ( + "from ." in line + and "import" in line + and not "test_imports" in testing_module + ): raise AssertionError( "A relative import statement was found in " - "module {testing_module} at linenumber {lineno}.".format(**vars())) + "module {testing_module} at linenumber {lineno}.".format( + **vars() + ) + ) diff --git a/testsuite/MDAnalysisTests/utils/test_log.py b/testsuite/MDAnalysisTests/utils/test_log.py index 2e95edb39b0..0abaed2795d 100644 --- a/testsuite/MDAnalysisTests/utils/test_log.py +++ b/testsuite/MDAnalysisTests/utils/test_log.py @@ -68,5 +68,8 @@ def buffer(): def _assert_in(output, string): - assert string in output, "Output '{0}' does not match required format '{1}'.".format(output.replace('\r', '\\r'), string.replace('\r', '\\r')) - + assert ( + string in output + ), "Output '{0}' does not match required format '{1}'.".format( + output.replace("\r", "\\r"), string.replace("\r", "\\r") + ) diff --git a/testsuite/MDAnalysisTests/utils/test_meta.py b/testsuite/MDAnalysisTests/utils/test_meta.py index def0a35e740..b6e536ef009 100644 --- a/testsuite/MDAnalysisTests/utils/test_meta.py +++ b/testsuite/MDAnalysisTests/utils/test_meta.py @@ -24,33 +24,47 @@ import MDAnalysisTests + def test_import(): try: import MDAnalysis except ImportError: - raise AssertionError('Failed to import module MDAnalysis. Install MDAnalysis' - 'first to run the tests, e.g. "pip install mdanalysis"') + raise AssertionError( + "Failed to import module MDAnalysis. Install MDAnalysis" + 'first to run the tests, e.g. "pip install mdanalysis"' + ) def test_matching_versions(): import MDAnalysis.version - assert MDAnalysis.version.__version__ == MDAnalysisTests.__version__, \ - "MDAnalysis release {0} must be installed to have meaningful tests, not {1}".format( - MDAnalysisTests.__version__, MDAnalysis.__version__) + + assert ( + MDAnalysis.version.__version__ == MDAnalysisTests.__version__ + ), "MDAnalysis release {0} must be installed to have meaningful tests, not {1}".format( + MDAnalysisTests.__version__, MDAnalysis.__version__ + ) def test_version_format(version=None): if version is None: import MDAnalysis.version + version = MDAnalysis.version.__version__ # see https://github.com/MDAnalysis/mdanalysis/wiki/SemanticVersioning for format definition - m = re.match(r'(?P\d+)\.(?P\d+)\.(?P\d+)(-(?P\w+))?$', - version) - assert m, "version {0} does not match the MAJOR.MINOR.PATCH(-suffix) format".format(version) + m = re.match( + r"(?P\d+)\.(?P\d+)\.(?P\d+)(-(?P\w+))?$", + version, + ) + assert ( + m + ), "version {0} does not match the MAJOR.MINOR.PATCH(-suffix) format".format( + version + ) def test_version_at_packagelevel(): import MDAnalysis + try: version = MDAnalysis.__version__ except: @@ -61,24 +75,24 @@ def test_version_at_packagelevel(): # The following allow testing of the memleak tester plugin. # Keep commented out unless you suspect the plugin # might be misbehaving. Apparently python3 is immune to these leaks!""" -#from numpy.testing import TestCase -#class A(): +# from numpy.testing import TestCase +# class A(): # """This is a small leaky class that won't break anything.""" # def __init__(self): # self.self_ref = self # def __del__(self): # pass # -#def test_that_memleaks(): +# def test_that_memleaks(): # """Test that memleaks (Issue 323)""" # a = A() # -#class TestML1(TestCase): +# class TestML1(TestCase): # def test_that_memleaks(self): # """Test that memleaks (Issue 323)""" # self.a = A() # -#class TestML2(TestCase): +# class TestML2(TestCase): # def setUp(self): # a = A() # def test_that_memleaks(self): diff --git a/testsuite/MDAnalysisTests/utils/test_modelling.py b/testsuite/MDAnalysisTests/utils/test_modelling.py index bae825da3ac..c014c9f4d03 100644 --- a/testsuite/MDAnalysisTests/utils/test_modelling.py +++ b/testsuite/MDAnalysisTests/utils/test_modelling.py @@ -32,7 +32,7 @@ capping_nma, merge_protein, merge_ligand, - merge_water + merge_water, ) import MDAnalysis.core.groups from MDAnalysis.core.groups import AtomGroup @@ -55,22 +55,39 @@ def capping(ref, ace, nma, output): # TODO pick the first residue in the protein (how should we cap the chains?) # TODO consider a case when the protein resid is 1 and all peptide has to be shifted by +1, put that in docs as a # post-processing step - alignto(ace, ref, select={ + alignto( + ace, + ref, + select={ "mobile": "resid {0} and backbone".format(resid_min), - "reference": "resid {0} and backbone".format(resid_min)}, - strict=True) - alignto(nma, ref, select={ - "mobile": "resid {0} and backbone and not (resname NMA NME)".format(resid_max), - "reference": "resid {0} and (backbone or name OT2)".format(resid_max)}, - strict=True) + "reference": "resid {0} and backbone".format(resid_min), + }, + strict=True, + ) + alignto( + nma, + ref, + select={ + "mobile": "resid {0} and backbone and not (resname NMA NME)".format( + resid_max + ), + "reference": "resid {0} and (backbone or name OT2)".format( + resid_max + ), + }, + strict=True, + ) # TODO remove the Hydrogen closest to ACE's oxygen nma.residues.resids = 16 - u = Merge(ace.select_atoms("resname ACE"), - ref.select_atoms( - "not (resid {0} and name HT*) and not (resid {1} and (name HT* OT1))" - "".format(resid_min, resid_max)), - nma.select_atoms("resname NME NMA")) + u = Merge( + ace.select_atoms("resname ACE"), + ref.select_atoms( + "not (resid {0} and name HT*) and not (resid {1} and (name HT* OT1))" + "".format(resid_min, resid_max) + ), + nma.select_atoms("resname NME NMA"), + ) u.trajectory.ts.dimensions = ref.trajectory.ts.dimensions u.atoms.write(output) return u @@ -84,11 +101,13 @@ def test_capping_file(self, tmpdir): ace = MDAnalysis.Universe(capping_ace) nma = MDAnalysis.Universe(capping_nma) - outfile = str(tmpdir.join('test.pdb')) + outfile = str(tmpdir.join("test.pdb")) u = capping(peptide, ace, nma, outfile) - assert_equal(len(u.select_atoms("not name H*")), - len(ref.select_atoms("not name H*"))) + assert_equal( + len(u.select_atoms("not name H*")), + len(ref.select_atoms("not name H*")), + ) u = MDAnalysis.Universe(outfile) @@ -99,8 +118,9 @@ def test_capping_file(self, tmpdir): assert_equal(ace.resids[0], 1) assert_equal(nma.resids[0], 16) - assert_array_equal(peptide.trajectory.ts.dimensions, - u.trajectory.ts.dimensions) + assert_array_equal( + peptide.trajectory.ts.dimensions, u.trajectory.ts.dimensions + ) def test_capping_inmemory(self, tmpdir): peptide = MDAnalysis.Universe(capping_input) @@ -108,10 +128,12 @@ def test_capping_inmemory(self, tmpdir): ace = MDAnalysis.Universe(capping_ace) nma = MDAnalysis.Universe(capping_nma) - outfile = str(tmpdir.join('test.pdb')) + outfile = str(tmpdir.join("test.pdb")) u = capping(peptide, ace, nma, outfile) - assert_equal(len(u.select_atoms("not name H*")), - len(ref.select_atoms("not name H*"))) + assert_equal( + len(u.select_atoms("not name H*")), + len(ref.select_atoms("not name H*")), + ) ace = u.select_atoms("resname ACE") nma = u.select_atoms("resname NMA") @@ -120,8 +142,9 @@ def test_capping_inmemory(self, tmpdir): assert_equal(ace.resids[0], 1) assert_equal(nma.resids[0], 16) - assert_array_equal(peptide.trajectory.ts.dimensions, - u.trajectory.ts.dimensions) + assert_array_equal( + peptide.trajectory.ts.dimensions, u.trajectory.ts.dimensions + ) @pytest.fixture() @@ -138,6 +161,7 @@ def u_ligand(): def u_water(): return MDAnalysis.Universe(merge_water) + @pytest.fixture() def u_without_coords(): return MDAnalysis.Universe(PSF) @@ -145,18 +169,35 @@ def u_without_coords(): class TestMerge(object): def test_merge(self, u_protein, u_ligand, u_water, tmpdir): - ids_before = [a.index for u in [u_protein, u_ligand, u_water] for a in u.atoms] + ids_before = [ + a.index for u in [u_protein, u_ligand, u_water] for a in u.atoms + ] # Do the merge u0 = MDAnalysis.Merge(u_protein.atoms, u_ligand.atoms, u_water.atoms) # Check that the output Universe has the same number of atoms as the # starting AtomGroups - assert_equal(len(u0.atoms), (len(u_protein.atoms) + len(u_ligand.atoms) + len(u_water.atoms))) + assert_equal( + len(u0.atoms), + (len(u_protein.atoms) + len(u_ligand.atoms) + len(u_water.atoms)), + ) # Check that the output Universe has the same number of residues and # segments as the starting AtomGroups - assert_equal(len(u0.residues), (len(u_protein.residues) + len(u_ligand.residues) + - len(u_water.residues))) - assert_equal(len(u0.segments), (len(u_protein.segments) + len(u_ligand.segments) + - len(u_water.segments))) + assert_equal( + len(u0.residues), + ( + len(u_protein.residues) + + len(u_ligand.residues) + + len(u_water.residues) + ), + ) + assert_equal( + len(u0.segments), + ( + len(u_protein.segments) + + len(u_ligand.segments) + + len(u_water.segments) + ), + ) # Make sure that all the atoms in the new universe are assigned to only # one, new Universe @@ -167,15 +208,20 @@ def test_merge(self, u_protein, u_ligand, u_water, tmpdir): # Make sure that the atom ids of the original universes are unchanged, # ie we didn't make the original Universes 'dirty' - ids_after = [a.index for u in [u_protein, u_ligand, u_water] for a in u.atoms] - assert_equal(len(ids_after), (len(u_protein.atoms) + len(u_ligand.atoms) + len(u_water.atoms))) + ids_after = [ + a.index for u in [u_protein, u_ligand, u_water] for a in u.atoms + ] + assert_equal( + len(ids_after), + (len(u_protein.atoms) + len(u_ligand.atoms) + len(u_water.atoms)), + ) assert_equal(ids_before, ids_after) # Test that we have a same number of atoms in a different way ids_new = [a.index for a in u0.atoms] assert_equal(len(ids_new), len(ids_before)) - outfile = str(tmpdir.join('test.pdb')) + outfile = str(tmpdir.join("test.pdb")) u0.atoms.write(outfile) u = MDAnalysis.Universe(outfile) @@ -183,20 +229,28 @@ def test_merge(self, u_protein, u_ligand, u_water, tmpdir): assert_equal(ids_new, ids_new2) def test_merge_same_universe(self, u_protein): - u0 = MDAnalysis.Merge(u_protein.atoms, u_protein.atoms, u_protein.atoms) + u0 = MDAnalysis.Merge( + u_protein.atoms, u_protein.atoms, u_protein.atoms + ) assert_equal(len(u0.atoms), 3 * len(u_protein.atoms)) assert_equal(len(u0.residues), 3 * len(u_protein.residues)) assert_equal(len(u0.segments), 3 * len(u_protein.segments)) def test_residue_references(self, u_protein, u_ligand): m = Merge(u_protein.atoms, u_ligand.atoms) - assert_equal(m.atoms.residues[0].universe, m, - "wrong universe reference for residues after Merge()") + assert_equal( + m.atoms.residues[0].universe, + m, + "wrong universe reference for residues after Merge()", + ) def test_segment_references(self, u_protein, u_ligand): m = Merge(u_protein.atoms, u_ligand.atoms) - assert_equal(m.atoms.segments[0].universe, m, - "wrong universe reference for segments after Merge()") + assert_equal( + m.atoms.segments[0].universe, + m, + "wrong universe reference for segments after Merge()", + ) def test_empty_ValueError(self): with pytest.raises(ValueError): @@ -204,7 +258,7 @@ def test_empty_ValueError(self): def test_nonsense_TypeError(self): with pytest.raises(TypeError): - Merge(['1', 2]) + Merge(["1", 2]) def test_emptyAG_ValueError(self, u_protein): a = AtomGroup([], u_protein) @@ -215,8 +269,8 @@ def test_emptyAG_ValueError(self, u_protein): def test_merge_without_coords(self, u_without_coords): subset = MDAnalysis.Merge(u_without_coords.atoms[:10]) - assert(isinstance(subset, MDAnalysis.Universe)) - assert_equal(len(subset.atoms) , 10) + assert isinstance(subset, MDAnalysis.Universe) + assert_equal(len(subset.atoms), 10) class TestMergeTopology(object): @@ -233,36 +287,45 @@ def test_merge_with_topology(self, u): u_merge = MDAnalysis.Merge(ag1, ag2) - assert(len(u_merge.atoms) == 30) - assert(len(u_merge.atoms.bonds) == 28) - assert(len(u_merge.atoms.angles) == 47) - assert(len(u_merge.atoms.dihedrals) == 53) - assert(len(u_merge.atoms.impropers) == 1) + assert len(u_merge.atoms) == 30 + assert len(u_merge.atoms.bonds) == 28 + assert len(u_merge.atoms.angles) == 47 + assert len(u_merge.atoms.dihedrals) == 53 + assert len(u_merge.atoms.impropers) == 1 # All these bonds are in the merged Universe - assert(len(ag1[0].bonds) == len(u_merge.atoms[0].bonds)) + assert len(ag1[0].bonds) == len(u_merge.atoms[0].bonds) # One of these bonds isn't in the merged Universe - assert(len(ag2[0].bonds) - 1 == len(u_merge.atoms[20].bonds)) + assert len(ag2[0].bonds) - 1 == len(u_merge.atoms[20].bonds) def test_merge_with_topology_from_different_universes(self, u, u_ligand): u_merge = MDAnalysis.Merge(u.atoms[:110], u_ligand.atoms) # merge_protein doesn't contain bond topology, so merged universe # shouldn't have one either - assert not hasattr(u_merge.atoms, 'bonds') + assert not hasattr(u_merge.atoms, "bonds") # PDB reader yields empty Bonds group, which means bonds from # PSF/DCD survive the merge # assert(not hasattr(u_merge.atoms, 'bonds') or len(u_merge.atoms.bonds) == 0) - assert(not hasattr(u_merge.atoms, 'angles') or len(u_merge.atoms.bonds) == 0) - assert(not hasattr(u_merge.atoms, 'dihedrals') or len(u_merge.atoms.bonds) == 0) - assert(not hasattr(u_merge.atoms, 'impropers') or len(u_merge.atoms.bonds) == 0) + assert ( + not hasattr(u_merge.atoms, "angles") + or len(u_merge.atoms.bonds) == 0 + ) + assert ( + not hasattr(u_merge.atoms, "dihedrals") + or len(u_merge.atoms.bonds) == 0 + ) + assert ( + not hasattr(u_merge.atoms, "impropers") + or len(u_merge.atoms.bonds) == 0 + ) def test_merge_without_topology(self, u): # This shouldn't have topology as we merged single atoms u_merge = MDAnalysis.Merge(u.atoms[0:1], u.atoms[10:11]) - assert(len(u_merge.atoms) == 2) - assert(len(u_merge.atoms.bonds) == 0) - assert(len(u_merge.atoms.angles) == 0) - assert(len(u_merge.atoms.dihedrals) == 0) - assert(len(u_merge.atoms.impropers) == 0) + assert len(u_merge.atoms) == 2 + assert len(u_merge.atoms.bonds) == 0 + assert len(u_merge.atoms.angles) == 0 + assert len(u_merge.atoms.dihedrals) == 0 + assert len(u_merge.atoms.impropers) == 0 diff --git a/testsuite/MDAnalysisTests/utils/test_persistence.py b/testsuite/MDAnalysisTests/utils/test_persistence.py index c2c00e7396d..ca9ca19e6e6 100644 --- a/testsuite/MDAnalysisTests/utils/test_persistence.py +++ b/testsuite/MDAnalysisTests/utils/test_persistence.py @@ -24,10 +24,7 @@ import pickle import MDAnalysis as mda -from numpy.testing import ( - TestCase, - assert_equal -) +from numpy.testing import TestCase, assert_equal import gc @@ -89,24 +86,22 @@ def test_pickle_unpickle_empty(self, universe): def test_unpickle_two_ag(self, pickle_str_two_ag): newag, newag2 = pickle.loads(pickle_str_two_ag) - assert newag.universe is newag2.universe, ( - "Two AtomGroups are unpickled to two different Universes" - ) + assert ( + newag.universe is newag2.universe + ), "Two AtomGroups are unpickled to two different Universes" - def test_unpickle_ag_with_universe_f(self, - pickle_str_ag_with_universe_f): + def test_unpickle_ag_with_universe_f(self, pickle_str_ag_with_universe_f): newu, newag = pickle.loads(pickle_str_ag_with_universe_f) assert newag.universe is newu, ( "AtomGroup is not unpickled to the bound Universe" "when Universe is pickled first" ) - def test_unpickle_ag_with_universe(self, - pickle_str_ag_with_universe): + def test_unpickle_ag_with_universe(self, pickle_str_ag_with_universe): newag, newu = pickle.loads(pickle_str_ag_with_universe) assert newag.universe is newu, ( - "AtomGroup is not unpickled to the bound Universe" - "when AtomGroup is pickled first" + "AtomGroup is not unpickled to the bound Universe" + "when AtomGroup is pickled first" ) @@ -119,15 +114,15 @@ def u(): def test_pickling_uag(self, u): ag = u.atoms[:100] - uag = ag.select_atoms('name C', updating=True) + uag = ag.select_atoms("name C", updating=True) pickle_str = pickle.dumps(uag, protocol=pickle.HIGHEST_PROTOCOL) new_uag = pickle.loads(pickle_str) assert_equal(uag.indices, new_uag.indices) def test_pickling_uag_of_uag(self, u): - uag1 = u.select_atoms('name C or name H', updating=True) - uag2 = uag1.select_atoms('name C', updating=True) + uag1 = u.select_atoms("name C or name H", updating=True) + uag2 = uag1.select_atoms("name C", updating=True) pickle_str = pickle.dumps(uag2, protocol=pickle.HIGHEST_PROTOCOL) new_uag2 = pickle.loads(pickle_str) diff --git a/testsuite/MDAnalysisTests/utils/test_pickleio.py b/testsuite/MDAnalysisTests/utils/test_pickleio.py index 64dc6a9a66b..ae6c342cec1 100644 --- a/testsuite/MDAnalysisTests/utils/test_pickleio.py +++ b/testsuite/MDAnalysisTests/utils/test_pickleio.py @@ -36,25 +36,16 @@ bz2_pickle_open, gzip_pickle_open, ) -from MDAnalysis.coordinates.GSD import ( - GSDPicklable, - gsd_pickle_open, - HAS_GSD -) +from MDAnalysis.coordinates.GSD import GSDPicklable, gsd_pickle_open, HAS_GSD from MDAnalysis.coordinates.TRJ import ( NCDFPicklable, ) -from MDAnalysis.coordinates.chemfiles import ( - check_chemfiles_version -) +from MDAnalysis.coordinates.chemfiles import check_chemfiles_version + if check_chemfiles_version(): - from MDAnalysis.coordinates.chemfiles import ( - ChemfilesPicklable - ) + from MDAnalysis.coordinates.chemfiles import ChemfilesPicklable from MDAnalysis.coordinates.H5MD import HAS_H5PY -from MDAnalysis.coordinates.H5MD import ( - H5PYPicklable -) +from MDAnalysis.coordinates.H5MD import H5PYPicklable from MDAnalysis.tests.datafiles import ( PDB, @@ -65,17 +56,19 @@ GSD, NCDF, TPR_xvf, - H5MD_xvf + H5MD_xvf, ) -@pytest.fixture(params=[ - # filename mode - (PDB, 'r'), - (PDB, 'rt'), - (XYZ_bz2, 'rt'), - (GMS_ASYMOPT, 'rt') -]) +@pytest.fixture( + params=[ + # filename mode + (PDB, "r"), + (PDB, "rt"), + (XYZ_bz2, "rt"), + (GMS_ASYMOPT, "rt"), + ] +) def f_text(request): filename, mode = request.param return anyopen(filename, mode) @@ -96,12 +89,14 @@ def test_offset_text_same(f_text): assert_equal(f_text_pickled.tell(), f_text.tell()) -@pytest.fixture(params=[ - # filename mode ref_class - (PDB, 'rb', BufferIOPicklable), - (XYZ_bz2, 'rb', BZ2Picklable), - (MMTF_gz, 'rb', GzipPicklable) -]) +@pytest.fixture( + params=[ + # filename mode ref_class + (PDB, "rb", BufferIOPicklable), + (XYZ_bz2, "rb", BZ2Picklable), + (MMTF_gz, "rb", GzipPicklable), + ] +) def f_byte(request): filename, mode, ref_reader_class = request.param return anyopen(filename, mode), ref_reader_class @@ -136,14 +131,16 @@ def test_fileio_pickle(): assert_equal(raw_io.readlines(), raw_io_pickled.readlines()) -@pytest.fixture(params=[ - # filename mode open_func open_class - ('test.pdb', 'w', pickle_open, FileIOPicklable), - ('test.pdb', 'x', pickle_open, FileIOPicklable), - ('test.pdb', 'a', pickle_open, FileIOPicklable), - ('test.bz2', 'w', bz2_pickle_open, BZ2Picklable), - ('test.gz', 'w', gzip_pickle_open, GzipPicklable), -]) +@pytest.fixture( + params=[ + # filename mode open_func open_class + ("test.pdb", "w", pickle_open, FileIOPicklable), + ("test.pdb", "x", pickle_open, FileIOPicklable), + ("test.pdb", "a", pickle_open, FileIOPicklable), + ("test.bz2", "w", bz2_pickle_open, BZ2Picklable), + ("test.gz", "w", gzip_pickle_open, GzipPicklable), + ] +) def unpicklable_f(request): filename, mode, open_func, open_class = request.param return filename, mode, open_func, open_class @@ -162,26 +159,28 @@ def test_pickle_with_write_mode(unpicklable_f, tmpdir): f_pickled = pickle.loads(pickle.dumps(f_open_by_class)) -@pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') +@pytest.mark.skipif(not HAS_GSD, reason="gsd not installed") def test_GSD_pickle(): - gsd_io = gsd_pickle_open(GSD, mode='r') + gsd_io = gsd_pickle_open(GSD, mode="r") gsd_io_pickled = pickle.loads(pickle.dumps(gsd_io)) - assert_equal(gsd_io[0].particles.position, - gsd_io_pickled[0].particles.position) + assert_equal( + gsd_io[0].particles.position, gsd_io_pickled[0].particles.position + ) -@pytest.mark.skipif(not HAS_GSD, reason='gsd not installed') +@pytest.mark.skipif(not HAS_GSD, reason="gsd not installed") def test_GSD_with_write_mode(tmpdir): with pytest.raises(ValueError, match=r"Only read mode"): - gsd_io = gsd_pickle_open(tmpdir.mkdir("gsd").join('t.gsd'), - mode='w') + gsd_io = gsd_pickle_open(tmpdir.mkdir("gsd").join("t.gsd"), mode="w") def test_NCDF_pickle(): ncdf_io = NCDFPicklable(NCDF, mmap=None) ncdf_io_pickled = pickle.loads(pickle.dumps(ncdf_io)) - assert_equal(ncdf_io.variables['coordinates'][0], - ncdf_io_pickled.variables['coordinates'][0]) + assert_equal( + ncdf_io.variables["coordinates"][0], + ncdf_io_pickled.variables["coordinates"][0], + ) def test_NCDF_mmap_pickle(): @@ -190,8 +189,9 @@ def test_NCDF_mmap_pickle(): assert_equal(ncdf_io_pickled.use_mmap, False) -@pytest.mark.skipif(not check_chemfiles_version(), - reason="Wrong version of chemfiles") +@pytest.mark.skipif( + not check_chemfiles_version(), reason="Wrong version of chemfiles" +) def test_Chemfiles_pickle(): chemfiles_io = ChemfilesPicklable(XYZ) chemfiles_io_pickled = pickle.loads(pickle.dumps(chemfiles_io)) @@ -199,29 +199,34 @@ def test_Chemfiles_pickle(): # As opposed to `chemfiles_io.read().positions) frame = chemfiles_io.read() frame_pickled = chemfiles_io_pickled.read() - assert_equal(frame.positions[:], - frame_pickled.positions[:]) + assert_equal(frame.positions[:], frame_pickled.positions[:]) -@pytest.mark.skipif(not check_chemfiles_version(), - reason="Wrong version of chemfiles") +@pytest.mark.skipif( + not check_chemfiles_version(), reason="Wrong version of chemfiles" +) def test_Chemfiles_with_write_mode(tmpdir): with pytest.raises(ValueError, match=r"Only read mode"): - chemfiles_io = ChemfilesPicklable(tmpdir.mkdir("xyz").join('t.xyz'), - mode='w') + chemfiles_io = ChemfilesPicklable( + tmpdir.mkdir("xyz").join("t.xyz"), mode="w" + ) @pytest.mark.skipif(not HAS_H5PY, reason="h5py not installed") def test_H5MD_pickle(): - h5md_io = H5PYPicklable(H5MD_xvf, 'r') + h5md_io = H5PYPicklable(H5MD_xvf, "r") h5md_io_pickled = pickle.loads(pickle.dumps(h5md_io)) - assert_equal(h5md_io['particles/trajectory/position/value'][0], - h5md_io_pickled['particles/trajectory/position/value'][0]) + assert_equal( + h5md_io["particles/trajectory/position/value"][0], + h5md_io_pickled["particles/trajectory/position/value"][0], + ) @pytest.mark.skipif(not HAS_H5PY, reason="h5py not installed") def test_H5MD_pickle_with_driver(): - h5md_io = H5PYPicklable(H5MD_xvf, 'r', driver='core') + h5md_io = H5PYPicklable(H5MD_xvf, "r", driver="core") h5md_io_pickled = pickle.loads(pickle.dumps(h5md_io)) - assert_equal(h5md_io['particles/trajectory/position/value'][0], - h5md_io_pickled['particles/trajectory/position/value'][0]) + assert_equal( + h5md_io["particles/trajectory/position/value"][0], + h5md_io_pickled["particles/trajectory/position/value"][0], + ) diff --git a/testsuite/MDAnalysisTests/utils/test_qcprot.py b/testsuite/MDAnalysisTests/utils/test_qcprot.py index 484fb78c59e..8ee971c227e 100644 --- a/testsuite/MDAnalysisTests/utils/test_qcprot.py +++ b/testsuite/MDAnalysisTests/utils/test_qcprot.py @@ -47,12 +47,17 @@ @pytest.fixture() def atoms_a(): - return np.array([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], dtype=np.float64) + return np.array( + [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], dtype=np.float64 + ) @pytest.fixture() def atoms_b(): - return np.array([[13,14,15], [16,17,18], [19,20,21], [22,23,24]], dtype=np.float64) + return np.array( + [[13, 14, 15], [16, 17, 18], [19, 20, 21], [22, 23, 24]], + dtype=np.float64, + ) # Calculate rmsd after applying rotation @@ -125,23 +130,35 @@ def test_CalcRMSDRotationalMatrix(): # Calculate rmsd and rotation matrix qcp_rmsd = qcp.CalcRMSDRotationalMatrix(frag_a.T, frag_b.T, N, rot, None) - #print 'qcp rmsd = ',rmsd - #print 'rotation matrix:' - #print rot.reshape((3,3)) + # print 'qcp rmsd = ',rmsd + # print 'rotation matrix:' + # print rot.reshape((3,3)) # rotate frag_b to obtain optimal alignment frag_br = np.dot(frag_b.T, rot.reshape((3, 3))) aligned_rmsd = rmsd(frag_br.T, frag_a) - #print 'rmsd after applying rotation: ',rmsd - - assert_almost_equal(aligned_rmsd, 0.719106, 6, "RMSD between fragments A and B does not match excpected value.") - - expected_rot = np.array([ - [0.72216358, -0.52038257, -0.45572112], - [0.69118937, 0.51700833, 0.50493528], - [-0.0271479, -0.67963547, 0.73304748]]) - assert_almost_equal(rot.reshape((3, 3)), expected_rot, 6, - "Rotation matrix for aliging B to A does not have expected values.") + # print 'rmsd after applying rotation: ',rmsd + + assert_almost_equal( + aligned_rmsd, + 0.719106, + 6, + "RMSD between fragments A and B does not match excpected value.", + ) + + expected_rot = np.array( + [ + [0.72216358, -0.52038257, -0.45572112], + [0.69118937, 0.51700833, 0.50493528], + [-0.0271479, -0.67963547, 0.73304748], + ] + ) + assert_almost_equal( + rot.reshape((3, 3)), + expected_rot, + 6, + "Rotation matrix for aliging B to A does not have expected values.", + ) def test_innerproduct(atoms_a, atoms_b): @@ -158,29 +175,63 @@ def test_innerproduct(atoms_a, atoms_b): def test_RMSDmatrix(atoms_a, atoms_b): number_of_atoms = 4 rotation = np.zeros(9, dtype=np.float64) - rmsd = qcp.CalcRMSDRotationalMatrix(atoms_a, atoms_b, number_of_atoms, rotation, None) # no weights + rmsd = qcp.CalcRMSDRotationalMatrix( + atoms_a, atoms_b, number_of_atoms, rotation, None + ) # no weights rmsd_ref = 20.73219522556076 assert_almost_equal(rmsd_ref, rmsd) - rotation_ref = np.array([0.9977195, 0.02926979, 0.06082009, -.0310942, 0.9990878, 0.02926979, -0.05990789, -.0310942, 0.9977195]) + rotation_ref = np.array( + [ + 0.9977195, + 0.02926979, + 0.06082009, + -0.0310942, + 0.9990878, + 0.02926979, + -0.05990789, + -0.0310942, + 0.9977195, + ] + ) assert_array_almost_equal(rotation, rotation_ref, 6) def test_RMSDmatrix_simple(atoms_a, atoms_b): number_of_atoms = 4 rotation = np.zeros(9, dtype=np.float64) - rmsd = qcp.CalcRMSDRotationalMatrix(atoms_a, atoms_b, number_of_atoms, rotation, None) # no weights + rmsd = qcp.CalcRMSDRotationalMatrix( + atoms_a, atoms_b, number_of_atoms, rotation, None + ) # no weights rmsd_ref = 20.73219522556076 assert_almost_equal(rmsd_ref, rmsd) - rotation_ref = np.array([0.9977195, 0.02926979, 0.06082009, -.0310942, 0.9990878, 0.02926979, -0.05990789, -.0310942, 0.9977195]) + rotation_ref = np.array( + [ + 0.9977195, + 0.02926979, + 0.06082009, + -0.0310942, + 0.9990878, + 0.02926979, + -0.05990789, + -0.0310942, + 0.9977195, + ] + ) assert_array_almost_equal(rotation, rotation_ref, 6) - + def test_rmsd(atoms_a, atoms_b): - rotation_m = np.array([[.9977195, .02926979, .06082009], [-.0310942, .9990878, .02926979], [-.05990789, -.0310942, .9977195]]) + rotation_m = np.array( + [ + [0.9977195, 0.02926979, 0.06082009], + [-0.0310942, 0.9990878, 0.02926979], + [-0.05990789, -0.0310942, 0.9977195], + ] + ) atoms_b_aligned = np.dot(atoms_b, rotation_m) rmsd = rms.rmsd(atoms_b_aligned, atoms_a) rmsd_ref = 20.73219522556076 @@ -189,11 +240,25 @@ def test_rmsd(atoms_a, atoms_b): def test_weights(atoms_a, atoms_b): no_of_atoms = 4 - weights = np.array([1,2,3,4], dtype=np.float64) + weights = np.array([1, 2, 3, 4], dtype=np.float64) rotation = np.zeros(9, dtype=np.float64) - rmsd = qcp.CalcRMSDRotationalMatrix(atoms_a, atoms_b, no_of_atoms, rotation, weights) + rmsd = qcp.CalcRMSDRotationalMatrix( + atoms_a, atoms_b, no_of_atoms, rotation, weights + ) assert_almost_equal(rmsd, 32.798779202159416) - rotation_ref = np.array([0.99861395, .022982, .04735006, -.02409085, .99944556, .022982, -.04679564, -.02409085, .99861395]) + rotation_ref = np.array( + [ + 0.99861395, + 0.022982, + 0.04735006, + -0.02409085, + 0.99944556, + 0.022982, + -0.04679564, + -0.02409085, + 0.99861395, + ] + ) np.testing.assert_almost_equal(rotation_ref, rotation) diff --git a/testsuite/MDAnalysisTests/utils/test_selections.py b/testsuite/MDAnalysisTests/utils/test_selections.py index d271f2f09f6..212bce97dae 100644 --- a/testsuite/MDAnalysisTests/utils/test_selections.py +++ b/testsuite/MDAnalysisTests/utils/test_selections.py @@ -42,7 +42,9 @@ class _SelectionWriter(object): filename = None - max_number = 357 # to keep fixtures smallish, only select CAs up to number 357 + max_number = ( + 357 # to keep fixtures smallish, only select CAs up to number 357 + ) @staticmethod @pytest.fixture() @@ -54,7 +56,9 @@ def namedfile(self): return NamedStream(StringIO(), self.filename) def _selection(self, universe): - return universe.select_atoms("protein and name CA and bynum 1-{0}".format(self.max_number)) + return universe.select_atoms( + "protein and name CA and bynum 1-{0}".format(self.max_number) + ) def _write(self, universe, namedfile, **kwargs): g = self._selection(universe) @@ -72,9 +76,13 @@ def _write_with(self, universe, namedfile, **kwargs): outfile.write(g) return g - def test_write_bad_mode(self, universe, namedfile,): + def test_write_bad_mode( + self, + universe, + namedfile, + ): with pytest.raises(ValueError): - self._write(universe, namedfile, name=self.ref_name, mode='a+') + self._write(universe, namedfile, name=self.ref_name, mode="a+") def test_write(self, universe, namedfile): self._write(universe, namedfile, name=self.ref_name) @@ -105,18 +113,25 @@ class TestSelectionWriter_Gromacs(_SelectionWriter): filename = "CA.ndx" ref_name = "CA_selection" ref_indices = ndx2array( - [ '5 22 46 65 84 103 122 129 141 153 160 170 \n', - '177 199 206 220 237 247 264 284 303 320 335 357 \n', - ] - ) + [ + "5 22 46 65 84 103 122 129 141 153 160 170 \n", + "177 199 206 220 237 247 264 284 303 320 335 357 \n", + ] + ) def _assert_selectionstring(self, namedfile): header = namedfile.readline().strip() - assert_equal(header, "[ {0} ]".format(self.ref_name), - err_msg="NDX file has wrong selection name") + assert_equal( + header, + "[ {0} ]".format(self.ref_name), + err_msg="NDX file has wrong selection name", + ) indices = ndx2array(namedfile.readlines()) - assert_array_equal(indices, self.ref_indices, - err_msg="indices were not written correctly") + assert_array_equal( + indices, + self.ref_indices, + err_msg="indices were not written correctly", + ) class TestSelectionWriter_Charmm(_SelectionWriter): @@ -124,8 +139,9 @@ class TestSelectionWriter_Charmm(_SelectionWriter): writer = MDAnalysis.selections.charmm.SelectionWriter filename = "CA.str" ref_name = "CA_selection" - ref_selectionstring = lines2one([ - """! MDAnalysis CHARMM selection + ref_selectionstring = lines2one( + [ + """! MDAnalysis CHARMM selection DEFINE CA_selection SELECT - BYNUM 5 .or. BYNUM 22 .or. BYNUM 46 .or. BYNUM 65 .or. - BYNUM 84 .or. BYNUM 103 .or. BYNUM 122 .or. BYNUM 129 .or. - @@ -133,12 +149,17 @@ class TestSelectionWriter_Charmm(_SelectionWriter): BYNUM 177 .or. BYNUM 199 .or. BYNUM 206 .or. BYNUM 220 .or. - BYNUM 237 .or. BYNUM 247 .or. BYNUM 264 .or. BYNUM 284 .or. - BYNUM 303 .or. BYNUM 320 .or. BYNUM 335 .or. BYNUM 357 END - """]) + """ + ] + ) def _assert_selectionstring(self, namedfile): selectionstring = lines2one(namedfile.readlines()) - assert_equal(selectionstring, self.ref_selectionstring, - err_msg="Charmm selection was not written correctly") + assert_equal( + selectionstring, + self.ref_selectionstring, + err_msg="Charmm selection was not written correctly", + ) class TestSelectionWriter_PyMOL(_SelectionWriter): @@ -146,18 +167,24 @@ class TestSelectionWriter_PyMOL(_SelectionWriter): writer = MDAnalysis.selections.pymol.SelectionWriter filename = "CA.pml" ref_name = "CA_selection" - ref_selectionstring = lines2one([ - """# MDAnalysis PyMol selection\n select CA_selection, \\ + ref_selectionstring = lines2one( + [ + """# MDAnalysis PyMol selection\n select CA_selection, \\ index 5 | index 22 | index 46 | index 65 | index 84 | index 103 | \\ index 122 | index 129 | index 141 | index 153 | index 160 | index 170 | \\ index 177 | index 199 | index 206 | index 220 | index 237 | index 247 | \\ index 264 | index 284 | index 303 | index 320 | index 335 | index 357 - """]) + """ + ] + ) def _assert_selectionstring(self, namedfile): selectionstring = lines2one(namedfile.readlines()) - assert_equal(selectionstring, self.ref_selectionstring, - err_msg="PyMOL selection was not written correctly") + assert_equal( + selectionstring, + self.ref_selectionstring, + err_msg="PyMOL selection was not written correctly", + ) class TestSelectionWriter_VMD(_SelectionWriter): @@ -165,21 +192,27 @@ class TestSelectionWriter_VMD(_SelectionWriter): writer = MDAnalysis.selections.vmd.SelectionWriter filename = "CA.vmd" ref_name = "CA_selection" - ref_selectionstring = lines2one([ - """# MDAnalysis VMD selection atomselect macro CA_selection {index 4 21 45 64 83 102 121 128 \\ + ref_selectionstring = lines2one( + [ + """# MDAnalysis VMD selection atomselect macro CA_selection {index 4 21 45 64 83 102 121 128 \\ 140 152 159 169 176 198 205 219 \\ 236 246 263 283 302 319 334 356 } - """]) + """ + ] + ) def _assert_selectionstring(self, namedfile): selectionstring = lines2one(namedfile.readlines()) - assert_equal(selectionstring, self.ref_selectionstring, - err_msg="PyMOL selection was not written correctly") + assert_equal( + selectionstring, + self.ref_selectionstring, + err_msg="PyMOL selection was not written correctly", + ) def spt2array(line): """Get name of and convert Jmol SPT definition to integer array""" - match = re.search(r'\@~(\w+) \(\{([\d\s]*)\}\)', line) + match = re.search(r"\@~(\w+) \(\{([\d\s]*)\}\)", line) return match.group(1), np.array(match.group(2).split(), dtype=int) @@ -188,16 +221,22 @@ class TestSelectionWriter_Jmol(_SelectionWriter): writer = MDAnalysis.selections.jmol.SelectionWriter filename = "CA.spt" ref_name, ref_indices = spt2array( - ( '@~ca ({4 21 45 64 83 102 121 128 140 152 159 169 176 198 205 219 236' - ' 246 263 283 302 319 334 356});') + ( + "@~ca ({4 21 45 64 83 102 121 128 140 152 159 169 176 198 205 219 236" + " 246 263 283 302 319 334 356});" ) + ) def _assert_selectionstring(self, namedfile): header, indices = spt2array(namedfile.readline()) - assert_equal(header, self.ref_name, - err_msg="SPT file has wrong selection name") - assert_array_equal(indices, self.ref_indices, - err_msg="SPT indices were not written correctly") + assert_equal( + header, self.ref_name, err_msg="SPT file has wrong selection name" + ) + assert_array_equal( + indices, + self.ref_indices, + err_msg="SPT indices were not written correctly", + ) class TestSelections: diff --git a/testsuite/MDAnalysisTests/utils/test_streamio.py b/testsuite/MDAnalysisTests/utils/test_streamio.py index 53eb1a95c8e..56a421dda7f 100644 --- a/testsuite/MDAnalysisTests/utils/test_streamio.py +++ b/testsuite/MDAnalysisTests/utils/test_streamio.py @@ -20,13 +20,26 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -from os.path import abspath, basename, dirname, expanduser, normpath, relpath, split, splitext +from os.path import ( + abspath, + basename, + dirname, + expanduser, + normpath, + relpath, + split, + splitext, +) from io import StringIO import pytest import numpy as np -from numpy.testing import assert_equal, assert_almost_equal, assert_array_almost_equal +from numpy.testing import ( + assert_equal, + assert_almost_equal, + assert_array_almost_equal, +) import MDAnalysis @@ -77,7 +90,7 @@ class TestNamedStream(object): text = [ "The Jabberwock, with eyes of flame,\n", "Came whiffling through the tulgey wood,\n", - "And burbled as it came!" + "And burbled as it came!", ] textname = "jabberwock.txt" @@ -110,7 +123,7 @@ def test_StringIO_read(self): ns.close(force=True) def test_File_read(self): - obj = open(self.filename, 'r') + obj = open(self.filename, "r") ns = util.NamedStream(obj, self.filename) assert_equal(ns.name, self.filename) assert_equal(str(ns), self.filename) @@ -148,15 +161,17 @@ def test_File_write(self, tmpdir): def test_matryoshka(self): obj = StringIO() - ns = util.NamedStream(obj, 'r') + ns = util.NamedStream(obj, "r") with pytest.warns(RuntimeWarning): - ns2 = util.NamedStream(ns, 'f') + ns2 = util.NamedStream(ns, "f") assert not isinstance(ns2.stream, util.NamedStream) - assert ns2.name == 'f' + assert ns2.name == "f" class TestNamedStream_filename_behavior(object): - textname = os.path.join("~", "stories", "jabberwock.txt") # with tilde ~ to test regular expanduser() + textname = os.path.join( + "~", "stories", "jabberwock.txt" + ) # with tilde ~ to test regular expanduser() # note: no setUp() because classes with generators would run it # *for each generated test* and we need it for the generator method @@ -166,7 +181,9 @@ def create_NamedStream(self, name=None): obj = StringIO() return util.NamedStream(obj, name) - @pytest.mark.parametrize('func', ( + @pytest.mark.parametrize( + "func", + ( abspath, basename, dirname, @@ -174,8 +191,9 @@ def create_NamedStream(self, name=None): normpath, relpath, split, - splitext - )) + splitext, + ), + ) def test_func(self, func): # - "expandvars" gave Segmentation fault (OS X 10.6, Python 2.7.11 -- orbeckst) # - "expanduser" will either return a string if it carried out interpolation @@ -186,9 +204,13 @@ def test_func(self, func): fn = self.textname reference = func(fn) value = func(ns) - assert_equal(value, reference, - err_msg=("os.path.{0}() does not work with " - "NamedStream").format(func.__name__)) + assert_equal( + value, + reference, + err_msg=("os.path.{0}() does not work with " "NamedStream").format( + func.__name__ + ), + ) def test_join(self, tmpdir, funcname="join"): # join not included because of different call signature @@ -198,62 +220,86 @@ def test_join(self, tmpdir, funcname="join"): fn = self.textname reference = str(tmpdir.join(fn)) value = os.path.join(str(tmpdir), ns) - assert_equal(value, reference, - err_msg=("os.path.{0}() does not work with " - "NamedStream").format(funcname)) + assert_equal( + value, + reference, + err_msg=("os.path.{0}() does not work with " "NamedStream").format( + funcname + ), + ) def test_expanduser_noexpansion_returns_NamedStream(self): - ns = self.create_NamedStream("de/zipferlack.txt") # no tilde ~ in name! + ns = self.create_NamedStream( + "de/zipferlack.txt" + ) # no tilde ~ in name! reference = ns.name value = os.path.expanduser(ns) - assert_equal(value, reference, - err_msg=("os.path.expanduser() without '~' did not " - "return NamedStream --- weird!!")) - - @pytest.mark.skipif("HOME" not in os.environ, reason='It is needed') + assert_equal( + value, + reference, + err_msg=( + "os.path.expanduser() without '~' did not " + "return NamedStream --- weird!!" + ), + ) + + @pytest.mark.skipif("HOME" not in os.environ, reason="It is needed") def test_expandvars(self): name = "${HOME}/stories/jabberwock.txt" ns = self.create_NamedStream(name) reference = os.path.expandvars(name) value = os.path.expandvars(ns) - assert_equal(value, reference, - err_msg="os.path.expandvars() did not expand HOME") + assert_equal( + value, + reference, + err_msg="os.path.expandvars() did not expand HOME", + ) def test_expandvars_noexpansion_returns_NamedStream(self): - ns = self.create_NamedStream() # no $VAR constructs + ns = self.create_NamedStream() # no $VAR constructs reference = ns.name value = os.path.expandvars(ns) - assert_equal(value, reference, - err_msg=("os.path.expandvars() without '$VARS' did not " - "return NamedStream --- weird!!")) + assert_equal( + value, + reference, + err_msg=( + "os.path.expandvars() without '$VARS' did not " + "return NamedStream --- weird!!" + ), + ) def test_add(self): ns = self.create_NamedStream() try: assert_equal(ns + "foo", self.textname + "foo") except TypeError: - raise pytest.fail("NamedStream does not support " - "string concatenation, NamedStream + str") + raise pytest.fail( + "NamedStream does not support " + "string concatenation, NamedStream + str" + ) def test_radd(self): ns = self.create_NamedStream() try: assert_equal("foo" + ns, "foo" + self.textname) except TypeError: - raise pytest.fail("NamedStream does not support right " - "string concatenation, str + NamedStream") + raise pytest.fail( + "NamedStream does not support right " + "string concatenation, str + NamedStream" + ) class _StreamData(object): """Data for StreamIO functions.""" + filenames = { - 'PSF': datafiles.PSF, - 'CRD': datafiles.CRD, - 'PDB': datafiles.PDB_small, - 'PQR': datafiles.PQR, - 'GRO': datafiles.GRO_velocity, - 'MOL2': datafiles.mol2_molecules, - 'PDBQT': datafiles.PDBQT_input, + "PSF": datafiles.PSF, + "CRD": datafiles.CRD, + "PDB": datafiles.PDB_small, + "PQR": datafiles.PQR, + "GRO": datafiles.GRO_velocity, + "MOL2": datafiles.mol2_molecules, + "PDBQT": datafiles.PDBQT_input, } def __init__(self): @@ -261,8 +307,10 @@ def __init__(self): for name, fn in self.filenames.items(): with open(fn) as filed: self.buffers[name] = "".join(filed.readlines()) - self.filenames['XYZ_PSF'] = u"bogus/path/mini.psf" - self.buffers['XYZ_PSF'] = u"""\ + self.filenames["XYZ_PSF"] = "bogus/path/mini.psf" + self.buffers[ + "XYZ_PSF" + ] = """\ PSF CMAP 1 !NTITLE @@ -278,8 +326,10 @@ def __init__(self): 7 A 380 THR C C 0.510000 12.0110 0 8 A 380 THR O O -0.510000 15.9990 0 """ - self.filenames['XYZ'] = "bogus/path/mini.xyz" - self.buffers['XYZ'] = """\ + self.filenames["XYZ"] = "bogus/path/mini.xyz" + self.buffers[ + "XYZ" + ] = """\ 8 frame 1 N 0.93100 17.31800 16.42300 @@ -320,7 +370,7 @@ def as_NamedStream(self, name): return util.NamedStream(self.as_StringIO(name), self.filenames[name]) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def streamData(): return _StreamData() @@ -328,72 +378,97 @@ def streamData(): # possibly add tests to individual readers instead? class TestStreamIO(RefAdKSmall): def test_PrimitivePDBReader(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('PDB')) + u = MDAnalysis.Universe(streamData.as_NamedStream("PDB")) assert_equal(u.atoms.n_atoms, self.ref_n_atoms) def test_PDBReader(self, streamData): try: - u = MDAnalysis.Universe(streamData.as_NamedStream('PDB')) + u = MDAnalysis.Universe(streamData.as_NamedStream("PDB")) except Exception as err: raise pytest.fail("StreamIO not supported:\n>>>>> {0}".format(err)) assert_equal(u.atoms.n_atoms, self.ref_n_atoms) def test_CRDReader(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('CRD')) + u = MDAnalysis.Universe(streamData.as_NamedStream("CRD")) assert_equal(u.atoms.n_atoms, self.ref_n_atoms) def test_PSFParser(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('PSF')) + u = MDAnalysis.Universe(streamData.as_NamedStream("PSF")) assert_equal(u.atoms.n_atoms, self.ref_n_atoms) def test_PSF_CRD(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('PSF'), - streamData.as_NamedStream('CRD')) + u = MDAnalysis.Universe( + streamData.as_NamedStream("PSF"), streamData.as_NamedStream("CRD") + ) assert_equal(u.atoms.n_atoms, self.ref_n_atoms) def test_PQRReader(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('PQR')) + u = MDAnalysis.Universe(streamData.as_NamedStream("PQR")) assert_equal(u.atoms.n_atoms, self.ref_n_atoms) - assert_almost_equal(u.atoms.total_charge(), self.ref_charmm_totalcharge, 3, - "Total charge (in CHARMM) does not match expected value.") - assert_almost_equal(u.atoms.select_atoms('name H').charges, self.ref_charmm_Hcharges, 3, - "Charges for H atoms do not match.") + assert_almost_equal( + u.atoms.total_charge(), + self.ref_charmm_totalcharge, + 3, + "Total charge (in CHARMM) does not match expected value.", + ) + assert_almost_equal( + u.atoms.select_atoms("name H").charges, + self.ref_charmm_Hcharges, + 3, + "Charges for H atoms do not match.", + ) def test_PDBQTReader(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('PDBQT')) - sel = u.select_atoms('backbone') + u = MDAnalysis.Universe(streamData.as_NamedStream("PDBQT")) + sel = u.select_atoms("backbone") assert_equal(sel.n_atoms, 796) - sel = u.select_atoms('segid A') + sel = u.select_atoms("segid A") assert_equal(sel.n_atoms, 909, "failed to select segment A") - sel = u.select_atoms('segid B') + sel = u.select_atoms("segid B") assert_equal(sel.n_atoms, 896, "failed to select segment B") def test_GROReader(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('GRO')) + u = MDAnalysis.Universe(streamData.as_NamedStream("GRO")) assert_equal(u.atoms.n_atoms, 6) - assert_almost_equal(u.atoms[3].position, - 10. * np.array([1.275, 0.053, 0.622]), 3, # manually convert nm -> A - err_msg="wrong coordinates for water 2 OW") - assert_almost_equal(u.atoms[3].velocity, - 10. * np.array([0.2519, 0.3140, -0.1734]), 3, # manually convert nm/ps -> A/ps - err_msg="wrong velocity for water 2 OW") + assert_almost_equal( + u.atoms[3].position, + 10.0 * np.array([1.275, 0.053, 0.622]), + 3, # manually convert nm -> A + err_msg="wrong coordinates for water 2 OW", + ) + assert_almost_equal( + u.atoms[3].velocity, + 10.0 * np.array([0.2519, 0.3140, -0.1734]), + 3, # manually convert nm/ps -> A/ps + err_msg="wrong velocity for water 2 OW", + ) def test_MOL2Reader(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('MOL2')) + u = MDAnalysis.Universe(streamData.as_NamedStream("MOL2")) assert_equal(len(u.atoms), 49) assert_equal(u.trajectory.n_frames, 200) u.trajectory[199] - assert_array_almost_equal(u.atoms.positions[0], [1.7240, 11.2730, 14.1200]) + assert_array_almost_equal( + u.atoms.positions[0], [1.7240, 11.2730, 14.1200] + ) def test_XYZReader(self, streamData): - u = MDAnalysis.Universe(streamData.as_NamedStream('XYZ_PSF'), - streamData.as_NamedStream('XYZ')) + u = MDAnalysis.Universe( + streamData.as_NamedStream("XYZ_PSF"), + streamData.as_NamedStream("XYZ"), + ) assert_equal(len(u.atoms), 8) assert_equal(u.trajectory.n_frames, 3) - assert_equal(u.trajectory.frame, 0) # weird, something odd with XYZ reader + assert_equal( + u.trajectory.frame, 0 + ) # weird, something odd with XYZ reader u.trajectory.next() # (should really only need one next()... ) assert_equal(u.trajectory.frame, 1) # !!!! ??? u.trajectory.next() # frame 2 assert_equal(u.trajectory.frame, 2) - assert_almost_equal(u.atoms[2].position, np.array([0.45600, 18.48700, 16.26500]), 3, - err_msg="wrong coordinates for atom CA at frame 2") + assert_almost_equal( + u.atoms[2].position, + np.array([0.45600, 18.48700, 16.26500]), + 3, + err_msg="wrong coordinates for atom CA at frame 2", + ) diff --git a/testsuite/MDAnalysisTests/utils/test_transformations.py b/testsuite/MDAnalysisTests/utils/test_transformations.py index 8a3a4baec98..72d6831cd1f 100644 --- a/testsuite/MDAnalysisTests/utils/test_transformations.py +++ b/testsuite/MDAnalysisTests/utils/test_transformations.py @@ -24,8 +24,12 @@ import numpy as np import pytest -from numpy.testing import (assert_allclose, assert_equal, assert_almost_equal, - assert_array_equal) +from numpy.testing import ( + assert_allclose, + assert_equal, + assert_almost_equal, + assert_array_equal, +) from MDAnalysis.lib import transformations as t @@ -50,10 +54,7 @@ _ATOL = 1e-06 -@pytest.mark.parametrize('f', [ - t._py_identity_matrix, - t.identity_matrix -]) +@pytest.mark.parametrize("f", [t._py_identity_matrix, t.identity_matrix]) def test_identity_matrix(f): I = f() assert_allclose(I, np.dot(I, I)) @@ -61,10 +62,13 @@ def test_identity_matrix(f): assert_allclose(I, np.identity(4, dtype=np.float64)) -@pytest.mark.parametrize('f', [ - t._py_translation_matrix, - t.translation_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_translation_matrix, + t.translation_matrix, + ], +) def test_translation_matrix(f): v = np.array([0.2, 0.2, 0.2]) assert_allclose(v, f(v)[:3, 3]) @@ -77,15 +81,18 @@ def test_translation_from_matrix(): assert_allclose(v0, v1) -@pytest.mark.parametrize('f', [ - t._py_reflection_matrix, - t.reflection_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_reflection_matrix, + t.reflection_matrix, + ], +) def test_reflection_matrix(f): v0 = np.array([0.2, 0.2, 0.2, 1.0]) # arbitrary values v1 = np.array([0.4, 0.4, 0.4]) R = f(v0, v1) - assert_allclose(2., np.trace(R)) + assert_allclose(2.0, np.trace(R)) assert_allclose(v0, np.dot(R, v0)) v2 = v0.copy() v2[:3] += v1 @@ -103,13 +110,16 @@ def test_reflection_from_matrix(): assert_equal(t.is_same_transform(M0, M1), True) -@pytest.mark.parametrize('f', [ - t._py_rotation_matrix, - t.rotation_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_rotation_matrix, + t.rotation_matrix, + ], +) def test_rotation_matrix(f): R = f(np.pi / 2.0, [0, 0, 1], [1, 0, 0]) - assert_allclose(np.dot(R, [0, 0, 0, 1]), [1., -1., 0., 1.]) + assert_allclose(np.dot(R, [0, 0, 0, 1]), [1.0, -1.0, 0.0, 1.0]) angle = 0.2 * 2 * np.pi # arbitrary value direc = np.array([0.2, 0.2, 0.2]) point = np.array([0.4, 0.4, 0.4]) @@ -121,7 +131,7 @@ def test_rotation_matrix(f): assert_equal(t.is_same_transform(R0, R1), True) I = np.identity(4, np.float64) assert_allclose(I, f(np.pi * 2, direc), atol=_ATOL) - assert_allclose(2., np.trace(f(np.pi / 2, direc, point))) + assert_allclose(2.0, np.trace(f(np.pi / 2, direc, point))) def test_rotation_from_matrix(): @@ -134,10 +144,13 @@ def test_rotation_from_matrix(): assert_equal(t.is_same_transform(R0, R1), True) -@pytest.mark.parametrize('f', [ - t._py_scale_matrix, - t.scale_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_scale_matrix, + t.scale_matrix, + ], +) def test_scale_matrix(f): v = np.array([14.1, 15.1, 16.1, 1]) S = f(-1.234) @@ -157,10 +170,14 @@ def test_scale_from_matrix(): S1 = t.scale_matrix(factor, origin, direction) assert_equal(t.is_same_transform(S0, S1), True) -@pytest.mark.parametrize('f', [ - t._py_projection_matrix, - t.projection_matrix, -]) + +@pytest.mark.parametrize( + "f", + [ + t._py_projection_matrix, + t.projection_matrix, + ], +) class TestProjectionMatrix(object): def test_projection_matrix_1(self, f): P = f((0, 0, 0), (1, 0, 0)) @@ -187,7 +204,6 @@ def test_projection_matrix_3(self, f): assert_allclose(v1[0], 3.0 - v1[1], atol=_ATOL) - class TestProjectionFromMatrix(object): @staticmethod @pytest.fixture() @@ -215,25 +231,27 @@ def test_projection_from_matrix_2(self, data): def test_projection_from_matrix_3(self, data): point, normal, direct, persp = data P0 = t.projection_matrix( - point, normal, perspective=persp, pseudo=False) + point, normal, perspective=persp, pseudo=False + ) result = t.projection_from_matrix(P0, pseudo=False) P1 = t.projection_matrix(*result) assert_equal(t.is_same_transform(P0, P1), True) def test_projection_from_matrix_4(self, data): point, normal, direct, persp = data - P0 = t.projection_matrix( - point, normal, perspective=persp, pseudo=True) + P0 = t.projection_matrix(point, normal, perspective=persp, pseudo=True) result = t.projection_from_matrix(P0, pseudo=True) P1 = t.projection_matrix(*result) assert_equal(t.is_same_transform(P0, P1), True) - -@pytest.mark.parametrize('f', [ - t._py_clip_matrix, - t.clip_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_clip_matrix, + t.clip_matrix, + ], +) class TestClipMatrix(object): def test_clip_matrix_1(self, f): frustrum = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) # arbitrary values @@ -243,10 +261,12 @@ def test_clip_matrix_1(self, f): M = f(perspective=False, *frustrum) assert_allclose( np.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0]), - np.array([-1., -1., -1., 1.])) + np.array([-1.0, -1.0, -1.0, 1.0]), + ) assert_allclose( np.dot(M, [frustrum[1], frustrum[3], frustrum[5], 1.0]), - np.array([1., 1., 1., 1.])) + np.array([1.0, 1.0, 1.0, 1.0]), + ) def test_clip_matrix_2(self, f): frustrum = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) # arbitrary values @@ -255,33 +275,36 @@ def test_clip_matrix_2(self, f): frustrum[5] += frustrum[4] M = f(perspective=True, *frustrum) v = np.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0]) - assert_allclose(v / v[3], np.array([-1., -1., -1., 1.])) + assert_allclose(v / v[3], np.array([-1.0, -1.0, -1.0, 1.0])) v = np.dot(M, [frustrum[1], frustrum[3], frustrum[4], 1.0]) - assert_allclose(v / v[3], np.array([1., 1., -1., 1.])) + assert_allclose(v / v[3], np.array([1.0, 1.0, -1.0, 1.0])) def test_clip_matrix_frustrum_left_right_bounds(self, f): - '''ValueError should be raised if left > right.''' + """ValueError should be raised if left > right.""" frustrum = np.array([0.4, 0.3, 0.3, 0.7, 0.5, 1.1]) with pytest.raises(ValueError): f(*frustrum) def test_clip_matrix_frustrum_bottom_top_bounds(self, f): - '''ValueError should be raised if bottom > top.''' + """ValueError should be raised if bottom > top.""" frustrum = np.array([0.1, 0.3, 0.71, 0.7, 0.5, 1.1]) with pytest.raises(ValueError): f(*frustrum) def test_clip_matrix_frustrum_near_far_bounds(self, f): - '''ValueError should be raised if near > far.''' + """ValueError should be raised if near > far.""" frustrum = np.array([0.1, 0.3, 0.3, 0.7, 1.5, 1.1]) with pytest.raises(ValueError): f(*frustrum) -@pytest.mark.parametrize('f', [ - t._py_shear_matrix, - t.shear_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_shear_matrix, + t.shear_matrix, + ], +) def test_shear_matrix(f): angle = 0.2 * 4 * np.pi # arbitrary values direct = np.array([0.2, 0.2, 0.2]) @@ -345,13 +368,16 @@ def test_compose_matrix(): assert_equal(t.is_same_transform(M0, M1), True) -@pytest.mark.parametrize('f', [ - t._py_orthogonalization_matrix, - t.orthogonalization_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_orthogonalization_matrix, + t.orthogonalization_matrix, + ], +) class TestOrthogonalizationMatrix(object): def test_orthogonalization_matrix_1(self, f): - O = f((10., 10., 10.), (90., 90., 90.)) + O = f((10.0, 10.0, 10.0), (90.0, 90.0, 90.0)) assert_allclose(O[:3, :3], np.identity(3, float) * 10, atol=_ATOL) def test_orthogonalization_matrix_2(self, f): @@ -359,10 +385,13 @@ def test_orthogonalization_matrix_2(self, f): assert_allclose(np.sum(O), 43.063229, atol=_ATOL) -@pytest.mark.parametrize('f', [ - t._py_superimposition_matrix, - t.superimposition_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_superimposition_matrix, + t.superimposition_matrix, + ], +) def test_superimposition_matrix(f): v0 = np.sin(np.linspace(0, 0.99, 30)).reshape(3, 10) # arbitrary values M = f(v0, v0) @@ -397,13 +426,16 @@ def test_superimposition_matrix(f): assert_allclose(v1, np.dot(M, v[:, :, 0]), atol=_ATOL) -@pytest.mark.parametrize('f', [ - t._py_euler_matrix, - t.euler_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_euler_matrix, + t.euler_matrix, + ], +) class TestEulerMatrix(object): def test_euler_matrix_1(self, f): - R = f(1, 2, 3, 'syxz') + R = f(1, 2, 3, "syxz") assert_allclose(np.sum(R[0]), -1.34786452) def test_euler_matrix_2(self, f): @@ -411,15 +443,18 @@ def test_euler_matrix_2(self, f): assert_allclose(np.sum(R[0]), -0.383436184) -@pytest.mark.parametrize('f', [ - t._py_euler_from_matrix, - t.euler_from_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_euler_from_matrix, + t.euler_from_matrix, + ], +) class TestEulerFromMatrix(object): def test_euler_from_matrix_1(self, f): - R0 = t.euler_matrix(1, 2, 3, 'syxz') - al, be, ga = f(R0, 'syxz') - R1 = t.euler_matrix(al, be, ga, 'syxz') + R0 = t.euler_matrix(1, 2, 3, "syxz") + al, be, ga = f(R0, "syxz") + R1 = t.euler_matrix(al, be, ga, "syxz") assert_allclose(R0, R1) def test_euler_from_matrix_2(self, f): @@ -435,28 +470,37 @@ def test_euler_from_quaternion(): assert_allclose(angles, [0.123, 0, 0], atol=_ATOL) -@pytest.mark.parametrize('f', [ - t._py_quaternion_from_euler, - t.quaternion_from_euler, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_from_euler, + t.quaternion_from_euler, + ], +) def test_quaternion_from_euler(f): - q = f(1, 2, 3, 'ryxz') + q = f(1, 2, 3, "ryxz") assert_allclose(q, [0.435953, 0.310622, -0.718287, 0.444435], atol=_ATOL) -@pytest.mark.parametrize('f', [ - t._py_quaternion_about_axis, - t.quaternion_about_axis, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_about_axis, + t.quaternion_about_axis, + ], +) def test_quaternion_about_axis(f): q = f(0.123, (1, 0, 0)) assert_allclose(q, [0.99810947, 0.06146124, 0, 0], atol=_ATOL) -@pytest.mark.parametrize('f', [ - t._py_quaternion_matrix, - t.quaternion_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_matrix, + t.quaternion_matrix, + ], +) class TestQuaternionMatrix(object): def test_quaternion_matrix_1(self, f): M = f([0.99810947, 0.06146124, 0, 0]) @@ -471,40 +515,53 @@ def test_quaternion_matrix_3(self, f): assert_allclose(M, np.diag([1, -1, -1, 1]), atol=_ATOL) -@pytest.mark.parametrize('f', [ - t._py_quaternion_from_matrix, - t.quaternion_from_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_from_matrix, + t.quaternion_from_matrix, + ], +) class TestQuaternionFromMatrix(object): def test_quaternion_from_matrix_1(self, f): q = f(t.identity_matrix(), True) - assert_allclose(q, [1., 0., 0., 0.], atol=_ATOL) + assert_allclose(q, [1.0, 0.0, 0.0, 0.0], atol=_ATOL) def test_quaternion_from_matrix_2(self, f): - q = f(np.diag([1., -1., -1., 1.])) - check = (np.allclose( - q, [0, 1, 0, 0], atol=_ATOL) or np.allclose( - q, [0, -1, 0, 0], atol=_ATOL)) + q = f(np.diag([1.0, -1.0, -1.0, 1.0])) + check = np.allclose(q, [0, 1, 0, 0], atol=_ATOL) or np.allclose( + q, [0, -1, 0, 0], atol=_ATOL + ) assert_equal(check, True) def test_quaternion_from_matrix_3(self, f): R = t.rotation_matrix(0.123, (1, 2, 3)) q = f(R, True) assert_allclose( - q, [0.9981095, 0.0164262, 0.0328524, 0.0492786], atol=_ATOL) + q, [0.9981095, 0.0164262, 0.0328524, 0.0492786], atol=_ATOL + ) def test_quaternion_from_matrix_4(self, f): - R = [[-0.545, 0.797, 0.260, 0], [0.733, 0.603, -0.313, 0], - [-0.407, 0.021, -0.913, 0], [0, 0, 0, 1]] + R = [ + [-0.545, 0.797, 0.260, 0], + [0.733, 0.603, -0.313, 0], + [-0.407, 0.021, -0.913, 0], + [0, 0, 0, 1], + ] q = f(R) assert_allclose(q, [0.19069, 0.43736, 0.87485, -0.083611], atol=_ATOL) def test_quaternion_from_matrix_5(self, f): - R = [[0.395, 0.362, 0.843, 0], [-0.626, 0.796, -0.056, 0], - [-0.677, -0.498, 0.529, 0], [0, 0, 0, 1]] + R = [ + [0.395, 0.362, 0.843, 0], + [-0.626, 0.796, -0.056, 0], + [-0.677, -0.498, 0.529, 0], + [0, 0, 0, 1], + ] q = f(R) assert_allclose( - q, [0.82336615, -0.13610694, 0.46344705, -0.29792603], atol=_ATOL) + q, [0.82336615, -0.13610694, 0.46344705, -0.29792603], atol=_ATOL + ) def test_quaternion_from_matrix_6(self, f): R = t.random_rotation_matrix() @@ -512,19 +569,25 @@ def test_quaternion_from_matrix_6(self, f): assert_equal(t.is_same_transform(R, t.quaternion_matrix(q)), True) -@pytest.mark.parametrize('f', [ - t._py_quaternion_multiply, - t.quaternion_multiply, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_multiply, + t.quaternion_multiply, + ], +) def test_quaternion_multiply(f): q = f([4, 1, -2, 3], [8, -5, 6, 7]) assert_allclose(q, [28, -44, -14, 48]) -@pytest.mark.parametrize('f', [ - t._py_quaternion_conjugate, - t.quaternion_conjugate, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_conjugate, + t.quaternion_conjugate, + ], +) def test_quaternion_conjugate(f): q0 = t.random_quaternion() q1 = f(q0) @@ -532,10 +595,13 @@ def test_quaternion_conjugate(f): assert_equal(check, True) -@pytest.mark.parametrize('f', [ - t._py_quaternion_inverse, - t.quaternion_inverse, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_inverse, + t.quaternion_inverse, + ], +) def test_quaternion_inverse(f): q0 = t.random_quaternion() q1 = f(q0) @@ -550,10 +616,13 @@ def test_quaternion_imag(): assert_allclose(t.quaternion_imag([3.0, 0.0, 1.0, 2.0]), [0.0, 1.0, 2.0]) -@pytest.mark.parametrize('f', [ - t._py_quaternion_slerp, - t.quaternion_slerp, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_quaternion_slerp, + t.quaternion_slerp, + ], +) def test_quaternion_slerp(f): q0 = t.random_quaternion() q1 = t.random_quaternion() @@ -566,16 +635,20 @@ def test_quaternion_slerp(f): q = f(q0, q1, 0.5) angle = np.arccos(np.dot(q0, q)) - check = (np.allclose(2.0, np.arccos(np.dot(q0, q1)) / angle) or - np.allclose(2.0, np.arccos(-np.dot(q0, q1)) / angle)) + check = np.allclose(2.0, np.arccos(np.dot(q0, q1)) / angle) or np.allclose( + 2.0, np.arccos(-np.dot(q0, q1)) / angle + ) assert_equal(check, True) -@pytest.mark.parametrize('f', [ - t._py_random_quaternion, - t.random_quaternion, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_random_quaternion, + t.random_quaternion, + ], +) class TestRandomQuaternion(object): def test_random_quaternion_1(self, f): q = f() @@ -587,21 +660,27 @@ def test_random_quaternion_2(self, f): assert_equal(q.shape[0] == 4, True) -@pytest.mark.parametrize('f', [ - t._py_random_rotation_matrix, - t.random_rotation_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_random_rotation_matrix, + t.random_rotation_matrix, + ], +) def test_random_rotation_matrix(f): R = f() assert_allclose(np.dot(R.T, R), np.identity(4), atol=_ATOL) -@pytest.mark.parametrize('f', [ - t._py_inverse_matrix, - t.inverse_matrix, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_inverse_matrix, + t.inverse_matrix, + ], +) class TestInverseMatrix(object): - @pytest.mark.parametrize('size', list(range(1, 7))) + @pytest.mark.parametrize("size", list(range(1, 7))) def test_inverse(self, size, f): # Create a known random state to generate numbers from # these numbers will then be uncorrelated but deterministic @@ -616,10 +695,13 @@ def test_inverse_matrix(self, f): assert_allclose(M1, np.linalg.inv(M0.T)) -@pytest.mark.parametrize('f', [ - t._py_is_same_transform, - t.is_same_transform, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_is_same_transform, + t.is_same_transform, + ], +) class TestIsSameTransform(object): def test_is_same_transform_1(self, f): assert_equal(f(np.identity(4), np.identity(4)), True) @@ -628,10 +710,13 @@ def test_is_same_transform_2(self, f): assert_equal(f(t.random_rotation_matrix(), np.identity(4)), False) -@pytest.mark.parametrize('f', [ - t._py_random_vector, - t.random_vector, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_random_vector, + t.random_vector, + ], +) class TestRandomVector(object): def test_random_vector_1(self, f): v = f(1000) @@ -644,10 +729,13 @@ def test_random_vector_2(self, f): assert_equal(np.any(v0 == v1), False) -@pytest.mark.parametrize('f', [ - t._py_unit_vector, - t.unit_vector, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_unit_vector, + t.unit_vector, + ], +) class TestUnitVector(object): def test_unit_vector_1(self, f): v0 = np.array([0.2, 0.2, 0.2]) @@ -680,10 +768,13 @@ def test_unit_vector_6(self, f): assert_equal(list(f([1.0])), [1.0]) -@pytest.mark.parametrize('f', [ - t._py_vector_norm, - t.vector_norm, -]) +@pytest.mark.parametrize( + "f", + [ + t._py_vector_norm, + t.vector_norm, + ], +) class TestVectorNorm(object): def test_vector_norm_1(self, f): v = np.array([0.2, 0.2, 0.2]) @@ -743,9 +834,13 @@ def test_rotaxis_equal_vectors(): def test_rotaxis_different_vectors(): # use random coordinate system e = np.eye(3) - r = np.array([[0.69884766, 0.59804425, -0.39237102], - [0.18784672, 0.37585347, 0.90744023], - [0.69016342, -0.7078681, 0.15032367]]) + r = np.array( + [ + [0.69884766, 0.59804425, -0.39237102], + [0.18784672, 0.37585347, 0.90744023], + [0.69016342, -0.7078681, 0.15032367], + ] + ) re = np.dot(r, e) for i, j, l in permutations(range(3)): diff --git a/testsuite/MDAnalysisTests/utils/test_units.py b/testsuite/MDAnalysisTests/utils/test_units.py index 7789df13597..8121188c1c6 100644 --- a/testsuite/MDAnalysisTests/utils/test_units.py +++ b/testsuite/MDAnalysisTests/utils/test_units.py @@ -31,15 +31,17 @@ class TestUnitEncoding(object): def test_unicode(self): try: - assert_equal(units.lengthUnit_factor[u"\u212b"], 1.0) + assert_equal(units.lengthUnit_factor["\u212b"], 1.0) except KeyError: raise AssertionError("Unicode symbol for Angtrom not supported") def test_unicode_encoding_with_symbol(self): try: - assert_equal(units.lengthUnit_factor[u"Å"], 1.0) + assert_equal(units.lengthUnit_factor["Å"], 1.0) except KeyError: - raise AssertionError("UTF-8-encoded symbol for Angtrom not supported") + raise AssertionError( + "UTF-8-encoded symbol for Angtrom not supported" + ) class TestConstants(object): @@ -48,91 +50,111 @@ class TestConstants(object): # Add a reference value to this dict for every entry in # units.constants constants_reference = ( - ('N_Avogadro', 6.02214129e+23), # mol**-1 - ('elementary_charge', 1.602176565e-19), # As - ('calorie', 4.184), # J - ('Boltzmann_constant', 8.314462159e-3), # KJ (mol K)**-1 - ('Boltzman_constant', 8.314462159e-3), # remove in 2.8.0 - ('electric_constant', 5.526350e-3), # As (Angstroms Volts)**-1 + ("N_Avogadro", 6.02214129e23), # mol**-1 + ("elementary_charge", 1.602176565e-19), # As + ("calorie", 4.184), # J + ("Boltzmann_constant", 8.314462159e-3), # KJ (mol K)**-1 + ("Boltzman_constant", 8.314462159e-3), # remove in 2.8.0 + ("electric_constant", 5.526350e-3), # As (Angstroms Volts)**-1 ) - @pytest.mark.parametrize('name, value', constants_reference) + @pytest.mark.parametrize("name, value", constants_reference) def test_constant(self, name, value): assert_almost_equal(units.constants[name], value) def test_boltzmann_typo_deprecation(self): - wmsg = ("Please use 'Boltzmann_constant' henceforth. The key " - "'Boltzman_constant' was a typo and will be removed " - "in MDAnalysis 2.8.0.") + wmsg = ( + "Please use 'Boltzmann_constant' henceforth. The key " + "'Boltzman_constant' was a typo and will be removed " + "in MDAnalysis 2.8.0." + ) with pytest.warns(DeprecationWarning, match=wmsg): - units.constants['Boltzman_constant'] + units.constants["Boltzman_constant"] class TestConversion(object): @staticmethod def _assert_almost_equal_convert(value, u1, u2, ref): val = units.convert(value, u1, u2) - assert_almost_equal(val, ref, - err_msg="Conversion {0} --> {1} failed".format(u1, u2)) + assert_almost_equal( + val, ref, err_msg="Conversion {0} --> {1} failed".format(u1, u2) + ) nm = 12.34567 - A = nm * 10. - @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( - (nm, 'nm', 'A', A), - (A, 'Angstrom', 'nm', nm), - )) + A = nm * 10.0 + + @pytest.mark.parametrize( + "quantity, unit1, unit2, ref", + ( + (nm, "nm", "A", A), + (A, "Angstrom", "nm", nm), + ), + ) def test_length(self, quantity, unit1, unit2, ref): self._assert_almost_equal_convert(quantity, unit1, unit2, ref) - @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( - (1, 'ps', 'AKMA', 20.45482949774598), - (1, 'AKMA', 'ps', 0.04888821), - (1, 'ps', 'ms', 1e-9), - (1, 'ms', 'ps', 1e9), - (1, 'ps', 'us', 1e-6), - (1, 'us', 'ps', 1e6), - )) + @pytest.mark.parametrize( + "quantity, unit1, unit2, ref", + ( + (1, "ps", "AKMA", 20.45482949774598), + (1, "AKMA", "ps", 0.04888821), + (1, "ps", "ms", 1e-9), + (1, "ms", "ps", 1e9), + (1, "ps", "us", 1e-6), + (1, "us", "ps", 1e6), + ), + ) def test_time(self, quantity, unit1, unit2, ref): self._assert_almost_equal_convert(quantity, unit1, unit2, ref) - @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( - (1, 'kcal/mol', 'kJ/mol', 4.184), - (1, 'kcal/mol', 'eV', 0.0433641), - )) + @pytest.mark.parametrize( + "quantity, unit1, unit2, ref", + ( + (1, "kcal/mol", "kJ/mol", 4.184), + (1, "kcal/mol", "eV", 0.0433641), + ), + ) def test_energy(self, quantity, unit1, unit2, ref): self._assert_almost_equal_convert(quantity, unit1, unit2, ref) - @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( - (1, 'kJ/(mol*A)', 'J/m', 1.66053892103219e-11), - (2.5, 'kJ/(mol*nm)', 'kJ/(mol*A)', 0.25), - (1, 'kcal/(mol*Angstrom)', 'kJ/(mol*Angstrom)', 4.184), - )) + @pytest.mark.parametrize( + "quantity, unit1, unit2, ref", + ( + (1, "kJ/(mol*A)", "J/m", 1.66053892103219e-11), + (2.5, "kJ/(mol*nm)", "kJ/(mol*A)", 0.25), + (1, "kcal/(mol*Angstrom)", "kJ/(mol*Angstrom)", 4.184), + ), + ) def test_force(self, quantity, unit1, unit2, ref): self._assert_almost_equal_convert(quantity, unit1, unit2, ref) - @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( - (1, 'A/ps', 'm/s', 1e-10/1e-12), - (1, 'A/ps', 'nm/ps', 0.1), - (1, 'A/ps', 'pm/ps', 1e2), - (1, 'A/ms', 'A/ps', 1e9), - (1, 'A/us', 'A/ps', 1e6), - (1, 'A/fs', 'A/ps', 1e-3), - (1, 'A/AKMA', 'A/ps', 1/4.888821e-2), - )) + @pytest.mark.parametrize( + "quantity, unit1, unit2, ref", + ( + (1, "A/ps", "m/s", 1e-10 / 1e-12), + (1, "A/ps", "nm/ps", 0.1), + (1, "A/ps", "pm/ps", 1e2), + (1, "A/ms", "A/ps", 1e9), + (1, "A/us", "A/ps", 1e6), + (1, "A/fs", "A/ps", 1e-3), + (1, "A/AKMA", "A/ps", 1 / 4.888821e-2), + ), + ) def test_speed(self, quantity, unit1, unit2, ref): self._assert_almost_equal_convert(quantity, unit1, unit2, ref) - @pytest.mark.parametrize('quantity, unit1, unit2', ((nm, 'Stone', 'nm'), - (nm, 'nm', 'Stone'))) + @pytest.mark.parametrize( + "quantity, unit1, unit2", ((nm, "Stone", "nm"), (nm, "nm", "Stone")) + ) def test_unit_unknown(self, quantity, unit1, unit2): with pytest.raises(ValueError): units.convert(quantity, unit1, unit2) def test_unit_unconvertable(self): nm = 12.34567 - A = nm * 10. + A = nm * 10.0 with pytest.raises(ValueError): - units.convert(A, 'A', 'ps') + units.convert(A, "A", "ps") class TestBaseUnits: @@ -141,12 +163,14 @@ class TestBaseUnits: def ref(): # This is a copy of the dictionary we expect. # We want to know if base units are added or altered. - ref = {"length": "A", - "time": "ps", - "energy": "kJ/mol", - "charge": "e", - "force": "kJ/(mol*A)", - "speed": "A/ps"} + ref = { + "length": "A", + "time": "ps", + "energy": "kJ/mol", + "charge": "e", + "force": "kJ/(mol*A)", + "speed": "A/ps", + } return ref def test_MDANALYSIS_BASE_UNITS_correct(self, ref): diff --git a/testsuite/MDAnalysisTests/visualization/test_streamlines.py b/testsuite/MDAnalysisTests/visualization/test_streamlines.py index 767903c74ad..437ccfd97e7 100644 --- a/testsuite/MDAnalysisTests/visualization/test_streamlines.py +++ b/testsuite/MDAnalysisTests/visualization/test_streamlines.py @@ -23,8 +23,7 @@ import numpy as np from numpy.testing import assert_allclose import MDAnalysis -from MDAnalysis.visualization import (streamlines, - streamlines_3D) +from MDAnalysis.visualization import streamlines, streamlines_3D from MDAnalysis.coordinates.XTC import XTCWriter from MDAnalysisTests.datafiles import Martini_membrane_gro import pytest @@ -32,50 +31,69 @@ import matplotlib.pyplot as plt import os + @pytest.fixture(scope="session") def univ(): u = MDAnalysis.Universe(Martini_membrane_gro) return u + @pytest.fixture(scope="session") def membrane_xtc(tmpdir_factory, univ): - x_delta, y_delta, z_delta = 0.5, 0.3, 0.2 - tmp_xtc = tmpdir_factory.mktemp('streamlines').join('dummy.xtc') + x_delta, y_delta, z_delta = 0.5, 0.3, 0.2 + tmp_xtc = tmpdir_factory.mktemp("streamlines").join("dummy.xtc") with XTCWriter(str(tmp_xtc), n_atoms=univ.atoms.n_atoms) as xtc_writer: for i in range(5): - univ.atoms.translate([x_delta, y_delta, z_delta]) - xtc_writer.write(univ.atoms) - x_delta += 0.1 - y_delta += 0.08 - z_delta += 0.02 + univ.atoms.translate([x_delta, y_delta, z_delta]) + xtc_writer.write(univ.atoms) + x_delta += 0.1 + y_delta += 0.08 + z_delta += 0.02 return str(tmp_xtc) + def test_streamplot_2D(membrane_xtc, univ): # regression test the data structures # generated by the 2D streamplot code - u1, v1, avg, std = streamlines.generate_streamlines(topology_file_path=Martini_membrane_gro, - trajectory_file_path=membrane_xtc, - grid_spacing=20, - MDA_selection='name PO4', - start_frame=1, - end_frame=2, - xmin=univ.atoms.positions[...,0].min(), - xmax=univ.atoms.positions[...,0].max(), - ymin=univ.atoms.positions[...,1].min(), - ymax=univ.atoms.positions[...,1].max(), - maximum_delta_magnitude=2.0, - num_cores=1) - assert_allclose(u1, np.array([[0.79999924, 0.79999924, 0.80000687, 0.79999542, 0.79998779], - [0.80000019, 0.79999542, 0.79999924, 0.79999542, 0.80001068], - [0.8000021, 0.79999924, 0.80001068, 0.80000305, 0.79999542], - [0.80000019, 0.79999542, 0.80001068, 0.80000305, 0.80000305], - [0.79999828, 0.80000305, 0.80000305, 0.80000305, 0.79999542]])) - assert_allclose(v1, np.array([[0.53999901, 0.53999996, 0.53999996, 0.53999996, 0.54000092], - [0.5399971, 0.54000092, 0.54000092, 0.54000092, 0.5399971 ], - [0.54000473, 0.54000473, 0.54000092, 0.5399971, 0.54000473], - [0.54000092, 0.53999329, 0.53999329, 0.53999329, 0.54000092], - [0.54000092, 0.53999329, 0.53999329, 0.54000092, 0.53999329]])) + u1, v1, avg, std = streamlines.generate_streamlines( + topology_file_path=Martini_membrane_gro, + trajectory_file_path=membrane_xtc, + grid_spacing=20, + MDA_selection="name PO4", + start_frame=1, + end_frame=2, + xmin=univ.atoms.positions[..., 0].min(), + xmax=univ.atoms.positions[..., 0].max(), + ymin=univ.atoms.positions[..., 1].min(), + ymax=univ.atoms.positions[..., 1].max(), + maximum_delta_magnitude=2.0, + num_cores=1, + ) + assert_allclose( + u1, + np.array( + [ + [0.79999924, 0.79999924, 0.80000687, 0.79999542, 0.79998779], + [0.80000019, 0.79999542, 0.79999924, 0.79999542, 0.80001068], + [0.8000021, 0.79999924, 0.80001068, 0.80000305, 0.79999542], + [0.80000019, 0.79999542, 0.80001068, 0.80000305, 0.80000305], + [0.79999828, 0.80000305, 0.80000305, 0.80000305, 0.79999542], + ] + ), + ) + assert_allclose( + v1, + np.array( + [ + [0.53999901, 0.53999996, 0.53999996, 0.53999996, 0.54000092], + [0.5399971, 0.54000092, 0.54000092, 0.54000092, 0.5399971], + [0.54000473, 0.54000473, 0.54000092, 0.5399971, 0.54000473], + [0.54000092, 0.53999329, 0.53999329, 0.53999329, 0.54000092], + [0.54000092, 0.53999329, 0.53999329, 0.54000092, 0.53999329], + ] + ), + ) assert avg == pytest.approx(0.965194167) assert std == pytest.approx(4.444808820e-06) @@ -84,18 +102,20 @@ def test_streamplot_2D_zero_return(membrane_xtc, univ, tmpdir): # simple roundtrip test to ensure that # zeroed arrays are returned by the 2D streamplot # code when called with an empty selection - u1, v1, avg, std = streamlines.generate_streamlines(topology_file_path=Martini_membrane_gro, - trajectory_file_path=membrane_xtc, - grid_spacing=20, - MDA_selection='name POX', - start_frame=1, - end_frame=2, - xmin=univ.atoms.positions[...,0].min(), - xmax=univ.atoms.positions[...,0].max(), - ymin=univ.atoms.positions[...,1].min(), - ymax=univ.atoms.positions[...,1].max(), - maximum_delta_magnitude=2.0, - num_cores=1) + u1, v1, avg, std = streamlines.generate_streamlines( + topology_file_path=Martini_membrane_gro, + trajectory_file_path=membrane_xtc, + grid_spacing=20, + MDA_selection="name POX", + start_frame=1, + end_frame=2, + xmin=univ.atoms.positions[..., 0].min(), + xmax=univ.atoms.positions[..., 0].max(), + ymin=univ.atoms.positions[..., 1].min(), + ymax=univ.atoms.positions[..., 1].max(), + maximum_delta_magnitude=2.0, + num_cores=1, + ) assert_allclose(u1, np.zeros((5, 5))) assert_allclose(v1, np.zeros((5, 5))) assert avg == approx(0.0) @@ -107,20 +127,22 @@ def test_streamplot_3D(membrane_xtc, univ, tmpdir): # for a roundtrip plotting test, simply # aim to check for sensible values # returned by generate_streamlines_3d - dx, dy, dz = streamlines_3D.generate_streamlines_3d(topology_file_path=Martini_membrane_gro, - trajectory_file_path=membrane_xtc, - grid_spacing=20, - MDA_selection='name PO4', - start_frame=1, - end_frame=2, - xmin=univ.atoms.positions[...,0].min(), - xmax=univ.atoms.positions[...,0].max(), - ymin=univ.atoms.positions[...,1].min(), - ymax=univ.atoms.positions[...,1].max(), - zmin=univ.atoms.positions[...,2].min(), - zmax=univ.atoms.positions[...,2].max(), - maximum_delta_magnitude=2.0, - num_cores=1) + dx, dy, dz = streamlines_3D.generate_streamlines_3d( + topology_file_path=Martini_membrane_gro, + trajectory_file_path=membrane_xtc, + grid_spacing=20, + MDA_selection="name PO4", + start_frame=1, + end_frame=2, + xmin=univ.atoms.positions[..., 0].min(), + xmax=univ.atoms.positions[..., 0].max(), + ymin=univ.atoms.positions[..., 1].min(), + ymax=univ.atoms.positions[..., 1].max(), + zmin=univ.atoms.positions[..., 2].min(), + zmax=univ.atoms.positions[..., 2].max(), + maximum_delta_magnitude=2.0, + num_cores=1, + ) assert dx.shape == (5, 5, 2) assert dy.shape == (5, 5, 2) assert dz.shape == (5, 5, 2) diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 63450efa749..8e9ed998022 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -171,6 +171,9 @@ setup\.py | MDAnalysisTests/formats/.*\.py | MDAnalysisTests/parallelism/.*\.py | MDAnalysisTests/scripts/.*\.py +| MDAnalysisTests/import/.*\.py +| MDAnalysisTests/utils/.*\.py +| MDAnalysisTests/visualization/.*\.py ) ''' extend-exclude = ''' From 2844005e3c4c5a7705cdd8345979c7d1cb199797 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Fri, 3 Jan 2025 01:01:37 +0100 Subject: [PATCH 51/58] Implementation of `WaterSelection` for water resiude selection (#4854) * Update selection.py Addition of WaterSelection * Update test_atomselections.py added tests * Update CHANGELOG changelog addition mentioned * Update selection.py --- package/CHANGELOG | 1 + package/MDAnalysis/core/selection.py | 31 +++++++++++++++++ .../core/test_atomselections.py | 34 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/package/CHANGELOG b/package/CHANGELOG index 7b85922487a..5d8328f3313 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -27,6 +27,7 @@ Fixes the function to prevent shared state. (Issue #4655) Enhancements + * Addition of 'water' token for water selection (Issue #4839) * Enables parallelization for analysis.density.DensityAnalysis (Issue #4677, PR #4729) * Enables parallelization for analysis.contacts.Contacts (Issue #4660) * Enable parallelization for analysis.nucleicacids.NucPairDist (Issue #4670) diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index 591c074030d..6c2ea3c8172 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -1094,6 +1094,37 @@ def _apply(self, group): return group[mask] +class WaterSelection(Selection): + """All atoms in water residues with recognized water residue names. + + Recognized residue names: + + * recognized 3 Letter resnames: 'H2O', 'HOH', 'OH2', 'HHO', 'OHH' + 'TIP', 'T3P', 'T4P', 'T5P', 'SOL', 'WAT' + + * recognized 4 Letter resnames: 'TIP2', 'TIP3', 'TIP4' + + .. versionadded:: 2.9.0 + """ + token = 'water' + + # Recognized water resnames + water_res = { + 'H2O', 'HOH', 'OH2', 'HHO', 'OHH', + 'T3P', 'T4P', 'T5P', 'SOL', 'WAT', + 'TIP', 'TIP2', 'TIP3', 'TIP4' + } + + def _apply(self, group): + resnames = group.universe._topology.resnames + nmidx = resnames.nmidx[group.resindices] + + matches = [ix for (nm, ix) in resnames.namedict.items() + if nm in self.water_res] + mask = np.isin(nmidx, matches) + + return group[mask] + class BackboneSelection(ProteinSelection): """A BackboneSelection contains all atoms with name 'N', 'CA', 'C', 'O'. diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index bced4c43bde..51b395f94cb 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -48,6 +48,8 @@ PDB_helix, PDB_elements, PDB_charges, + waterPSF, + PDB_full, ) from MDAnalysisTests import make_Universe @@ -650,6 +652,38 @@ def test_nucleicsugar(self, universe): assert_equal(rna.n_atoms, rna.n_residues * 5) +class TestSelectionsWater(object): + @pytest.fixture(scope='class') + def universe(self): + return MDAnalysis.Universe(GRO) + + @pytest.fixture(scope='class') + def universe2(self): + return MDAnalysis.Universe(waterPSF) + + @pytest.fixture(scope='class') + def universe3(self): + return MDAnalysis.Universe(PDB_full) + + def test_water_gro(self, universe): + # Test SOL water with 4 atoms + water_gro = universe.select_atoms("water") + assert_equal(water_gro.n_atoms, 44336) + assert_equal(water_gro.n_residues, 11084) + + def test_water_tip3(self, universe2): + # Test TIP3 water with 3 atoms + water_tip3 = universe2.select_atoms('water') + assert_equal(water_tip3.n_atoms, 15) + assert_equal(water_tip3.n_residues, 5) + + def test_water_pdb(self, universe3): + # Test HOH water with 1 atom + water_pdb = universe3.select_atoms("water") + assert_equal(water_pdb.n_residues, 188) + assert_equal(water_pdb.n_atoms, 188) + + class BaseDistanceSelection(object): """Both KDTree and distmat selections on orthogonal system From 0ac698194f5dc579c488ac03f6b6f1f4dd53614a Mon Sep 17 00:00:00 2001 From: Tanish Yelgoe <143334319+tanishy7777@users.noreply.github.com> Date: Mon, 6 Jan 2025 05:32:37 +0530 Subject: [PATCH 52/58] DocFix: Added the documentation for run() method in polymer.rst (#4864) - fix #4793 - Added :inherited-members: to polymer.rst so that run method from AnalysisBase is added in the documentation for MDAnalysis.analysis.polymer --- .../doc/sphinx/source/documentation_pages/analysis/polymer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/doc/sphinx/source/documentation_pages/analysis/polymer.rst b/package/doc/sphinx/source/documentation_pages/analysis/polymer.rst index 6641bb56894..6b7d9b81138 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis/polymer.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis/polymer.rst @@ -1,3 +1,3 @@ .. automodule:: MDAnalysis.analysis.polymer :members: - + :inherited-members: From 536a39018ac29a3d288ed24fbd00fd13f3ee22c6 Mon Sep 17 00:00:00 2001 From: Valerij Talagayev <82884038+talagayev@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:54:52 +0100 Subject: [PATCH 53/58] Addition of water selection documentation (#4881) * Update selections.rst addition of water in simple selection with its description added mention of all available resnames for water that are recognized --------- Co-authored-by: Irfan Alibay --- .../doc/sphinx/source/documentation_pages/selections.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/package/doc/sphinx/source/documentation_pages/selections.rst b/package/doc/sphinx/source/documentation_pages/selections.rst index 6ee7f26bc2d..124e231207d 100644 --- a/package/doc/sphinx/source/documentation_pages/selections.rst +++ b/package/doc/sphinx/source/documentation_pages/selections.rst @@ -114,6 +114,15 @@ protein, backbone, nucleic, nucleicbackbone is identfied by a hard-coded set of residue names so it may not work for esoteric residues. +water + selects all atoms that belong to a set of water residues; water + is defined with a set of common water abbreviations present in + topology files and may not work with certain water residue names. + Currently the following water resnames are supported: + 3 letter resnames: ``H2O``, ``HOH``, ``OH2``, ``HHO``, ``OHH``, ``TIP``, + ``T3P``, ``T4P``, ``T5P``, ``SOL``, ``WAT``. + 4 letter resnames: ``TIP2``, ``TIP3``, ``TIP4``. + segid *seg-name* select by segid (as given in the topology), e.g. ``segid 4AKE`` or ``segid DMPC`` From 59e478db53ffb974fe94539bfc520c84a1946e72 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Tue, 7 Jan 2025 00:08:51 +0100 Subject: [PATCH 54/58] refine pep8speaks (#4877) Allow line break with leading operator (Knuth's style) See https://peps.python.org/pep-0008/#should-a-line-break-before-or-after-a-binary-operator --- .pep8speaks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pep8speaks.yml b/.pep8speaks.yml index 98b4a206b34..cdc6775f986 100644 --- a/.pep8speaks.yml +++ b/.pep8speaks.yml @@ -4,7 +4,7 @@ scanner: pycodestyle: # Valid if scanner.linter is pycodestyle max-line-length: 79 - ignore: ["E203", "E701"] + ignore: ["E203", "E701", "W503"] exclude: [] count: False first: False @@ -17,7 +17,7 @@ pycodestyle: # Valid if scanner.linter is pycodestyle flake8: # Valid if scanner.linter is flake8 max-line-length: 79 - ignore: ["E203", "E501", "E701"] + ignore: ["E203", "E501", "E701", "W503"] exclude: [] count: False show-source: False From 1eca9f2d7ce9776c9a6141e1024863863209617f Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Mon, 6 Jan 2025 19:05:46 -0700 Subject: [PATCH 55/58] update Python versions gh-ci-cron.yaml (#4845) - remove Python 3.9 - add 3.13 to PyPi installation - hold off on 3.13 for conda because of #4805 --- .github/workflows/gh-ci-cron.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gh-ci-cron.yaml b/.github/workflows/gh-ci-cron.yaml index 230a99dbb73..f35df0f2527 100644 --- a/.github/workflows/gh-ci-cron.yaml +++ b/.github/workflows/gh-ci-cron.yaml @@ -237,9 +237,11 @@ jobs: fail-fast: false matrix: # Stick to macos-13 because some of our - # optional depss don't support arm64 (i.e. macos-14) + # optional deps don't support arm64 (i.e. macos-14) + # + # add "3.13" once conda-forge packages are available (see #4805) os: [ubuntu-latest, macos-13] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 @@ -285,11 +287,8 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, macos-14, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] wheels: ['true', 'false'] - exclude: - - os: "macos-14" - python-version: "3.9" steps: # Checkout to have access to local actions (i.e. setup-os) - uses: actions/checkout@v4 From 5eef34165b03281515e08a69159f6504e3a2ff8b Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Wed, 8 Jan 2025 12:49:12 +0100 Subject: [PATCH 56/58] [fmt] core (#4874) --- package/MDAnalysis/core/__init__.py | 2 +- package/MDAnalysis/core/_get_readers.py | 61 +- package/MDAnalysis/core/accessors.py | 7 +- package/MDAnalysis/core/groups.py | 1044 ++++++++----- package/MDAnalysis/core/topology.py | 127 +- package/MDAnalysis/core/topologyattrs.py | 1135 ++++++++------ package/MDAnalysis/core/topologyobjects.py | 243 +-- package/pyproject.toml | 1 + .../MDAnalysisTests/core/test_accessors.py | 22 +- .../MDAnalysisTests/core/test_accumulate.py | 154 +- testsuite/MDAnalysisTests/core/test_atom.py | 31 +- .../MDAnalysisTests/core/test_atomgroup.py | 1221 +++++++++------ .../core/test_atomselections.py | 1315 ++++++++++------- .../MDAnalysisTests/core/test_copying.py | 117 +- .../MDAnalysisTests/core/test_fragments.py | 49 +- .../core/test_group_traj_access.py | 240 +-- testsuite/MDAnalysisTests/core/test_groups.py | 944 ++++++------ .../MDAnalysisTests/core/test_index_dtype.py | 9 +- .../MDAnalysisTests/core/test_requires.py | 25 +- .../MDAnalysisTests/core/test_residue.py | 3 +- .../MDAnalysisTests/core/test_residuegroup.py | 151 +- .../MDAnalysisTests/core/test_segment.py | 8 +- .../MDAnalysisTests/core/test_segmentgroup.py | 52 +- .../MDAnalysisTests/core/test_topology.py | 294 ++-- .../core/test_topologyattrs.py | 315 ++-- .../core/test_topologyobjects.py | 348 +++-- .../MDAnalysisTests/core/test_universe.py | 901 ++++++----- testsuite/MDAnalysisTests/core/test_unwrap.py | 309 ++-- .../core/test_updating_atomgroup.py | 107 +- testsuite/MDAnalysisTests/core/test_wrap.py | 365 +++-- testsuite/MDAnalysisTests/core/util.py | 364 +++-- testsuite/pyproject.toml | 1 + 32 files changed, 5822 insertions(+), 4143 deletions(-) diff --git a/package/MDAnalysis/core/__init__.py b/package/MDAnalysis/core/__init__.py index 254cc6fead1..5247ae35057 100644 --- a/package/MDAnalysis/core/__init__.py +++ b/package/MDAnalysis/core/__init__.py @@ -50,7 +50,7 @@ and write your own Python code. """ -__all__ = ['AtomGroup', 'Selection'] +__all__ = ["AtomGroup", "Selection"] from .groups import AtomGroup from .selection import Selection diff --git a/package/MDAnalysis/core/_get_readers.py b/package/MDAnalysis/core/_get_readers.py index a1d603965e7..45d61b3cadf 100644 --- a/package/MDAnalysis/core/_get_readers.py +++ b/package/MDAnalysis/core/_get_readers.py @@ -22,9 +22,15 @@ import copy import inspect -from .. import (_READERS, _READER_HINTS, - _PARSERS, _PARSER_HINTS, - _MULTIFRAME_WRITERS, _SINGLEFRAME_WRITERS, _CONVERTERS) +from .. import ( + _READERS, + _READER_HINTS, + _PARSERS, + _PARSER_HINTS, + _MULTIFRAME_WRITERS, + _SINGLEFRAME_WRITERS, + _CONVERTERS, +) from ..lib import util @@ -80,8 +86,8 @@ def get_reader_for(filename, format=None): return format # ChainReader gets returned even if format is specified - if _READER_HINTS['CHAIN'](filename): - format = 'CHAIN' + if _READER_HINTS["CHAIN"](filename): + format = "CHAIN" # Only guess if format is not specified if format is None: for fmt_name, test in _READER_HINTS.items(): @@ -103,7 +109,9 @@ def get_reader_for(filename, format=None): " Use the format keyword to explicitly set the format: 'Universe(...,format=FORMAT)'\n" " For missing formats, raise an issue at " "https://github.com/MDAnalysis/mdanalysis/issues".format( - format, filename, _READERS.keys())) + format, filename, _READERS.keys() + ) + ) raise ValueError(errmsg) from None @@ -158,7 +166,7 @@ def get_writer_for(filename, format=None, multiframe=None): The `filename` argument has been made mandatory. """ if filename is None: - format = 'NULL' + format = "NULL" elif format is None: try: root, ext = util.get_ext(filename) @@ -172,18 +180,24 @@ def get_writer_for(filename, format=None, multiframe=None): else: format = util.check_compressed_format(root, ext) - if format == '': - raise ValueError(( - 'File format could not be guessed from {}, ' - 'resulting in empty string - ' - 'only None or valid formats are supported.' - ).format(filename)) + if format == "": + raise ValueError( + ( + "File format could not be guessed from {}, " + "resulting in empty string - " + "only None or valid formats are supported." + ).format(filename) + ) format = format.upper() if multiframe is None: # Multiframe takes priority, else use singleframe - options = copy.copy(_SINGLEFRAME_WRITERS) # do copy to avoid changing in place - options.update(_MULTIFRAME_WRITERS) # update overwrites existing entries + options = copy.copy( + _SINGLEFRAME_WRITERS + ) # do copy to avoid changing in place + options.update( + _MULTIFRAME_WRITERS + ) # update overwrites existing entries errmsg = "No trajectory or frame writer for format '{0}'" elif multiframe is True: options = _MULTIFRAME_WRITERS @@ -192,9 +206,11 @@ def get_writer_for(filename, format=None, multiframe=None): options = _SINGLEFRAME_WRITERS errmsg = "No single frame writer for format '{0}'" else: - raise ValueError("Unknown value '{0}' for multiframe," - " only True, False, None allowed" - "".format(multiframe)) + raise ValueError( + "Unknown value '{0}' for multiframe," + " only True, False, None allowed" + "".format(multiframe) + ) try: return options[format] @@ -252,10 +268,13 @@ def get_parser_for(filename, format=None): " See https://docs.mdanalysis.org/documentation_pages/topology/init.html#supported-topology-formats\n" " For missing formats, raise an issue at \n" " https://github.com/MDAnalysis/mdanalysis/issues".format( - format, _PARSERS.keys())) + format, _PARSERS.keys() + ) + ) raise ValueError(errmsg) from None else: - return _PARSERS['MINIMAL'] + return _PARSERS["MINIMAL"] + def get_converter_for(format): """Return the appropriate topology converter for ``format``. @@ -276,6 +295,6 @@ def get_converter_for(format): try: writer = _CONVERTERS[format] except KeyError: - errmsg = f'No converter found for {format} format' + errmsg = f"No converter found for {format} format" raise TypeError(errmsg) from None return writer diff --git a/package/MDAnalysis/core/accessors.py b/package/MDAnalysis/core/accessors.py index 3338d009702..1d366ec81fe 100644 --- a/package/MDAnalysis/core/accessors.py +++ b/package/MDAnalysis/core/accessors.py @@ -168,6 +168,7 @@ class ConverterWrapper: be accessed as a method with the name of the package in lowercase, i.e. `convert_to.parmed()` """ + _CONVERTERS = {} def __init__(self, ag): @@ -199,6 +200,8 @@ def __call__(self, package, *args, **kwargs): try: convert = getattr(self, package.lower()) except AttributeError: - raise ValueError(f"No {package!r} converter found. Available: " - f"{' '.join(self._CONVERTERS.keys())}") from None + raise ValueError( + f"No {package!r} converter found. Available: " + f"{' '.join(self._CONVERTERS.keys())}" + ) from None return convert(*args, **kwargs) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index e8bf30ba110..40044be7c80 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -99,13 +99,20 @@ import contextlib import warnings -from .. import (_CONVERTERS, - _TOPOLOGY_ATTRS, _TOPOLOGY_TRANSPLANTS, _TOPOLOGY_ATTRNAMES) +from .. import ( + _CONVERTERS, + _TOPOLOGY_ATTRS, + _TOPOLOGY_TRANSPLANTS, + _TOPOLOGY_ATTRNAMES, +) from ..lib import util -from ..lib.util import (cached, warn_if_not_unique, - unique_int_1d, unique_int_1d_unsorted, - int_array_is_sorted - ) +from ..lib.util import ( + cached, + warn_if_not_unique, + unique_int_1d, + unique_int_1d_unsorted, + int_array_is_sorted, +) from ..lib import distances from ..lib import transformations from ..lib import mdamath @@ -154,7 +161,8 @@ def make_classes(): bases[cls] = GBase._subclass(is_group=True) # CBase for patching all components CBase = bases[ComponentBase] = _TopologyAttrContainer._subclass( - is_group=False) + is_group=False + ) for cls in components: bases[cls] = CBase._subclass(is_group=False) @@ -180,6 +188,7 @@ class _TopologyAttrContainer(object): The mixed subclasses become the final container classes specific to each :class:`~MDAnalysis.core.universe.Universe`. """ + @classmethod def _subclass(cls, is_group): """Factory method returning :class:`_TopologyAttrContainer` subclasses. @@ -200,16 +209,29 @@ def _subclass(cls, is_group): type A subclass of :class:`_TopologyAttrContainer`, with the same name. """ - newcls = type(cls.__name__, (cls,), {'_is_group': bool(is_group)}) + newcls = type(cls.__name__, (cls,), {"_is_group": bool(is_group)}) if is_group: newcls._SETATTR_WHITELIST = { - 'positions', 'velocities', 'forces', 'dimensions', - 'atoms', 'residue', 'residues', 'segment', 'segments', + "positions", + "velocities", + "forces", + "dimensions", + "atoms", + "residue", + "residues", + "segment", + "segments", } else: newcls._SETATTR_WHITELIST = { - 'position', 'velocity', 'force', 'dimensions', - 'atoms', 'residue', 'residues', 'segment', + "position", + "velocity", + "force", + "dimensions", + "atoms", + "residue", + "residues", + "segment", } return newcls @@ -259,12 +281,18 @@ def setter(self, values): return attr.__setitem__(self, values) if cls._is_group: - setattr(cls, attr.attrname, - property(getter, setter, None, attr.groupdoc)) + setattr( + cls, + attr.attrname, + property(getter, setter, None, attr.groupdoc), + ) cls._SETATTR_WHITELIST.add(attr.attrname) else: - setattr(cls, attr.singular, - property(getter, setter, None, attr.singledoc)) + setattr( + cls, + attr.singular, + property(getter, setter, None, attr.singledoc), + ) cls._SETATTR_WHITELIST.add(attr.singular) @classmethod @@ -285,12 +313,16 @@ def _del_prop(cls, attr): def __setattr__(self, attr, value): # `ag.this = 42` calls setattr(ag, 'this', 42) - if not (attr.startswith('_') or # 'private' allowed - attr in self._SETATTR_WHITELIST or # known attributes allowed - hasattr(self, attr)): # preexisting (eg properties) allowed + if not ( + attr.startswith("_") # 'private' allowed + or attr in self._SETATTR_WHITELIST # known attributes allowed + or hasattr(self, attr) + ): # preexisting (eg properties) allowed raise AttributeError( "Cannot set arbitrary attributes to a {}".format( - 'Group' if self._is_group else 'Component')) + "Group" if self._is_group else "Component" + ) + ) # if it is, we allow the setattr to proceed by deferring to the super # behaviour (ie do it) super(_TopologyAttrContainer, self).__setattr__(attr, value) @@ -310,6 +342,7 @@ class _MutableBase(object): in cache retrieval. """ + def __new__(cls, *args, **kwargs): # This pre-initialization wrapper must be pretty generic to # allow for different initialization schemes of the possible classes. @@ -323,32 +356,37 @@ def __new__(cls, *args, **kwargs): u = args[0][0].universe except (TypeError, IndexError, AttributeError): from .universe import Universe + # Let's be generic and get the first argument that's either a # Universe, a Group, or a Component, and go from there. # This is where the UpdatingAtomGroup args get matched. - for arg in args+tuple(kwargs.values()): - if isinstance(arg, (Universe, GroupBase, - ComponentBase)): + for arg in args + tuple(kwargs.values()): + if isinstance(arg, (Universe, GroupBase, ComponentBase)): u = arg.universe break else: errmsg = ( f"No universe, or universe-containing object " - f"passed to the initialization of {cls.__name__}") + f"passed to the initialization of {cls.__name__}" + ) raise TypeError(errmsg) from None try: return object.__new__(u._classes[cls]) except KeyError: # Cache miss. Let's find which kind of class this is and merge. try: - parent_cls = next(u._class_bases[parent] - for parent in cls.mro() - if parent in u._class_bases) + parent_cls = next( + u._class_bases[parent] + for parent in cls.mro() + if parent in u._class_bases + ) except StopIteration: - errmsg = (f"Attempted to instantiate class '{cls.__name__}' " - f"but none of its parents are known to the universe." - f" Currently possible parent classes are: " - f"{str(sorted(u._class_bases.keys()))}") + errmsg = ( + f"Attempted to instantiate class '{cls.__name__}' " + f"but none of its parents are known to the universe." + f" Currently possible parent classes are: " + f"{str(sorted(u._class_bases.keys()))}" + ) raise TypeError(errmsg) from None newcls = u._classes[cls] = parent_cls._mix(cls) return object.__new__(newcls) @@ -360,37 +398,46 @@ def __getattr__(self, attr): topattr, meth, clstype = _TOPOLOGY_TRANSPLANTS[attr] if isinstance(meth, property): attrname = attr - attrtype = 'property' + attrtype = "property" else: - attrname = attr + '()' - attrtype = 'method' + attrname = attr + "()" + attrtype = "method" # property of wrong group/component if not isinstance(self, clstype): - mname = 'property' if isinstance(meth, property) else 'method' - err = '{attr} is a {method} of {clstype}, not {selfcls}' + mname = "property" if isinstance(meth, property) else "method" + err = "{attr} is a {method} of {clstype}, not {selfcls}" clsname = clstype.__name__ - if clsname == 'GroupBase': - clsname = selfcls + 'Group' - raise AttributeError(err.format(attr=attrname, - method=attrtype, - clstype=clsname, - selfcls=selfcls)) + if clsname == "GroupBase": + clsname = selfcls + "Group" + raise AttributeError( + err.format( + attr=attrname, + method=attrtype, + clstype=clsname, + selfcls=selfcls, + ) + ) # missing required topologyattr else: - err = ('{selfcls}.{attrname} not available; ' - 'this requires {topattr}') - raise NoDataError(err.format(selfcls=selfcls, - attrname=attrname, - topattr=topattr)) + err = ( + "{selfcls}.{attrname} not available; " + "this requires {topattr}" + ) + raise NoDataError( + err.format( + selfcls=selfcls, attrname=attrname, topattr=topattr + ) + ) else: - clean = attr.lower().replace('_', '') - err = '{selfcls} has no attribute {attr}. '.format(selfcls=selfcls, - attr=attr) + clean = attr.lower().replace("_", "") + err = "{selfcls} has no attribute {attr}. ".format( + selfcls=selfcls, attr=attr + ) if clean in _TOPOLOGY_ATTRNAMES: match = _TOPOLOGY_ATTRNAMES[clean] - err += 'Did you mean {match}?'.format(match=match) + err += "Did you mean {match}?".format(match=match) raise AttributeError(err) def get_connections(self, typename, outside=True): @@ -430,9 +477,8 @@ def get_connections(self, typename, outside=True): class _ImmutableBase(object): - """Class used to shortcut :meth:`__new__` to :meth:`object.__new__`. + """Class used to shortcut :meth:`__new__` to :meth:`object.__new__`.""" - """ # When mixed via _TopologyAttrContainer._mix this class has MRO priority. # Setting __new__ like this will avoid having to go through the # cache lookup if the class is reused (as in ag._derived_class(...)). @@ -441,26 +487,32 @@ class _ImmutableBase(object): def _pbc_to_wrap(function): """Raises deprecation warning if 'pbc' is set and assigns value to 'wrap'""" + @functools.wraps(function) def wrapped(group, *args, **kwargs): - if kwargs.get('pbc', None) is not None: - warnings.warn("The 'pbc' kwarg has been deprecated and will be " - "removed in version 3.0., " - "please use 'wrap' instead", - DeprecationWarning) - kwargs['wrap'] = kwargs.pop('pbc') + if kwargs.get("pbc", None) is not None: + warnings.warn( + "The 'pbc' kwarg has been deprecated and will be " + "removed in version 3.0., " + "please use 'wrap' instead", + DeprecationWarning, + ) + kwargs["wrap"] = kwargs.pop("pbc") return function(group, *args, **kwargs) + return wrapped def check_wrap_and_unwrap(function): """Raises ValueError when both 'wrap' and 'unwrap' are set to True""" + @functools.wraps(function) def wrapped(group, *args, **kwargs): - if kwargs.get('wrap') and kwargs.get('unwrap'): + if kwargs.get("wrap") and kwargs.get("unwrap"): raise ValueError("both 'wrap' and 'unwrap' can not be set to true") return function(group, *args, **kwargs) + return wrapped @@ -468,18 +520,25 @@ def _only_same_level(function): @functools.wraps(function) def wrapped(self, other): if not isinstance(other, (ComponentBase, GroupBase)): # sanity check - raise TypeError("Can't perform '{}' between objects:" - " '{}' and '{}'".format( - function.__name__, - type(self).__name__, - type(other).__name__)) + raise TypeError( + "Can't perform '{}' between objects:" + " '{}' and '{}'".format( + function.__name__, + type(self).__name__, + type(other).__name__, + ) + ) if self.level != other.level: - raise TypeError("Can't perform '{}' on different level objects" - "".format(function.__name__)) + raise TypeError( + "Can't perform '{}' on different level objects" + "".format(function.__name__) + ) if self.universe is not other.universe: raise ValueError( - "Can't operate on objects from different Universes") + "Can't operate on objects from different Universes" + ) return function(self, other) + return wrapped @@ -560,19 +619,22 @@ def __init__(self, *args): else: # current/new init method ix, u = args - except (AttributeError, # couldn't find ix/universe - TypeError): # couldn't iterate the object we got + except ( + AttributeError, # couldn't find ix/universe + TypeError, + ): # couldn't iterate the object we got errmsg = ( "Can only initialise a Group from an iterable of Atom/Residue/" "Segment objects eg: AtomGroup([Atom1, Atom2, Atom3]) " "or an iterable of indices and a Universe reference " - "eg: AtomGroup([0, 5, 7, 8], u).") + "eg: AtomGroup([0, 5, 7, 8], u)." + ) raise TypeError(errmsg) from None # indices for the objects I hold ix = np.asarray(ix, dtype=np.intp) if ix.ndim > 1: - raise IndexError('Group index must be 1d') + raise IndexError("Group index must be 1d") self._ix = ix self._u = u self._cache = dict() @@ -592,7 +654,7 @@ def __getitem__(self, item): # it can be sliced by all of these already, # so just return ourselves sliced by the item if item is None: - raise TypeError('None cannot be used to index a group.') + raise TypeError("None cannot be used to index a group.") elif isinstance(item, numbers.Integral): return self.level.singular(self.ix[item], self.universe) else: @@ -611,30 +673,35 @@ def __getattr__(self, attr): if attr in _TOPOLOGY_ATTRS: cls = _TOPOLOGY_ATTRS[attr] if attr == cls.singular and attr != cls.attrname: - err = ('{selfcls} has no attribute {attr}. ' - 'Do you mean {plural}?') - raise AttributeError(err.format(selfcls=selfcls, attr=attr, - plural=cls.attrname)) + err = ( + "{selfcls} has no attribute {attr}. " + "Do you mean {plural}?" + ) + raise AttributeError( + err.format(selfcls=selfcls, attr=attr, plural=cls.attrname) + ) else: - err = 'This Universe does not contain {singular} information' + err = "This Universe does not contain {singular} information" raise NoDataError(err.format(singular=cls.singular)) else: return super(GroupBase, self).__getattr__(attr) def __repr__(self): name = self.level.name - return ("<{}Group with {} {}{}>" - "".format(name.capitalize(), len(self), name, - "s"[len(self) == 1:])) # Shorthand for a conditional plural 's'. + return "<{}Group with {} {}{}>" "".format( + name.capitalize(), len(self), name, "s"[len(self) == 1 :] + ) # Shorthand for a conditional plural 's'. def __str__(self): name = self.level.name if len(self) <= 10: - return '<{}Group {}>'.format(name.capitalize(), repr(list(self))) + return "<{}Group {}>".format(name.capitalize(), repr(list(self))) else: - return '<{}Group {}, ..., {}>'.format(name.capitalize(), - repr(list(self)[:3])[:-1], - repr(list(self)[-3:])[1:]) + return "<{}Group {}, ..., {}>".format( + name.capitalize(), + repr(list(self)[:3])[:-1], + repr(list(self)[-3:])[1:], + ) def __add__(self, other): """Concatenate the Group with another Group or Component of the same @@ -671,9 +738,12 @@ def __radd__(self, other): if other == 0: return self._derived_class(self.ix, self.universe) else: - raise TypeError("unsupported operand type(s) for +:" - " '{}' and '{}'".format(type(self).__name__, - type(other).__name__)) + raise TypeError( + "unsupported operand type(s) for +:" + " '{}' and '{}'".format( + type(self).__name__, type(other).__name__ + ) + ) def __sub__(self, other): return self.difference(other) @@ -754,22 +824,22 @@ def dimensions(self, dimensions): self.universe.trajectory.ts.dimensions = dimensions @property - @cached('sorted_unique') + @cached("sorted_unique") def sorted_unique(self): return self.asunique(sorted=True) @property - @cached('unsorted_unique') + @cached("unsorted_unique") def unsorted_unique(self): return self.asunique(sorted=False) @property - @cached('issorted') + @cached("issorted") def issorted(self): return int_array_is_sorted(self.ix) @property - @cached('isunique') + @cached("isunique") def isunique(self): """Boolean indicating whether all components of the group are unique, i.e., the group contains no duplicates. @@ -809,34 +879,35 @@ def isunique(self): def _asunique(self, group, sorted=False, set_mask=False): try: - name = 'sorted_unique' if sorted else 'unsorted_unique' + name = "sorted_unique" if sorted else "unsorted_unique" return self._cache[name] except KeyError: pass if self.isunique: if not sorted: - self._cache['unsorted_unique'] = self + self._cache["unsorted_unique"] = self return self if self.issorted: - self._cache['unsorted_unique'] = self - self._cache['sorted_unique'] = self + self._cache["unsorted_unique"] = self + self._cache["sorted_unique"] = self return self if sorted: if set_mask: unique_ix, restore_mask = np.unique( - self.ix, return_inverse=True) + self.ix, return_inverse=True + ) self._unique_restore_mask = restore_mask else: unique_ix = unique_int_1d(self.ix) _unique = group[unique_ix] - _unique._cache['isunique'] = True - _unique._cache['issorted'] = True - _unique._cache['sorted_unique'] = _unique - _unique._cache['unsorted_unique'] = _unique - self._cache['sorted_unique'] = _unique + _unique._cache["isunique"] = True + _unique._cache["issorted"] = True + _unique._cache["sorted_unique"] = _unique + _unique._cache["unsorted_unique"] = _unique + self._cache["sorted_unique"] = _unique return _unique indices = unique_int_1d_unsorted(self.ix) @@ -848,45 +919,51 @@ def _asunique(self, group, sorted=False, set_mask=False): self._unique_restore_mask = mask issorted = int_array_is_sorted(indices) - if issorted and 'sorted_unique' in self._cache: - self._cache['unsorted_unique'] = self.sorted_unique + if issorted and "sorted_unique" in self._cache: + self._cache["unsorted_unique"] = self.sorted_unique return self.sorted_unique _unique = group[indices] - _unique._cache['isunique'] = True - _unique._cache['issorted'] = issorted - _unique._cache['unsorted_unique'] = _unique - self._cache['unsorted_unique'] = _unique + _unique._cache["isunique"] = True + _unique._cache["issorted"] = issorted + _unique._cache["unsorted_unique"] = _unique + self._cache["unsorted_unique"] = _unique if issorted: - self._cache['sorted_unique'] = _unique - _unique._cache['sorted_unique'] = _unique + self._cache["sorted_unique"] = _unique + _unique._cache["sorted_unique"] = _unique return _unique def _get_compound_indices(self, compound): - if compound == 'residues': + if compound == "residues": compound_indices = self.atoms.resindices - elif compound == 'segments': + elif compound == "segments": compound_indices = self.atoms.segindices - elif compound == 'molecules': + elif compound == "molecules": try: compound_indices = self.atoms.molnums except AttributeError: - errmsg = ("Cannot use compound='molecules': No molecule " - "information in topology.") + errmsg = ( + "Cannot use compound='molecules': No molecule " + "information in topology." + ) raise NoDataError(errmsg) from None - elif compound == 'fragments': + elif compound == "fragments": try: compound_indices = self.atoms.fragindices except NoDataError: - errmsg = ("Cannot use compound='fragments': No bond " - "information in topology.") + errmsg = ( + "Cannot use compound='fragments': No bond " + "information in topology." + ) raise NoDataError(errmsg) from None - elif compound == 'group': + elif compound == "group": raise ValueError("This method does not accept compound='group'") else: - raise ValueError("Unrecognized compound definition: {}\nPlease use" - " one of 'residues', 'segments', 'molecules'," - " or 'fragments'.".format(compound)) + raise ValueError( + "Unrecognized compound definition: {}\nPlease use" + " one of 'residues', 'segments', 'molecules'," + " or 'fragments'.".format(compound) + ) return compound_indices def _split_by_compound_indices(self, compound, stable_sort=False): @@ -960,7 +1037,7 @@ def _split_by_compound_indices(self, compound, stable_sort=False): # stable sort ensures reproducibility, especially concerning who # gets to be a compound's atom[0] and be a reference for unwrap. if stable_sort: - sort_indices = np.argsort(compound_indices, kind='stable') + sort_indices = np.argsort(compound_indices, kind="stable") else: # Quicksort sort_indices = np.argsort(compound_indices) @@ -972,18 +1049,24 @@ def _split_by_compound_indices(self, compound, stable_sort=False): for compound_size in unique_compound_sizes: compound_masks.append(compound_sizes == compound_size) if needs_sorting: - atom_masks.append(sort_indices[size_per_atom == compound_size] - .reshape(-1, compound_size)) + atom_masks.append( + sort_indices[size_per_atom == compound_size].reshape( + -1, compound_size + ) + ) else: - atom_masks.append(np.where(size_per_atom == compound_size)[0] - .reshape(-1, compound_size)) + atom_masks.append( + np.where(size_per_atom == compound_size)[0].reshape( + -1, compound_size + ) + ) return atom_masks, compound_masks, len(compound_sizes) @warn_if_not_unique @_pbc_to_wrap @check_wrap_and_unwrap - def center(self, weights, wrap=False, unwrap=False, compound='group'): + def center(self, weights, wrap=False, unwrap=False, compound="group"): """Weighted center of (compounds of) the group Computes the weighted center of :class:`Atoms` in the group. @@ -1086,12 +1169,13 @@ def center(self, weights, wrap=False, unwrap=False, compound='group'): dtype = np.float64 comp = compound.lower() - if comp == 'group': + if comp == "group": if wrap: coords = atoms.pack_into_box(inplace=False) elif unwrap: coords = atoms.unwrap( - compound=comp, reference=None, inplace=False) + compound=comp, reference=None, inplace=False + ) else: coords = atoms.positions # If there's no atom, return its (empty) coordinates unchanged. @@ -1103,15 +1187,17 @@ def center(self, weights, wrap=False, unwrap=False, compound='group'): return coords.mean(axis=0) # promote weights to dtype if required: weights = weights.astype(dtype, copy=False) - return np.einsum('ij,ij->j',coords,weights[:, None]) / weights.sum() + return ( + np.einsum("ij,ij->j", coords, weights[:, None]) / weights.sum() + ) # When compound split caching gets implemented it will be clever to # preempt at this point whether or not stable sorting will be needed # later for unwrap (so that we don't split now with non-stable sort, # only to have to re-split with stable sort if unwrap is requested). - (atom_masks, - compound_masks, - n_compounds) = self._split_by_compound_indices(comp) + (atom_masks, compound_masks, n_compounds) = ( + self._split_by_compound_indices(comp) + ) # Unwrap Atoms if unwrap: @@ -1132,7 +1218,9 @@ def center(self, weights, wrap=False, unwrap=False, compound='group'): _centers = _coords.mean(axis=1) else: _weights = weights[atom_mask] - _centers = np.einsum('ijk,ijk->ik',_coords,_weights[:, :, None]) + _centers = np.einsum( + "ijk,ijk->ik", _coords, _weights[:, :, None] + ) _centers /= _weights.sum(axis=1)[:, None] centers[compound_mask] = _centers if wrap: @@ -1142,7 +1230,7 @@ def center(self, weights, wrap=False, unwrap=False, compound='group'): @warn_if_not_unique @_pbc_to_wrap @check_wrap_and_unwrap - def center_of_geometry(self, wrap=False, unwrap=False, compound='group'): + def center_of_geometry(self, wrap=False, unwrap=False, compound="group"): r"""Center of geometry of (compounds of) the group .. math:: @@ -1199,7 +1287,7 @@ def center_of_geometry(self, wrap=False, unwrap=False, compound='group'): centroid = center_of_geometry @warn_if_not_unique - def accumulate(self, attribute, function=np.sum, compound='group'): + def accumulate(self, attribute, function=np.sum, compound="group"): r"""Accumulates the attribute associated with (compounds of) the group. Accumulates the attribute of :class:`Atoms` in the group. @@ -1307,18 +1395,20 @@ def accumulate(self, attribute, function=np.sum, compound='group'): else: attribute_values = np.asarray(attribute) if len(attribute_values) != len(atoms): - raise ValueError("The input array length ({}) does not match " - "the number of atoms ({}) in the group." - "".format(len(attribute_values), len(atoms))) + raise ValueError( + "The input array length ({}) does not match " + "the number of atoms ({}) in the group." + "".format(len(attribute_values), len(atoms)) + ) comp = compound.lower() - if comp == 'group': + if comp == "group": return function(attribute_values, axis=0) - (atom_masks, - compound_masks, - n_compounds) = self._split_by_compound_indices(comp) + (atom_masks, compound_masks, n_compounds) = ( + self._split_by_compound_indices(comp) + ) higher_dims = list(attribute_values.shape[1:]) @@ -1727,15 +1817,19 @@ def wrap(self, compound="atoms", center="com", box=None, inplace=True): if box is None: box = self.dimensions if box is None: - raise ValueError("No dimensions information in Universe. " - " Either use the 'box' argument or" - " set the '.dimensions' attribute") + raise ValueError( + "No dimensions information in Universe. " + " Either use the 'box' argument or" + " set the '.dimensions' attribute" + ) else: box = np.asarray(box, dtype=np.float32) if not np.all(box > 0.0) or box.shape != (6,): - raise ValueError("Invalid box: Box has invalid shape or not all " - "box dimensions are positive. You can specify a " - "valid box using the 'box' argument.") + raise ValueError( + "Invalid box: Box has invalid shape or not all " + "box dimensions are positive. You can specify a " + "valid box using the 'box' argument." + ) # no matter what kind of group we have, we need to work on its (unique) # atoms: @@ -1746,12 +1840,20 @@ def wrap(self, compound="atoms", center="com", box=None, inplace=True): atoms = _atoms comp = compound.lower() - if comp not in ('atoms', 'group', 'segments', 'residues', 'molecules', - 'fragments'): - raise ValueError("Unrecognized compound definition '{}'. " - "Please use one of 'atoms', 'group', 'segments', " - "'residues', 'molecules', or 'fragments'." - "".format(compound)) + if comp not in ( + "atoms", + "group", + "segments", + "residues", + "molecules", + "fragments", + ): + raise ValueError( + "Unrecognized compound definition '{}'. " + "Please use one of 'atoms', 'group', 'segments', " + "'residues', 'molecules', or 'fragments'." + "".format(compound) + ) if len(atoms) == 0: return np.zeros((0, 3), dtype=np.float32) @@ -1760,32 +1862,38 @@ def wrap(self, compound="atoms", center="com", box=None, inplace=True): positions = distances.apply_PBC(atoms.positions, box) else: ctr = center.lower() - if ctr == 'com': + if ctr == "com": # Don't use hasattr(self, 'masses') because that's incredibly # slow for ResidueGroups or SegmentGroups - if not hasattr(self._u._topology, 'masses'): - raise NoDataError("Cannot perform wrap with center='com', " - "this requires masses.") - elif ctr != 'cog': - raise ValueError("Unrecognized center definition '{}'. Please " - "use one of 'com' or 'cog'.".format(center)) + if not hasattr(self._u._topology, "masses"): + raise NoDataError( + "Cannot perform wrap with center='com', " + "this requires masses." + ) + elif ctr != "cog": + raise ValueError( + "Unrecognized center definition '{}'. Please " + "use one of 'com' or 'cog'.".format(center) + ) positions = atoms.positions # compute and apply required shift: - if ctr == 'com': + if ctr == "com": ctrpos = atoms.center_of_mass(wrap=False, compound=comp) if np.any(np.isnan(ctrpos)): - specifier = 'the' if comp == 'group' else 'one of the' - raise ValueError("Cannot use compound='{0}' with " - "center='com' because {1} {0}\'s total " - "mass is zero.".format(comp, specifier)) + specifier = "the" if comp == "group" else "one of the" + raise ValueError( + "Cannot use compound='{0}' with " + "center='com' because {1} {0}'s total " + "mass is zero.".format(comp, specifier) + ) else: # ctr == 'cog' ctrpos = atoms.center_of_geometry(wrap=False, compound=comp) ctrpos = ctrpos.astype(np.float32, copy=False) target = distances.apply_PBC(ctrpos, box) shifts = target - ctrpos - if comp == 'group': + if comp == "group": positions += shifts else: @@ -1805,7 +1913,7 @@ def wrap(self, compound="atoms", center="com", box=None, inplace=True): positions = positions[restore_mask] return positions - def unwrap(self, compound='fragments', reference='com', inplace=True): + def unwrap(self, compound="fragments", reference="com", inplace=True): r"""Move atoms of this group so that bonds within the group's compounds aren't split across periodic boundaries. @@ -1878,7 +1986,7 @@ def unwrap(self, compound='fragments', reference='com', inplace=True): """ atoms = self.atoms # bail out early if no bonds in topology: - if not hasattr(atoms, 'bonds'): + if not hasattr(atoms, "bonds"): raise NoDataError( f"{self.__class__.__name__}.unwrap() not available; this AtomGroup lacks defined bonds. " "To resolve this, you can either:\n" @@ -1891,16 +1999,20 @@ def unwrap(self, compound='fragments', reference='com', inplace=True): if reference is not None: try: reference = reference.lower() - if reference not in ('cog', 'com'): + if reference not in ("cog", "com"): raise ValueError except (AttributeError, ValueError): - raise ValueError("Unrecognized reference '{}'. Please use one " - "of 'com', 'cog', or None.".format(reference)) + raise ValueError( + "Unrecognized reference '{}'. Please use one " + "of 'com', 'cog', or None.".format(reference) + ) # Don't use hasattr(self, 'masses') because that's incredibly slow for # ResidueGroups or SegmentGroups - if reference == 'com' and not hasattr(unique_atoms, 'masses'): - raise NoDataError("Cannot perform unwrap with reference='com', " - "this requires masses.") + if reference == "com" and not hasattr(unique_atoms, "masses"): + raise NoDataError( + "Cannot perform unwrap with reference='com', " + "this requires masses." + ) # Sanity checking of the compound parameter is done downstream in # _split_by_compound_indices @@ -1911,18 +2023,20 @@ def unwrap(self, compound='fragments', reference='com', inplace=True): # case below. Both code paths could be merged, but 'group' can be done # unidimensionally whereas the general multi-compound case involves # more indexing and is therefore slower. Leaving separate for now. - if comp == 'group': + if comp == "group": positions = mdamath.make_whole(unique_atoms, inplace=False) # Apply reference shift if required: if reference is not None and len(positions) > 0: - if reference == 'com': + if reference == "com": masses = unique_atoms.masses total_mass = masses.sum() if np.isclose(total_mass, 0.0): - raise ValueError("Cannot perform unwrap with " - "reference='com' because the total " - "mass of the group is zero.") - refpos = np.einsum('ij,ij->j',positions,masses[:, None]) + raise ValueError( + "Cannot perform unwrap with " + "reference='com' because the total " + "mass of the group is zero." + ) + refpos = np.einsum("ij,ij->j", positions, masses[:, None]) refpos /= total_mass else: # reference == 'cog' refpos = positions.mean(axis=0) @@ -1934,32 +2048,40 @@ def unwrap(self, compound='fragments', reference='com', inplace=True): # When unwrapping and not shifting with a cog/com reference we # need to make sure that the first atom of each compound is stable # regarding sorting. - atom_masks = unique_atoms._split_by_compound_indices(comp, - stable_sort=reference is None)[0] + atom_masks = unique_atoms._split_by_compound_indices( + comp, stable_sort=reference is None + )[0] positions = unique_atoms.positions for atom_mask in atom_masks: for mask in atom_mask: - positions[mask] = mdamath.make_whole(unique_atoms[mask], - inplace=False) + positions[mask] = mdamath.make_whole( + unique_atoms[mask], inplace=False + ) # Apply reference shift if required: if reference is not None: - if reference == 'com': + if reference == "com": masses = unique_atoms.masses[atom_mask] total_mass = masses.sum(axis=1) if np.any(np.isclose(total_mass, 0.0)): - raise ValueError("Cannot perform unwrap with " - "reference='com' because the " - "total mass of at least one of " - "the {} is zero.".format(comp)) - refpos = np.einsum('ijk,ijk->ik',positions[atom_mask], - masses[:, :, None]) + raise ValueError( + "Cannot perform unwrap with " + "reference='com' because the " + "total mass of at least one of " + "the {} is zero.".format(comp) + ) + refpos = np.einsum( + "ijk,ijk->ik", + positions[atom_mask], + masses[:, :, None], + ) refpos /= total_mass[:, None] else: # reference == 'cog' refpos = positions[atom_mask].mean(axis=1) refpos = refpos.astype(np.float32, copy=False) target = distances.apply_PBC(refpos, self.dimensions) - positions[atom_mask] += (target[:, None, :] - - refpos[:, None, :]) + positions[atom_mask] += ( + target[:, None, :] - refpos[:, None, :] + ) if inplace: unique_atoms.positions = positions if not atoms.isunique: @@ -1979,21 +2101,21 @@ def copy(self): def _set_unique_caches_from(self, other): # Try to fill the copied group's uniqueness caches: try: - self._cache['isunique'] = other._cache['isunique'] + self._cache["isunique"] = other._cache["isunique"] except KeyError: pass else: if self.isunique: - self._cache['unsorted_unique'] = self + self._cache["unsorted_unique"] = self try: - self._cache['issorted'] = other._cache['issorted'] + self._cache["issorted"] = other._cache["issorted"] except KeyError: pass else: if self.issorted: - if self._cache.get('isunique'): - self._cache['sorted_unique'] = self + if self._cache.get("isunique"): + self._cache["sorted_unique"] = self def groupby(self, topattrs): """Group together items in this group according to values of *topattr* @@ -2061,7 +2183,7 @@ def groupby(self, topattrs): if isinstance(topattrs, (str, bytes)): attr = topattrs if isinstance(topattrs, bytes): - attr = topattrs.decode('utf-8') + attr = topattrs.decode("utf-8") ta = getattr(self, attr) return {i: self[ta == i] for i in set(ta)} @@ -2119,8 +2241,9 @@ def concatenate(self, other): .. versionadded:: 0.16.0 """ o_ix = other.ix_array - return self._derived_class(np.concatenate([self.ix, o_ix]), - self.universe) + return self._derived_class( + np.concatenate([self.ix, o_ix]), self.universe + ) @_only_same_level def union(self, other): @@ -2207,7 +2330,9 @@ def intersection(self, other): .. versionadded:: 0.16 """ o_ix = other.ix_array - return self._derived_class(np.intersect1d(self.ix, o_ix), self.universe) + return self._derived_class( + np.intersect1d(self.ix, o_ix), self.universe + ) @_only_same_level def subtract(self, other): @@ -2651,10 +2776,10 @@ class AtomGroup(GroupBase): def __getattr__(self, attr): # special-case timestep info - if attr in ('velocities', 'forces'): - raise NoDataError('This Timestep has no ' + attr) - elif attr == 'positions': - raise NoDataError('This Universe has no coordinates') + if attr in ("velocities", "forces"): + raise NoDataError("This Timestep has no " + attr) + elif attr == "positions": + raise NoDataError("This Universe has no coordinates") return super(AtomGroup, self).__getattr__(attr) def __reduce__(self): @@ -2691,10 +2816,10 @@ def residues(self): :class:`Residues` present in the :class:`AtomGroup`. """ rg = self.universe.residues[unique_int_1d(self.resindices)] - rg._cache['isunique'] = True - rg._cache['issorted'] = True - rg._cache['sorted_unique'] = rg - rg._cache['unsorted_unique'] = rg + rg._cache["isunique"] = True + rg._cache["issorted"] = True + rg._cache["sorted_unique"] = rg + rg._cache["unsorted_unique"] = rg return rg @residues.setter @@ -2708,14 +2833,20 @@ def residues(self, new): try: r_ix = [r.resindex for r in new] except AttributeError: - errmsg = ("Can only set AtomGroup residues to Residue " - "or ResidueGroup not {}".format( - ', '.join(type(r) for r in new - if not isinstance(r, Residue)))) + errmsg = ( + "Can only set AtomGroup residues to Residue " + "or ResidueGroup not {}".format( + ", ".join( + type(r) for r in new if not isinstance(r, Residue) + ) + ) + ) raise TypeError(errmsg) from None if not isinstance(r_ix, itertools.cycle) and len(r_ix) != len(self): - raise ValueError("Incorrect size: {} for AtomGroup of size: {}" - "".format(len(new), len(self))) + raise ValueError( + "Incorrect size: {} for AtomGroup of size: {}" + "".format(len(new), len(self)) + ) # Optimisation TODO: # This currently rebuilds the tt len(self) times # Ideally all changes would happen and *afterwards* tables are built @@ -2740,16 +2871,18 @@ def segments(self): :class:`AtomGroup`. """ sg = self.universe.segments[unique_int_1d(self.segindices)] - sg._cache['isunique'] = True - sg._cache['issorted'] = True - sg._cache['sorted_unique'] = sg - sg._cache['unsorted_unique'] = sg + sg._cache["isunique"] = True + sg._cache["issorted"] = True + sg._cache["sorted_unique"] = sg + sg._cache["unsorted_unique"] = sg return sg @segments.setter def segments(self, new): - raise NotImplementedError("Cannot assign Segments to AtomGroup. " - "Segments are assigned to Residues") + raise NotImplementedError( + "Cannot assign Segments to AtomGroup. " + "Segments are assigned to Residues" + ) @property def n_segments(self): @@ -2760,7 +2893,7 @@ def n_segments(self): return len(self.segments) @property - @cached('unique_restore_mask') + @cached("unique_restore_mask") def _unique_restore_mask(self): # The _unique_restore_mask property's cache is populated whenever the # AtomGroup.unique property of a *non-unique* AtomGroup is accessed. @@ -2770,18 +2903,24 @@ def _unique_restore_mask(self): # then be replaced by the __getattr__() error message. To prevent the # message from being overridden, we raise a RuntimeError instead. if self.isunique: - msg = ("{0}._unique_restore_mask is not available if the {0} is " - "unique. ".format(self.__class__.__name__)) + msg = ( + "{0}._unique_restore_mask is not available if the {0} is " + "unique. ".format(self.__class__.__name__) + ) else: - msg = ("{0}._unique_restore_mask is only available after " - "accessing {0}.unique. ".format(self.__class__.__name__)) - msg += ("If you see this error message in an unmodified release " - "version of MDAnalysis, this is almost certainly a bug!") + msg = ( + "{0}._unique_restore_mask is only available after " + "accessing {0}.unique. ".format(self.__class__.__name__) + ) + msg += ( + "If you see this error message in an unmodified release " + "version of MDAnalysis, this is almost certainly a bug!" + ) raise RuntimeError(msg) @_unique_restore_mask.setter def _unique_restore_mask(self, mask): - self._cache['unique_restore_mask'] = mask + self._cache["unique_restore_mask"] = mask @property def unique(self): @@ -2822,10 +2961,10 @@ def unique(self): This function now always returns a copy. """ group = self.sorted_unique[:] - group._cache['isunique'] = True - group._cache['issorted'] = True - group._cache['sorted_unique'] = group - group._cache['unsorted_unique'] = group + group._cache["isunique"] = True + group._cache["issorted"] = True + group._cache["sorted_unique"] = group + group._cache["unsorted_unique"] = group return group def asunique(self, sorted=False): @@ -2871,8 +3010,9 @@ def asunique(self, sorted=False): .. versionadded:: 2.0.0 """ - return self._asunique(sorted=sorted, group=self.universe.atoms, - set_mask=True) + return self._asunique( + sorted=sorted, group=self.universe.atoms, set_mask=True + ) @property def positions(self): @@ -2992,9 +3132,19 @@ def ts(self): # As with universe.select_atoms, needing to fish out specific kwargs # (namely, 'updating') doesn't allow a very clean signature. - def select_atoms(self, sel, *othersel, periodic=True, rtol=1e-05, - atol=1e-08, updating=False, sorted=True, - rdkit_kwargs=None, smarts_kwargs=None, **selgroups): + def select_atoms( + self, + sel, + *othersel, + periodic=True, + rtol=1e-05, + atol=1e-08, + updating=False, + sorted=True, + rdkit_kwargs=None, + smarts_kwargs=None, + **selgroups, + ): """Select atoms from within this Group using a selection string. Returns an :class:`AtomGroup` sorted according to their index in the @@ -3208,7 +3358,7 @@ def select_atoms(self, sel, *othersel, periodic=True, rtol=1e-05, universe = mda.Universe(PSF, DCD) guessed_elements = guess_types(universe.atoms.names) universe.add_TopologyAttr('elements', guessed_elements) - + .. doctest:: AtomGroup.select_atoms.smarts >>> universe.select_atoms("smarts C", smarts_kwargs={"maxMatches": 100}) @@ -3383,31 +3533,46 @@ def select_atoms(self, sel, *othersel, periodic=True, rtol=1e-05, """ if not sel: - warnings.warn("Empty string to select atoms, empty group returned.", - UserWarning) + warnings.warn( + "Empty string to select atoms, empty group returned.", + UserWarning, + ) return self[[]] sel_strs = (sel,) + othersel for group, thing in selgroups.items(): if not isinstance(thing, AtomGroup): - raise TypeError("Passed groups must be AtomGroups. " - "You provided {} for group '{}'".format( - thing.__class__.__name__, group)) - - selections = tuple((selection.Parser.parse(s, selgroups, - periodic=periodic, - atol=atol, rtol=rtol, - sorted=sorted, - rdkit_kwargs=rdkit_kwargs, - smarts_kwargs=smarts_kwargs) - for s in sel_strs)) + raise TypeError( + "Passed groups must be AtomGroups. " + "You provided {} for group '{}'".format( + thing.__class__.__name__, group + ) + ) + + selections = tuple( + ( + selection.Parser.parse( + s, + selgroups, + periodic=periodic, + atol=atol, + rtol=rtol, + sorted=sorted, + rdkit_kwargs=rdkit_kwargs, + smarts_kwargs=smarts_kwargs, + ) + for s in sel_strs + ) + ) if updating: atomgrp = UpdatingAtomGroup(self, selections, sel_strs) else: # Apply the first selection and sum to it - atomgrp = sum([sel.apply(self) for sel in selections[1:]], - selections[0].apply(self)) + atomgrp = sum( + [sel.apply(self) for sel in selections[1:]], + selections[0].apply(self), + ) return atomgrp def split(self, level): @@ -3422,9 +3587,11 @@ def split(self, level): .. versionadded:: 0.9.0 .. versionchanged:: 0.17.0 Added the 'molecule' level. """ - accessors = {'segment': 'segindices', - 'residue': 'resindices', - 'molecule': 'molnums'} + accessors = { + "segment": "segindices", + "residue": "resindices", + "molecule": "molnums", + } if level == "atom": return [self.universe.atoms[[a.ix]] for a in self] @@ -3433,16 +3600,22 @@ def split(self, level): try: levelindices = getattr(self, accessors[level]) except AttributeError: - errmsg = (f'This universe does not have {level} information. Maybe' - f' it is not provided in the topology format in use.') + errmsg = ( + f"This universe does not have {level} information. Maybe" + f" it is not provided in the topology format in use." + ) raise AttributeError(errmsg) from None except KeyError: - errmsg = (f"level = '{level}' not supported, must be one of " - f"{accessors.keys()}") + errmsg = ( + f"level = '{level}' not supported, must be one of " + f"{accessors.keys()}" + ) raise ValueError(errmsg) from None - return [self[levelindices == index] for index in - unique_int_1d(levelindices)] + return [ + self[levelindices == index] + for index in unique_int_1d(levelindices) + ] def guess_bonds(self, vdwradii=None, fudge_factor=0.55, lower_bound=0.1): """Guess bonds, angles, and dihedrals between the atoms in this @@ -3489,21 +3662,24 @@ def get_TopAttr(u, name, cls): return attr # indices of bonds - guesser = DefaultGuesser(None, fudge_factor=fudge_factor, - lower_bound=lower_bound, - box=self.dimensions, - vdwradii=vdwradii) + 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 = get_TopAttr(self.universe, "bonds", Bonds) bondattr._add_bonds(b, guessed=True) a = guesser.guess_angles(self.bonds) - angleattr = get_TopAttr(self.universe, 'angles', Angles) + angleattr = get_TopAttr(self.universe, "angles", Angles) angleattr._add_bonds(a, guessed=True) d = guesser.guess_dihedrals(self.angles) - diheattr = get_TopAttr(self.universe, 'dihedrals', Dihedrals) + diheattr = get_TopAttr(self.universe, "dihedrals", Dihedrals) diheattr._add_bonds(d) @property @@ -3521,7 +3697,8 @@ def bond(self): """ if len(self) != 2: raise ValueError( - "bond only makes sense for a group with exactly 2 atoms") + "bond only makes sense for a group with exactly 2 atoms" + ) return topologyobjects.Bond(self.ix, self.universe) @property @@ -3539,7 +3716,8 @@ def angle(self): """ if len(self) != 3: raise ValueError( - "angle only makes sense for a group with exactly 3 atoms") + "angle only makes sense for a group with exactly 3 atoms" + ) return topologyobjects.Angle(self.ix, self.universe) @property @@ -3557,7 +3735,8 @@ def dihedral(self): """ if len(self) != 4: raise ValueError( - "dihedral only makes sense for a group with exactly 4 atoms") + "dihedral only makes sense for a group with exactly 4 atoms" + ) return topologyobjects.Dihedral(self.ix, self.universe) @property @@ -3575,7 +3754,8 @@ def improper(self): """ if len(self) != 4: raise ValueError( - "improper only makes sense for a group with exactly 4 atoms") + "improper only makes sense for a group with exactly 4 atoms" + ) return topologyobjects.ImproperDihedral(self.ix, self.universe) @property @@ -3593,7 +3773,8 @@ def ureybradley(self): """ if len(self) != 2: raise ValueError( - "urey bradley only makes sense for a group with exactly 2 atoms") + "urey bradley only makes sense for a group with exactly 2 atoms" + ) return topologyobjects.UreyBradley(self.ix, self.universe) @property @@ -3611,13 +3792,20 @@ def cmap(self): """ if len(self) != 5: raise ValueError( - "cmap only makes sense for a group with exactly 5 atoms") + "cmap only makes sense for a group with exactly 5 atoms" + ) return topologyobjects.CMap(self.ix, self.universe) convert_to = Accessor("convert_to", ConverterWrapper) - def write(self, filename=None, file_format=None, - filenamefmt="{trjname}_{frame}", frames=None, **kwargs): + def write( + self, + filename=None, + file_format=None, + filenamefmt="{trjname}_{frame}", + frames=None, + **kwargs, + ): """Write `AtomGroup` to a file. The output can either be a coordinate file or a selection, depending on @@ -3674,7 +3862,7 @@ def write(self, filename=None, file_format=None, raise IndexError("Cannot write an AtomGroup with 0 atoms") trj = self.universe.trajectory # unified trajectory API - if frames is None or frames == 'all': + if frames is None or frames == "all": trj_frames = trj[::] elif isinstance(frames, numbers.Integral): # We accept everything that indexes a trajectory and returns a @@ -3688,25 +3876,27 @@ def write(self, filename=None, file_format=None, else: if test_trajectory is not trj: raise ValueError( - 'The trajectory of {} provided to the frames keyword ' - 'attribute is different from the trajectory of the ' - 'AtomGroup.'.format(frames) + "The trajectory of {} provided to the frames keyword " + "attribute is different from the trajectory of the " + "AtomGroup.".format(frames) ) trj_frames = frames if filename is None: trjname, ext = os.path.splitext(os.path.basename(trj.filename)) filename = filenamefmt.format(trjname=trjname, frame=trj.frame) - filename = util.filename(filename, - ext=file_format if file_format is not None else 'PDB', - keep=True) + filename = util.filename( + filename, + ext=file_format if file_format is not None else "PDB", + keep=True, + ) # Some writer behave differently when they are given a "multiframe" # argument. It is the case of the PDB writer tht writes models when # "multiframe" is True. # We want to honor what the user provided with the argument if # provided explicitly. If not, then we need to figure out if we write # multiple frames or not. - multiframe = kwargs.pop('multiframe', None) + multiframe = kwargs.pop("multiframe", None) if len(trj_frames) > 1 and multiframe == False: raise ValueError( 'Cannot explicitely set "multiframe" to False and request ' @@ -3725,7 +3915,8 @@ def write(self, filename=None, file_format=None, # Once (and if!) class is selected, use it in with block try: writer = get_writer_for( - filename, format=file_format, multiframe=multiframe) + filename, format=file_format, multiframe=multiframe + ) except (ValueError, TypeError): pass else: @@ -3744,8 +3935,9 @@ def write(self, filename=None, file_format=None, try: # here `file_format` is only used as default, # anything pulled off `filename` will be used preferentially - writer = get_selection_writer_for(filename, - file_format if file_format is not None else 'PDB') + writer = get_selection_writer_for( + filename, file_format if file_format is not None else "PDB" + ) except (TypeError, NotImplementedError): pass else: @@ -3755,7 +3947,7 @@ def write(self, filename=None, file_format=None, raise ValueError("No writer found for format: {}".format(filename)) - def sort(self, key='ix', keyfunc=None): + def sort(self, key="ix", keyfunc=None): """ Returns a sorted ``AtomGroup`` using a specified attribute as the key. @@ -3812,24 +4004,28 @@ def sort(self, key='ix', keyfunc=None): """ idx = getattr(self.atoms, key) if len(idx) != len(self.atoms): - raise ValueError("The array returned by the attribute '{}' " - "must have the same length as the number of " - "atoms in the input AtomGroup".format(key)) + raise ValueError( + "The array returned by the attribute '{}' " + "must have the same length as the number of " + "atoms in the input AtomGroup".format(key) + ) if idx.ndim == 1: - order = np.argsort(idx, kind='stable') + order = np.argsort(idx, kind="stable") elif idx.ndim > 1: if keyfunc is None: - raise NameError("The {} attribute returns a multidimensional " - "array. In order to sort it, a function " - "returning a 1D array (to be used as the sort " - "key) must be passed to the keyfunc argument" - .format(key)) + raise NameError( + "The {} attribute returns a multidimensional " + "array. In order to sort it, a function " + "returning a 1D array (to be used as the sort " + "key) must be passed to the keyfunc argument".format(key) + ) sortkeys = keyfunc(idx) if sortkeys.ndim != 1: - raise ValueError("The function assigned to the argument " - "'keyfunc':{} doesn't return a 1D array." - .format(keyfunc)) - order = np.argsort(sortkeys, kind='stable') + raise ValueError( + "The function assigned to the argument " + "'keyfunc':{} doesn't return a 1D array.".format(keyfunc) + ) + order = np.argsort(sortkeys, kind="stable") return self.atoms[order] @@ -3913,10 +4109,10 @@ def segments(self): the :class:`ResidueGroup`. """ sg = self.universe.segments[unique_int_1d(self.segindices)] - sg._cache['isunique'] = True - sg._cache['issorted'] = True - sg._cache['sorted_unique'] = sg - sg._cache['unsorted_unique'] = sg + sg._cache["isunique"] = True + sg._cache["issorted"] = True + sg._cache["sorted_unique"] = sg + sg._cache["unsorted_unique"] = sg return sg @segments.setter @@ -3930,14 +4126,20 @@ def segments(self, new): try: s_ix = [s.segindex for s in new] except AttributeError: - errmsg = ("Can only set ResidueGroup segments to Segment " - "or SegmentGroup, not {}".format( - ', '.join(type(r) for r in new - if not isinstance(r, Segment)))) + errmsg = ( + "Can only set ResidueGroup segments to Segment " + "or SegmentGroup, not {}".format( + ", ".join( + type(r) for r in new if not isinstance(r, Segment) + ) + ) + ) raise TypeError(errmsg) from None if not isinstance(s_ix, itertools.cycle) and len(s_ix) != len(self): - raise ValueError("Incorrect size: {} for ResidueGroup of size: {}" - "".format(len(new), len(self))) + raise ValueError( + "Incorrect size: {} for ResidueGroup of size: {}" + "".format(len(new), len(self)) + ) # Optimisation TODO: # This currently rebuilds the tt len(self) times # Ideally all changes would happen and *afterwards* tables are built @@ -3992,10 +4194,10 @@ def unique(self): This function now always returns a copy. """ group = self.sorted_unique[:] - group._cache['isunique'] = True - group._cache['issorted'] = True - group._cache['sorted_unique'] = group - group._cache['unsorted_unique'] = group + group._cache["isunique"] = True + group._cache["issorted"] = True + group._cache["sorted_unique"] = group + group._cache["unsorted_unique"] = group return group def asunique(self, sorted=False): @@ -4184,10 +4386,10 @@ def unique(self): This function now always returns a copy. """ group = self.sorted_unique[:] - group._cache['isunique'] = True - group._cache['issorted'] = True - group._cache['sorted_unique'] = group - group._cache['unsorted_unique'] = group + group._cache["isunique"] = True + group._cache["issorted"] = True + group._cache["sorted_unique"] = group + group._cache["unsorted_unique"] = group return group def asunique(self, sorted=False): @@ -4248,7 +4450,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') + raise IndexError( + "Component can only be indexed by a single integer" + ) self._ix = ix self._u = u @@ -4258,12 +4462,17 @@ def __getattr__(self, attr): if attr in _TOPOLOGY_ATTRS: cls = _TOPOLOGY_ATTRS[attr] if attr == cls.attrname and attr != cls.singular: - err = ('{selfcls} has no attribute {attr}. ' - 'Do you mean {singular}?') - raise AttributeError(err.format(selfcls=selfcls, attr=attr, - singular=cls.singular)) + err = ( + "{selfcls} has no attribute {attr}. " + "Do you mean {singular}?" + ) + raise AttributeError( + err.format( + selfcls=selfcls, attr=attr, singular=cls.singular + ) + ) else: - err = 'This Universe does not contain {singular} information' + err = "This Universe does not contain {singular} information" raise NoDataError(err.format(singular=cls.singular)) else: return super(ComponentBase, self).__getattr__(attr) @@ -4302,7 +4511,8 @@ def __add__(self, other): o_ix = other.ix_array return self.level.plural( - np.concatenate([self.ix_array, o_ix]), self.universe) + np.concatenate([self.ix_array, o_ix]), self.universe + ) def __radd__(self, other): """Using built-in sum requires supporting 0 + self. If other is @@ -4321,9 +4531,12 @@ def __radd__(self, other): if other == 0: return self.level.plural(self.ix_array, self.universe) else: - raise TypeError("unsupported operand type(s) for +:" - " '{}' and '{}'".format(type(self).__name__, - type(other).__name__)) + raise TypeError( + "unsupported operand type(s) for +:" + " '{}' and '{}'".format( + type(self).__name__, type(other).__name__ + ) + ) @property def universe(self): @@ -4364,31 +4577,31 @@ class Atom(ComponentBase): """ def __repr__(self): - me = '' + me = "" def __reduce__(self): return (_unpickle2, (self.universe, self.ix, Atom)) def __getattr__(self, attr): # special-case timestep info - ts = {'velocity': 'velocities', 'force': 'forces'} + ts = {"velocity": "velocities", "force": "forces"} if attr in ts: - raise NoDataError('This Timestep has no ' + ts[attr]) - elif attr == 'position': - raise NoDataError('This Universe has no coordinates') + raise NoDataError("This Timestep has no " + ts[attr]) + elif attr == "position": + raise NoDataError("This Universe has no coordinates") return super(Atom, self).__getattr__(attr) @property @@ -4398,8 +4611,10 @@ def residue(self): @residue.setter def residue(self, new): if not isinstance(new, Residue): - raise TypeError("Can only set Atom residue to Residue, not {}" - "".format(type(new))) + raise TypeError( + "Can only set Atom residue to Residue, not {}" + "".format(type(new)) + ) self.universe._topology.tt.move_atom(self.ix, new.resindex) @property @@ -4408,8 +4623,9 @@ def segment(self): @segment.setter def segment(self, new): - raise NotImplementedError("Cannot set atom segment. " - "Segments are assigned to Residues") + raise NotImplementedError( + "Cannot set atom segment. " "Segments are assigned to Residues" + ) @property def position(self): @@ -4496,13 +4712,13 @@ class Residue(ComponentBase): """ def __repr__(self): - me = '' + return me + ">" def __reduce__(self): return (_unpickle2, (self.universe, self.ix, Residue)) @@ -4513,23 +4729,24 @@ def atoms(self): :class:`Residue`. """ ag = self.universe.atoms[self.universe._topology.indices[self][0]] - ag._cache['isunique'] = True - ag._cache['issorted'] = True - ag._cache['sorted_unique'] = ag - ag._cache['unsorted_unique'] = ag + ag._cache["isunique"] = True + ag._cache["issorted"] = True + ag._cache["sorted_unique"] = ag + ag._cache["unsorted_unique"] = ag return ag @property def segment(self): - """The :class:`Segment` this :class:`Residue` belongs to. - """ + """The :class:`Segment` this :class:`Residue` belongs to.""" return self.universe.segments[self.universe._topology.segindices[self]] @segment.setter def segment(self, new): if not isinstance(new, Segment): - raise TypeError("Can only set Residue segment to Segment, not {}" - "".format(type(new))) + raise TypeError( + "Can only set Residue segment to Segment, not {}" + "".format(type(new)) + ) self.universe._topology.tt.move_residue(self.ix, new.segindex) @@ -4552,10 +4769,10 @@ class Segment(ComponentBase): """ def __repr__(self): - me = '' + me = "" def __reduce__(self): return (_unpickle2, (self.universe, self.ix, Segment)) @@ -4566,10 +4783,10 @@ def atoms(self): :class:`Segment`. """ ag = self.universe.atoms[self.universe._topology.indices[self][0]] - ag._cache['isunique'] = True - ag._cache['issorted'] = True - ag._cache['sorted_unique'] = ag - ag._cache['unsorted_unique'] = ag + ag._cache["isunique"] = True + ag._cache["issorted"] = True + ag._cache["sorted_unique"] = ag + ag._cache["unsorted_unique"] = ag return ag @property @@ -4577,11 +4794,13 @@ def residues(self): """A :class:`ResidueGroup` of :class:`Residues` present in this :class:`Segment`. """ - rg = self.universe.residues[self.universe._topology.resindices[self][0]] - rg._cache['isunique'] = True - rg._cache['issorted'] = True - rg._cache['sorted_unique'] = rg - rg._cache['unsorted_unique'] = rg + rg = self.universe.residues[ + self.universe._topology.resindices[self][0] + ] + rg._cache["isunique"] = True + rg._cache["issorted"] = True + rg._cache["sorted_unique"] = rg + rg._cache["unsorted_unique"] = rg return rg @@ -4590,10 +4809,15 @@ def residues(self): # here, otherwise we get __getattribute__ infinite loops. _UAG_SHORTCUT_ATTRS = { # Class information of the UAG - "__class__", "_derived_class", + "__class__", + "_derived_class", # Metadata of the UAG - "_base_group", "_selections", "_lastupdate", - "level", "_u", "universe", + "_base_group", + "_selections", + "_lastupdate", + "level", + "_u", + "universe", # Methods of the UAG "_ensure_updated", "is_uptodate", @@ -4613,6 +4837,7 @@ class UpdatingAtomGroup(AtomGroup): .. versionadded:: 0.16.0 """ + # WARNING: This class has __getattribute__ and __getattr__ methods (the # latter inherited from AtomGroup). Because of this bugs introduced in the # class that cause an AttributeError may be very hard to diagnose and @@ -4654,8 +4879,7 @@ def update_selection(self): sels = self._selections if sels: # As with select_atoms, we select the first sel and then sum to it. - ix = sum([sel.apply(bg) for sel in sels[1:]], - sels[0].apply(bg)).ix + ix = sum([sel.apply(bg) for sel in sels[1:]], sels[0].apply(bg)).ix else: ix = np.array([], dtype=np.intp) # Run back through AtomGroup init with this information to remake @@ -4712,7 +4936,7 @@ def __getattribute__(self, name): # ALL attribute access goes through here # If the requested attribute is public (not starting with '_') and # isn't in the shortcut list, update ourselves - if not (name.startswith('_') or name in _UAG_SHORTCUT_ATTRS): + if not (name.startswith("_") or name in _UAG_SHORTCUT_ATTRS): self._ensure_updated() # Going via object.__getattribute__ then bypasses this check stage return object.__getattribute__(self, name) @@ -4722,9 +4946,14 @@ def __reduce__(self): # - unpickle base group # - recreate UAG as created through select_atoms (basegroup and selstrs) # even if base_group is a UAG this will work through recursion - return (_unpickle_uag, - (self._base_group.__reduce__(), self._selections, - self._selection_strings)) + return ( + _unpickle_uag, + ( + self._base_group.__reduce__(), + self._selections, + self._selection_strings, + ), + ) def __repr__(self): basestr = super(UpdatingAtomGroup, self).__repr__() @@ -4739,7 +4968,11 @@ def __repr__(self): basegrp = "another AtomGroup." # With a shorthand to conditionally append the 's' in 'selections'. return "{}, with selection{} {} on {}>".format( - basestr[:-1], "s"[len(self._selection_strings) == 1:], sels, basegrp) + basestr[:-1], + "s"[len(self._selection_strings) == 1 :], + sels, + basegrp, + ) @property def atoms(self): @@ -4803,16 +5036,17 @@ def copy(self): .. versionadded:: 0.19.0 """ - return UpdatingAtomGroup(self._base_group, self._selections, - self._selection_strings) + return UpdatingAtomGroup( + self._base_group, self._selections, self._selection_strings + ) # Define relationships between these classes # with Level objects -_Level = namedtuple('Level', ['name', 'singular', 'plural']) -ATOMLEVEL = _Level('atom', Atom, AtomGroup) -RESIDUELEVEL = _Level('residue', Residue, ResidueGroup) -SEGMENTLEVEL = _Level('segment', Segment, SegmentGroup) +_Level = namedtuple("Level", ["name", "singular", "plural"]) +ATOMLEVEL = _Level("atom", Atom, AtomGroup) +RESIDUELEVEL = _Level("residue", Residue, ResidueGroup) +SEGMENTLEVEL = _Level("segment", Segment, SegmentGroup) Atom.level = ATOMLEVEL AtomGroup.level = ATOMLEVEL @@ -4835,21 +5069,25 @@ def requires(*attrs): def mass_times_charge(atomgroup): return atomgroup.masses * atomgroup.charges """ + def require_dec(func): @functools.wraps(func) def check_args(*args, **kwargs): for a in args: # for each argument if isinstance(a, AtomGroup): # Make list of missing attributes - missing = [attr for attr in attrs - if not hasattr(a, attr)] + missing = [attr for attr in attrs if not hasattr(a, attr)] if missing: raise NoDataError( "{funcname} failed. " "AtomGroup is missing the following required " "attributes: {attrs}".format( funcname=func.__name__, - attrs=', '.join(missing))) + attrs=", ".join(missing), + ) + ) return func(*args, **kwargs) + return check_args + return require_dec diff --git a/package/MDAnalysis/core/topology.py b/package/MDAnalysis/core/topology.py index 899260721c3..dffff294d3a 100644 --- a/package/MDAnalysis/core/topology.py +++ b/package/MDAnalysis/core/topology.py @@ -117,7 +117,7 @@ def make_downshift_arrays(upshift, nparents): # reset nparents to the larger one between input and heuristic from data # This is useful for creating empty Universe where default value is 1. - nparents = np.max([nparents, u_values.max()+1]) + nparents = np.max([nparents, u_values.max() + 1]) residue_indices = np.zeros(nparents, dtype=int) missing_resids = np.sort(np.setdiff1d(np.arange(nparents), u_values)) indices = np.append(indices, upshift_sorted.shape[0]) @@ -128,7 +128,7 @@ def make_downshift_arrays(upshift, nparents): if missing_resid == 0: residue_indices[missing_resid] = 0 else: - residue_indices[missing_resid] = residue_indices[missing_resid-1] + residue_indices[missing_resid] = residue_indices[missing_resid - 1] downshift = np.split(order, residue_indices[:-1]) # Add None to end of array to force it to be of type Object @@ -181,10 +181,15 @@ class TransTable(object): .. versionchanged:: 2.3.0 Lazy building RA and SR. """ - def __init__(self, - n_atoms, n_residues, n_segments, # Size of tables - atom_resindex=None, residue_segindex=None, # Contents of tables - ): + + def __init__( + self, + n_atoms, + n_residues, + n_segments, # Size of tables + atom_resindex=None, + residue_segindex=None, # Contents of tables + ): self.n_atoms = n_atoms self.n_residues = n_residues self.n_segments = n_segments @@ -209,21 +214,24 @@ def __init__(self, def copy(self): """Return a deepcopy of this Transtable""" - return self.__class__(self.n_atoms, self.n_residues, self.n_segments, - atom_resindex=self._AR, residue_segindex=self._RS) + return self.__class__( + self.n_atoms, + self.n_residues, + self.n_segments, + atom_resindex=self._AR, + residue_segindex=self._RS, + ) @property def RA(self): if self._RA is None: - self._RA = make_downshift_arrays(self._AR, - self.n_residues) + self._RA = make_downshift_arrays(self._AR, self.n_residues) return self._RA @property def SR(self): if self._SR is None: - self._SR = make_downshift_arrays(self._RS, - self.n_segments) + self._SR = make_downshift_arrays(self._RS, self.n_segments) return self._SR @property @@ -425,7 +433,6 @@ def add_Residue(self, segidx): self._RS = np.concatenate([self._RS, np.array([segidx])]) self._SR = None - return self.n_residues - 1 def add_Segment(self): @@ -436,8 +443,8 @@ def add_Segment(self): def __getstate__(self): # don't serialize _RA and _SR for performance. attrs = self.__dict__ - attrs['_RA'] = None - attrs['_SR'] = None + attrs["_RA"] = None + attrs["_SR"] = None return attrs @@ -452,10 +459,15 @@ class Topology(object): """ - def __init__(self, n_atoms=1, n_res=1, n_seg=1, - attrs=None, - atom_resindex=None, - residue_segindex=None): + def __init__( + self, + n_atoms=1, + n_res=1, + n_seg=1, + attrs=None, + atom_resindex=None, + residue_segindex=None, + ): """ Parameters ---------- @@ -473,9 +485,13 @@ def __init__(self, n_atoms=1, n_res=1, n_seg=1, 1-D array giving the segindex of each residue in the system """ - self.tt = TransTable(n_atoms, n_res, n_seg, - atom_resindex=atom_resindex, - residue_segindex=residue_segindex) + self.tt = TransTable( + n_atoms, + n_res, + n_seg, + atom_resindex=atom_resindex, + residue_segindex=residue_segindex, + ) if attrs is None: attrs = [] @@ -541,16 +557,26 @@ def del_TopologyAttr(self, topologyattr): @property def guessed_attributes(self): """A list of the guessed attributes in this topology""" - return filter(lambda x: x.is_guessed - if(not isinstance(x.is_guessed, typing.Container)) - else True in x.is_guessed, self.attrs) + return filter( + lambda x: ( + x.is_guessed + if (not isinstance(x.is_guessed, typing.Container)) + else True in x.is_guessed + ), + self.attrs, + ) @property def read_attributes(self): """A list of the attributes read from the topology""" - return filter(lambda x: not x.is_guessed - if(not isinstance(x.is_guessed, typing.Container)) - else False in x.is_guessed, self.attrs) + return filter( + lambda x: ( + not x.is_guessed + if (not isinstance(x.is_guessed, typing.Container)) + else False in x.is_guessed + ), + self.attrs, + ) def add_Residue(self, segment, **new_attrs): """ @@ -569,21 +595,28 @@ def add_Residue(self, segment, **new_attrs): """ # Check that all data is here before making any changes for attr in self.attrs: - if not attr.per_object == 'residue': + if not attr.per_object == "residue": continue if attr.singular not in new_attrs: - missing = (attr.singular for attr in self.attrs - if (attr.per_object == 'residue' and - attr.singular not in new_attrs)) - raise NoDataError("Missing the following attributes for the new" - " Residue: {}".format(', '.join(missing))) + missing = ( + attr.singular + for attr in self.attrs + if ( + attr.per_object == "residue" + and attr.singular not in new_attrs + ) + ) + raise NoDataError( + "Missing the following attributes for the new" + " Residue: {}".format(", ".join(missing)) + ) # Resize topology table residx = self.tt.add_Residue(segment.segindex) # Add new value to each attribute for attr in self.attrs: - if not attr.per_object == 'residue': + if not attr.per_object == "residue": continue newval = new_attrs[attr.singular] attr._add_new(newval) @@ -613,22 +646,28 @@ def add_Segment(self, **new_attrs): Added use of _add_new to resize topology attrs """ for attr in self.attrs: - if attr.per_object == 'segment': + if attr.per_object == "segment": if attr.singular not in new_attrs: - missing = (attr.singular for attr in self.attrs - if (attr.per_object == 'segment' and - attr.singular not in new_attrs)) - raise NoDataError("Missing the following attributes for the" - " new Segment: {}" - "".format(', '.join(missing))) + missing = ( + attr.singular + for attr in self.attrs + if ( + attr.per_object == "segment" + and attr.singular not in new_attrs + ) + ) + raise NoDataError( + "Missing the following attributes for the" + " new Segment: {}" + "".format(", ".join(missing)) + ) segidx = self.tt.add_Segment() for attr in self.attrs: - if not attr.per_object == 'segment': + if not attr.per_object == "segment": continue newval = new_attrs[attr.singular] attr._add_new(newval) return segidx - diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index d1b103e3410..359bd20c9c1 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -58,16 +58,30 @@ import numpy as np -from ..lib.util import (cached, convert_aa_code, iterable, warn_if_not_unique, - unique_int_1d, check_atomgroup_not_empty) +from ..lib.util import ( + cached, + convert_aa_code, + iterable, + warn_if_not_unique, + unique_int_1d, + check_atomgroup_not_empty, +) from ..lib import transformations, mdamath from ..exceptions import NoDataError, SelectionError from .topologyobjects import TopologyGroup from . import selection -from .groups import (ComponentBase, GroupBase, - Atom, Residue, Segment, - AtomGroup, ResidueGroup, SegmentGroup, - check_wrap_and_unwrap, _pbc_to_wrap) +from .groups import ( + ComponentBase, + GroupBase, + Atom, + Residue, + Segment, + AtomGroup, + ResidueGroup, + SegmentGroup, + check_wrap_and_unwrap, + _pbc_to_wrap, +) from .. import _TOPOLOGY_ATTRS, _TOPOLOGY_TRANSPLANTS, _TOPOLOGY_ATTRNAMES @@ -90,13 +104,17 @@ def set_X(self, group, values): values must be single value OR same length as group """ - _SINGLE_VALUE_ERROR = ("Setting {cls} {attrname} with wrong sized input. " - "Must use single value, length of supplied values: {lenvalues}.") + _SINGLE_VALUE_ERROR = ( + "Setting {cls} {attrname} with wrong sized input. " + "Must use single value, length of supplied values: {lenvalues}." + ) # Eg "Setting Residue resid with wrong sized input. Must use single value, length of supplied # values: 2." - _GROUP_VALUE_ERROR = ("Setting {group} {attrname} with wrong sized array. " - "Length {group}: {lengroup}, length of supplied values: {lenvalues}.") + _GROUP_VALUE_ERROR = ( + "Setting {group} {attrname} with wrong sized array. " + "Length {group}: {lengroup}, length of supplied values: {lenvalues}." + ) # Eg "Setting AtomGroup masses with wrong sized array. Length AtomGroup: 100, length of # supplied values: 50." @@ -116,14 +134,23 @@ def wrapper(attr, group, values): if isinstance(group, ComponentBase): if not val_len == 0: - raise ValueError(_SINGLE_VALUE_ERROR.format( - cls=group.__class__.__name__, attrname=attr.singular, - lenvalues=val_len)) + raise ValueError( + _SINGLE_VALUE_ERROR.format( + cls=group.__class__.__name__, + attrname=attr.singular, + lenvalues=val_len, + ) + ) else: if not (val_len == 0 or val_len == len(group)): - raise ValueError(_GROUP_VALUE_ERROR.format( - group=group.__class__.__name__, attrname=attr.attrname, - lengroup=len(group), lenvalues=val_len)) + raise ValueError( + _GROUP_VALUE_ERROR.format( + group=group.__class__.__name__, + attrname=attr.attrname, + lengroup=len(group), + lenvalues=val_len, + ) + ) # if everything went OK, continue with the function return func(attr, group, values) @@ -153,13 +180,13 @@ def _wronglevel_error(attr, group): # What level to go to before trying to set this attr if isinstance(attr, AtomAttr): - corr_classes = ('atoms', 'atom') + corr_classes = ("atoms", "atom") attr_level = 1 elif isinstance(attr, ResidueAttr): - corr_classes = ('residues', 'residue') + corr_classes = ("residues", "residue") attr_level = 2 elif isinstance(attr, SegmentAttr): - corr_classes = ('segments', 'segment') + corr_classes = ("segments", "segment") attr_level = 3 if isinstance(group, ComponentBase) and (attr_level > group_level): @@ -174,9 +201,13 @@ def _wronglevel_error(attr, group): err_msg = "Cannot set {attr} from {cls}. Use '{cls}.{correct}.{attr} = '" # eg "Cannot set masses from Residue. 'Use Residue.atoms.masses = '" - return NotImplementedError(err_msg.format( - attr=attrname, cls=group.__class__.__name__, correct=correct, - )) + return NotImplementedError( + err_msg.format( + attr=attrname, + cls=group.__class__.__name__, + correct=correct, + ) + ) def _build_stub(method_name, method, attribute_name): @@ -204,10 +235,11 @@ def _build_stub(method_name, method, attribute_name): ------- The stub. """ + def stub_method(self, *args, **kwargs): message = ( - '{class_name}.{method_name}() ' - 'not available; this requires {attribute_name}' + "{class_name}.{method_name}() " + "not available; this requires {attribute_name}" ).format( class_name=self.__class__.__name__, method_name=method_name, @@ -215,22 +247,22 @@ def stub_method(self, *args, **kwargs): ) raise NoDataError(message) - annotation = textwrap.dedent("""\ + annotation = textwrap.dedent( + """\ .. note:: This requires the underlying topology to have {}. Otherwise, a :exc:`~MDAnalysis.exceptions.NoDataError` is raised. - """.format(attribute_name)) - # The first line of the original docstring is not indented, but the - # subsequent lines are. We want to dedent the whole docstring. - first_line, other_lines = method.__doc__.split('\n', 1) - stub_method.__doc__ = ( - first_line + '\n' - + textwrap.dedent(other_lines) - + '\n\n' + annotation + """.format( + attribute_name + ) ) + # The original docstring is assumed to be formatted with black + # (i.e., *all text* lines are already indented, the first line + # is a triple quote followed by newline) + stub_method.__doc__ = textwrap.dedent(method.__doc__) + "\n\n" + annotation stub_method.__name__ = method_name stub_method.__signature__ = inspect_signature(method) return stub_method @@ -251,10 +283,11 @@ def _attach_transplant_stubs(attribute_name, topology_attribute_class): """ transplants = topology_attribute_class.transplants for dest_class, methods in transplants.items(): - if dest_class == 'Universe': + if dest_class == "Universe": # Cannot be imported at the top level, it creates issues with # circular imports. from .universe import Universe + dest_class = Universe for method_name, method_callback in methods: # Methods the name of which is prefixed by _ should not be accessed @@ -262,7 +295,7 @@ def _attach_transplant_stubs(attribute_name, topology_attribute_class): # only relevant for user-facing method and properties. Also, # methods _-prefixed can be operator methods, and we do not want # to overwrite these with a stub. - if method_name.startswith('_'): + if method_name.startswith("_"): continue is_property = False @@ -279,15 +312,16 @@ def _attach_transplant_stubs(attribute_name, topology_attribute_class): # TODO: remove bfactors in 3.0 -BFACTOR_WARNING = ("The bfactor topology attribute is only " - "provided as an alias to the tempfactor " - "attribute. It will be removed in " - "3.0. Please use the tempfactor attribute " - "instead.") +BFACTOR_WARNING = ( + "The bfactor topology attribute is only " + "provided as an alias to the tempfactor " + "attribute. It will be removed in " + "3.0. Please use the tempfactor attribute " + "instead." +) def deprecate_bfactor_warning(func): - def wrapper(*args, **kwargs): """ Bfactor alias with warning @@ -321,26 +355,26 @@ class _TopologyAttrMeta(type): def __init__(cls, name, bases, classdict): type.__init__(type, name, bases, classdict) - attrname = classdict.get('attrname') - singular = classdict.get('singular', attrname) + attrname = classdict.get("attrname") + singular = classdict.get("singular", attrname) if attrname is None: attrname = singular if singular: _TOPOLOGY_ATTRS[singular] = _TOPOLOGY_ATTRS[attrname] = cls - _singular = singular.lower().replace('_', '') - _attrname = attrname.lower().replace('_', '') + _singular = singular.lower().replace("_", "") + _attrname = attrname.lower().replace("_", "") _TOPOLOGY_ATTRNAMES[_singular] = singular _TOPOLOGY_ATTRNAMES[_attrname] = attrname for clstype, transplants in cls.transplants.items(): for name, method in transplants: _TOPOLOGY_TRANSPLANTS[name] = [attrname, method, clstype] - clean = name.lower().replace('_', '') + clean = name.lower().replace("_", "") _TOPOLOGY_ATTRNAMES[clean] = name - for attr in ['singular', 'attrname']: + for attr in ["singular", "attrname"]: try: attrname = classdict[attr] except KeyError: @@ -362,22 +396,28 @@ def __init__(cls, name, bases, classdict): if dtype is not None: per_obj = classdict.get("per_object", bases[0].per_object) try: - selection.gen_selection_class(singular, attrname, - dtype, per_obj) + selection.gen_selection_class( + singular, attrname, dtype, per_obj + ) except ValueError: - msg = ("A selection keyword could not be " - "automatically generated for the " - f"{singular} attribute. If you need a " - "selection keyword, define it manually " - "by subclassing core.selection.Selection") + msg = ( + "A selection keyword could not be " + "automatically generated for the " + f"{singular} attribute. If you need a " + "selection keyword, define it manually " + "by subclassing core.selection.Selection" + ) warnings.warn(msg) # TODO: remove in 3.0 if attrname == "tempfactors": _TOPOLOGY_ATTRS["bfactor"] = _TOPOLOGY_ATTRS["bfactors"] = cls - selcls = selection.gen_selection_class("bfactor", "bfactors", - classdict.get("dtype"), - per_object="atom") + selcls = selection.gen_selection_class( + "bfactor", + "bfactors", + classdict.get("dtype"), + per_object="atom", + ) selcls.apply = deprecate_bfactor_warning(selcls.apply) @@ -406,8 +446,9 @@ class TopologyAttr(object, metaclass=_TopologyAttrMeta): handle for the Topology object TopologyAttr is associated with """ - attrname = 'topologyattrs' - singular = 'topologyattr' + + attrname = "topologyattrs" + singular = "topologyattr" per_object = None # ie Resids per_object = 'residue' top = None # pointer to Topology object transplants = defaultdict(list) @@ -435,8 +476,9 @@ def _gen_initial_values(n_atoms, n_residues, n_segments): raise NotImplementedError("No default values") @classmethod - def from_blank(cls, n_atoms=None, n_residues=None, n_segments=None, - values=None): + def from_blank( + cls, n_atoms=None, n_residues=None, n_segments=None, values=None + ): """Create a blank version of this TopologyAttribute Parameters @@ -524,13 +566,14 @@ def are_values_missing(cls, values): .. versionadded:: 2.8.0 """ - missing_value_label = getattr(cls, 'missing_value_label', None) + 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 @@ -546,8 +589,9 @@ class Atomindices(TopologyAttr): elements in that group. """ - attrname = 'indices' - singular = 'index' + + attrname = "indices" + singular = "index" target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom] dtype = int @@ -579,8 +623,9 @@ class Resindices(TopologyAttr): order of the elements in that group. """ - attrname = 'resindices' - singular = 'resindex' + + attrname = "resindices" + singular = "resindex" target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue] dtype = int @@ -613,11 +658,18 @@ class Segindices(TopologyAttr): order of the elements in that group. """ - attrname = 'segindices' - singular = 'segindex' + + attrname = "segindices" + singular = "segindex" dtype = int - target_classes = [AtomGroup, ResidueGroup, SegmentGroup, - Atom, Residue, Segment] + target_classes = [ + AtomGroup, + ResidueGroup, + SegmentGroup, + Atom, + Residue, + Segment, + ] def __init__(self): self._guessed = False @@ -639,11 +691,10 @@ def set_segments(self, sg, values): class AtomAttr(TopologyAttr): - """Base class for atom attributes. + """Base class for atom attributes.""" - """ - attrname = 'atomattrs' - singular = 'atomattr' + attrname = "atomattrs" + singular = "atomattr" target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom] def get_atoms(self, ag): @@ -680,11 +731,11 @@ def set_segments(self, sg, values): # TODO: update docs to property doc class Atomids(AtomAttr): - """ID for each atom. - """ - attrname = 'ids' - singular = 'id' - per_object = 'atom' + """ID for each atom.""" + + attrname = "ids" + singular = "id" + per_object = "atom" dtype = int @staticmethod @@ -778,7 +829,9 @@ def _set_X(self, ag, values): newnames.append(val) newidx[i] = nextidx - self.nmidx[ag.ix] = newidx # newidx either single value or same size array + self.nmidx[ag.ix] = ( + newidx # newidx either single value or same size array + ) if newnames: self.name_lookup = np.concatenate([self.name_lookup, newnames]) self.values = self.name_lookup[self.nmidx] @@ -787,27 +840,26 @@ def _set_X(self, ag, values): # woe betide anyone who switches this inheritance order # Mixin needs to be first (L to R) to get correct __init__ and set_atoms class AtomStringAttr(_StringInternerMixin, AtomAttr): - @_check_length def set_atoms(self, ag, values): return self._set_X(ag, values) @staticmethod def _gen_initial_values(na, nr, ns): - return np.full(na, '', dtype=object) + return np.full(na, "", dtype=object) # TODO: update docs to property doc class Atomnames(AtomStringAttr): - """Name for each atom. - """ - attrname = 'names' - singular = 'name' - per_object = 'atom' + """Name for each atom.""" + + attrname = "names" + singular = "name" + per_object = "atom" dtype = object transplants = defaultdict(list) - def phi_selection(residue, c_name='C', n_name='N', ca_name='CA'): + def phi_selection(residue, c_name="C", n_name="N", ca_name="CA"): """Select AtomGroup corresponding to the phi protein backbone dihedral C'-N-CA-C. @@ -831,11 +883,11 @@ def phi_selection(residue, c_name='C', n_name='N', ca_name='CA'): faster atom matching with boolean arrays. """ # fnmatch is expensive. try the obv candidate first - prev = residue.universe.residues[residue.ix-1] + prev = residue.universe.residues[residue.ix - 1] sid = residue.segment.segid - rid = residue.resid-1 + rid = residue.resid - 1 if not (prev.segment.segid == sid and prev.resid == rid): - sel = 'segid {} and resid {}'.format(sid, rid) + sel = "segid {} and resid {}".format(sid, rid) try: prev = residue.universe.select_atoms(sel).residues[0] except IndexError: @@ -850,12 +902,12 @@ def phi_selection(residue, c_name='C', n_name='N', ca_name='CA'): if not all(len(ag) == 1 for ag in ncac): return None - sel = c_+sum(ncac) + sel = c_ + sum(ncac) return sel - transplants[Residue].append(('phi_selection', phi_selection)) + transplants[Residue].append(("phi_selection", phi_selection)) - def phi_selections(residues, c_name='C', n_name='N', ca_name='CA'): + def phi_selections(residues, c_name="C", n_name="N", ca_name="CA"): """Select list of AtomGroups corresponding to the phi protein backbone dihedral C'-N-CA-C. @@ -882,11 +934,11 @@ def phi_selections(residues, c_name='C', n_name='N', ca_name='CA'): return [] u = residues[0].universe - prev = u.residues[residues.ix-1] # obv candidates first + prev = u.residues[residues.ix - 1] # obv candidates first rsid = residues.segids - prid = residues.resids-1 + prid = residues.resids - 1 ncac_names = [n_name, ca_name, c_name] - sel = 'segid {} and resid {}' + sel = "segid {} and resid {}" # replace wrong residues wix = np.where((prev.segids != rsid) | (prev.resids != prid))[0] @@ -901,8 +953,10 @@ def phi_selections(residues, c_name='C', n_name='N', ca_name='CA'): prev = sum(prevls) keep_prev = [sum(r.atoms.names == c_name) == 1 for r in prev] - keep_res = [all(sum(r.atoms.names == n) == 1 for n in ncac_names) - for r in residues] + keep_res = [ + all(sum(r.atoms.names == n) == 1 for n in ncac_names) + for r in residues + ] keep = np.array(keep_prev) & np.array(keep_res) keep[invalid] = False results = np.zeros_like(residues, dtype=object) @@ -918,9 +972,9 @@ def phi_selections(residues, c_name='C', n_name='N', ca_name='CA'): results[keepix] = [sum(atoms) for atoms in zip(c_, n, ca, c)] return list(results) - transplants[ResidueGroup].append(('phi_selections', phi_selections)) + transplants[ResidueGroup].append(("phi_selections", phi_selections)) - def psi_selection(residue, c_name='C', n_name='N', ca_name='CA'): + def psi_selection(residue, c_name="C", n_name="N", ca_name="CA"): """Select AtomGroup corresponding to the psi protein backbone dihedral N-CA-C-N'. @@ -947,9 +1001,9 @@ def psi_selection(residue, c_name='C', n_name='N', ca_name='CA'): # fnmatch is expensive. try the obv candidate first _manual_sel = False sid = residue.segment.segid - rid = residue.resid+1 + rid = residue.resid + 1 try: - nxt = residue.universe.residues[residue.ix+1] + nxt = residue.universe.residues[residue.ix + 1] except IndexError: _manual_sel = True else: @@ -957,7 +1011,7 @@ def psi_selection(residue, c_name='C', n_name='N', ca_name='CA'): _manual_sel = True if _manual_sel: - sel = 'segid {} and resid {}'.format(sid, rid) + sel = "segid {} and resid {}".format(sid, rid) try: nxt = residue.universe.select_atoms(sel).residues[0] except IndexError: @@ -975,7 +1029,7 @@ def psi_selection(residue, c_name='C', n_name='N', ca_name='CA'): sel = sum(ncac) + n_ return sel - transplants[Residue].append(('psi_selection', psi_selection)) + transplants[Residue].append(("psi_selection", psi_selection)) def _get_next_residues_by_resid(residues): """Select list of Residues corresponding to the next resid for each @@ -993,19 +1047,19 @@ def _get_next_residues_by_resid(residues): u = residues[0].universe except IndexError: return residues - nxres = np.array([None]*len(residues), dtype=object) + nxres = np.array([None] * len(residues), dtype=object) ix = np.arange(len(residues)) # no guarantee residues is ordered or unique last = max(residues.ix) - if last == len(u.residues)-1: + if last == len(u.residues) - 1: notlast = residues.ix != last ix = ix[notlast] residues = residues[notlast] - nxres[ix] = nxt = u.residues[residues.ix+1] + nxres[ix] = nxt = u.residues[residues.ix + 1] rsid = residues.segids - nrid = residues.resids+1 - sel = 'segid {} and resid {}' + nrid = residues.resids + 1 + sel = "segid {} and resid {}" # replace wrong residues wix = np.where((nxt.segids != rsid) | (nxt.resids != nrid))[0] @@ -1017,8 +1071,9 @@ def _get_next_residues_by_resid(residues): nxres[ix[i]] = None return nxres - transplants[ResidueGroup].append(('_get_next_residues_by_resid', - _get_next_residues_by_resid)) + transplants[ResidueGroup].append( + ("_get_next_residues_by_resid", _get_next_residues_by_resid) + ) def _get_prev_residues_by_resid(residues): """Select list of Residues corresponding to the previous resid for each @@ -1036,11 +1091,11 @@ def _get_prev_residues_by_resid(residues): u = residues[0].universe except IndexError: return residues - pvres = np.array([None]*len(residues)) - pvres[:] = prev = u.residues[residues.ix-1] + pvres = np.array([None] * len(residues)) + pvres[:] = prev = u.residues[residues.ix - 1] rsid = residues.segids - prid = residues.resids-1 - sel = 'segid {} and resid {}' + prid = residues.resids - 1 + sel = "segid {} and resid {}" # replace wrong residues wix = np.where((prev.segids != rsid) | (prev.resids != prid))[0] @@ -1052,10 +1107,11 @@ def _get_prev_residues_by_resid(residues): pvres[i] = None return pvres - transplants[ResidueGroup].append(('_get_prev_residues_by_resid', - _get_prev_residues_by_resid)) + transplants[ResidueGroup].append( + ("_get_prev_residues_by_resid", _get_prev_residues_by_resid) + ) - def psi_selections(residues, c_name='C', n_name='N', ca_name='CA'): + def psi_selections(residues, c_name="C", n_name="N", ca_name="CA"): """Select list of AtomGroups corresponding to the psi protein backbone dihedral N-CA-C-N'. @@ -1081,7 +1137,7 @@ def psi_selections(residues, c_name='C', n_name='N', ca_name='CA'): if not residues: return [] - results = np.array([None]*len(residues), dtype=object) + results = np.array([None] * len(residues), dtype=object) nxtres = residues._get_next_residues_by_resid() rix = np.where(nxtres)[0] nxt = sum(nxtres[rix]) @@ -1089,8 +1145,10 @@ def psi_selections(residues, c_name='C', n_name='N', ca_name='CA'): ncac_names = [n_name, ca_name, c_name] keep_nxt = [sum(r.atoms.names == n_name) == 1 for r in nxt] - keep_res = [all(sum(r.atoms.names == n) == 1 for n in ncac_names) - for r in residues] + keep_res = [ + all(sum(r.atoms.names == n) == 1 for n in ncac_names) + for r in residues + ] keep = np.array(keep_nxt) & np.array(keep_res) nxt = nxt[keep] residues = residues[keep] @@ -1103,9 +1161,9 @@ def psi_selections(residues, c_name='C', n_name='N', ca_name='CA'): results[rix[keepix]] = [sum(atoms) for atoms in zip(n, ca, c, n_)] return list(results) - transplants[ResidueGroup].append(('psi_selections', psi_selections)) + transplants[ResidueGroup].append(("psi_selections", psi_selections)) - def omega_selection(residue, c_name='C', n_name='N', ca_name='CA'): + def omega_selection(residue, c_name="C", n_name="N", ca_name="CA"): """Select AtomGroup corresponding to the omega protein backbone dihedral CA-C-N'-CA'. @@ -1135,9 +1193,9 @@ def omega_selection(residue, c_name='C', n_name='N', ca_name='CA'): # fnmatch is expensive. try the obv candidate first _manual_sel = False sid = residue.segment.segid - rid = residue.resid+1 + rid = residue.resid + 1 try: - nxt = residue.universe.residues[residue.ix+1] + nxt = residue.universe.residues[residue.ix + 1] except IndexError: _manual_sel = True else: @@ -1145,7 +1203,7 @@ def omega_selection(residue, c_name='C', n_name='N', ca_name='CA'): _manual_sel = True if _manual_sel: - sel = 'segid {} and resid {}'.format(sid, rid) + sel = "segid {} and resid {}".format(sid, rid) try: nxt = residue.universe.select_atoms(sel).residues[0] except IndexError: @@ -1159,11 +1217,11 @@ def omega_selection(residue, c_name='C', n_name='N', ca_name='CA'): if not all(len(ag) == 1 for ag in [ca_, n_, ca, c]): return None - return ca+c+n_+ca_ + return ca + c + n_ + ca_ - transplants[Residue].append(('omega_selection', omega_selection)) + transplants[Residue].append(("omega_selection", omega_selection)) - def omega_selections(residues, c_name='C', n_name='N', ca_name='CA'): + def omega_selections(residues, c_name="C", n_name="N", ca_name="CA"): """Select list of AtomGroups corresponding to the omega protein backbone dihedral CA-C-N'-CA'. @@ -1193,7 +1251,7 @@ def omega_selections(residues, c_name='C', n_name='N', ca_name='CA'): if not residues: return [] - results = np.array([None]*len(residues), dtype=object) + results = np.array([None] * len(residues), dtype=object) nxtres = residues._get_next_residues_by_resid() rix = np.where(nxtres)[0] nxt = sum(nxtres[rix]) @@ -1201,10 +1259,13 @@ def omega_selections(residues, c_name='C', n_name='N', ca_name='CA'): nxtatoms = [ca_name, n_name] resatoms = [ca_name, c_name] - keep_nxt = [all(sum(r.atoms.names == n) == 1 for n in nxtatoms) - for r in nxt] - keep_res = [all(sum(r.atoms.names == n) == 1 for n in resatoms) - for r in residues] + keep_nxt = [ + all(sum(r.atoms.names == n) == 1 for n in nxtatoms) for r in nxt + ] + keep_res = [ + all(sum(r.atoms.names == n) == 1 for n in resatoms) + for r in residues + ] keep = np.array(keep_nxt) & np.array(keep_res) nxt = nxt[keep] residues = residues[keep] @@ -1218,10 +1279,15 @@ def omega_selections(residues, c_name='C', n_name='N', ca_name='CA'): results[rix[keepix]] = [sum(atoms) for atoms in zip(ca, c, n_, ca_)] return list(results) - transplants[ResidueGroup].append(('omega_selections', omega_selections)) + transplants[ResidueGroup].append(("omega_selections", omega_selections)) - def chi1_selection(residue, n_name='N', ca_name='CA', cb_name='CB', - cg_name='CG CG1 OG OG1 SG'): + def chi1_selection( + residue, + n_name="N", + ca_name="CA", + cb_name="CB", + cg_name="CG CG1 OG OG1 SG", + ): r"""Select AtomGroup corresponding to the chi1 sidechain dihedral ``N-CA-CB-*G.`` The gamma atom is taken to be the heavy atom in the gamma position. If more than one heavy atom is present (e.g. CG1 and CG2), the one with the lower number is used (CG1). @@ -1264,10 +1330,15 @@ def chi1_selection(residue, n_name='N', ca_name='CA', cb_name='CB', return None return sum(ags) - transplants[Residue].append(('chi1_selection', chi1_selection)) + transplants[Residue].append(("chi1_selection", chi1_selection)) - def chi1_selections(residues, n_name='N', ca_name='CA', cb_name='CB', - cg_name='CG CG1 OG OG1 SG'): + def chi1_selections( + residues, + n_name="N", + ca_name="CA", + cb_name="CB", + cg_name="CG CG1 OG OG1 SG", + ): """Select list of AtomGroups corresponding to the chi1 sidechain dihedral N-CA-CB-CG. @@ -1294,10 +1365,12 @@ def chi1_selections(residues, n_name='N', ca_name='CA', cb_name='CB', if not residues: return [] - results = np.array([None]*len(residues)) + results = np.array([None] * len(residues)) names = [n_name, ca_name, cb_name, cg_name] - keep = [all(sum(np.isin(r.atoms.names, n.split())) == 1 - for n in names) for r in residues] + keep = [ + all(sum(np.isin(r.atoms.names, n.split())) == 1 for n in names) + for r in residues + ] keepix = np.where(keep)[0] residues = residues[keep] @@ -1306,36 +1379,39 @@ def chi1_selections(residues, n_name='N', ca_name='CA', cb_name='CB', results[keepix] = [sum(atoms) for atoms in zip(*ags)] return list(results) - transplants[ResidueGroup].append(('chi1_selections', chi1_selections)) + transplants[ResidueGroup].append(("chi1_selections", chi1_selections)) # TODO: update docs to property doc class Atomtypes(AtomStringAttr): """Type for each atom""" - attrname = 'types' - singular = 'type' - per_object = 'atom' + + attrname = "types" + singular = "type" + per_object = "atom" dtype = object # TODO: update docs to property doc class Elements(AtomStringAttr): """Element for each atom""" - attrname = 'elements' - singular = 'element' + + attrname = "elements" + singular = "element" dtype = object @staticmethod def _gen_initial_values(na, nr, ns): - return np.array(['' for _ in range(na)], dtype=object) + return np.array(["" for _ in range(na)], dtype=object) # TODO: update docs to property doc class Radii(AtomAttr): """Radii for each atom""" - attrname = 'radii' - singular = 'radius' - per_object = 'atom' + + attrname = "radii" + singular = "radius" + per_object = "atom" dtype = float @staticmethod @@ -1351,14 +1427,15 @@ class RecordTypes(AtomStringAttr): .. versionchanged:: 0.20.0 Now stores array of dtype object rather than boolean mapping """ - attrname = 'record_types' - singular = 'record_type' - per_object = 'atom' + + attrname = "record_types" + singular = "record_type" + per_object = "atom" dtype = object @staticmethod def _gen_initial_values(na, nr, ns): - return np.array(['ATOM'] * na, dtype=object) + return np.array(["ATOM"] * na, dtype=object) class ChainIDs(AtomStringAttr): @@ -1368,17 +1445,19 @@ class ChainIDs(AtomStringAttr): ---- This is an attribute of the Atom, not Residue or Segment """ - attrname = 'chainIDs' - singular = 'chainID' - per_object = 'atom' + + attrname = "chainIDs" + singular = "chainID" + per_object = "atom" dtype = object class Tempfactors(AtomAttr): """Tempfactor for atoms""" - attrname = 'tempfactors' - singular = 'tempfactor' - per_object = 'atom' + + attrname = "tempfactors" + singular = "tempfactor" + per_object = "atom" dtype = float transplants = defaultdict(list) @@ -1440,22 +1519,31 @@ def bfactors_setter(self, value): self.universe.atoms[self.atoms.ix].tempfactors = value transplants[Atom].append( - ('bfactor', property(bfactor, bfactor_setter, None, - bfactor.__doc__))) + ("bfactor", property(bfactor, bfactor_setter, None, bfactor.__doc__)) + ) for group in (AtomGroup, Residue, ResidueGroup, Segment, SegmentGroup): transplants[group].append( - ("bfactors", property(bfactors, bfactors_setter, None, - bfactors.__doc__))) + ( + "bfactors", + property(bfactors, bfactors_setter, None, bfactors.__doc__), + ) + ) class Masses(AtomAttr): - attrname = 'masses' - singular = 'mass' - per_object = 'atom' + attrname = "masses" + singular = "mass" + per_object = "atom" missing_value_label = np.nan - target_classes = [AtomGroup, ResidueGroup, SegmentGroup, - Atom, Residue, Segment] + target_classes = [ + AtomGroup, + ResidueGroup, + SegmentGroup, + Atom, + Residue, + Segment, + ] transplants = defaultdict(list) dtype = np.float64 @@ -1503,7 +1591,7 @@ def get_segments(self, sg): @_pbc_to_wrap @check_wrap_and_unwrap @check_atomgroup_not_empty - def center_of_mass(group, wrap=False, unwrap=False, compound='group'): + def center_of_mass(group, wrap=False, unwrap=False, compound="group"): r"""Center of mass of (compounds of) the group .. math:: @@ -1569,15 +1657,15 @@ def center_of_mass(group, wrap=False, unwrap=False, compound='group'): is deprecated and will be removed in version 3.0. """ atoms = group.atoms - return atoms.center(weights=atoms.masses, wrap=wrap, compound=compound, - unwrap=unwrap) + return atoms.center( + weights=atoms.masses, wrap=wrap, compound=compound, unwrap=unwrap + ) - transplants[GroupBase].append( - ('center_of_mass', center_of_mass)) + transplants[GroupBase].append(("center_of_mass", center_of_mass)) @warn_if_not_unique @check_atomgroup_not_empty - def total_mass(group, compound='group'): + def total_mass(group, compound="group"): r"""Total mass of (compounds of) the group. Computes the total mass of :class:`Atoms` in the group. @@ -1609,8 +1697,7 @@ def total_mass(group, compound='group'): """ return group.accumulate("masses", compound=compound) - transplants[GroupBase].append( - ('total_mass', total_mass)) + transplants[GroupBase].append(("total_mass", total_mass)) @warn_if_not_unique @_pbc_to_wrap @@ -1680,10 +1767,12 @@ def moment_of_inertia(group, wrap=False, unwrap=False, compound="group"): atomgroup = group.atoms com = atomgroup.center_of_mass( - wrap=wrap, unwrap=unwrap, compound=compound) - if compound != 'group': - com = (com * group.masses[:, None] - ).sum(axis=0) / group.masses.sum() + wrap=wrap, unwrap=unwrap, compound=compound + ) + if compound != "group": + com = (com * group.masses[:, None]).sum( + axis=0 + ) / group.masses.sum() if wrap: pos = atomgroup.pack_into_box(inplace=False) - com @@ -1706,20 +1795,19 @@ def moment_of_inertia(group, wrap=False, unwrap=False, compound="group"): # xx tens[0][0] = (masses * (pos[:, 1] ** 2 + pos[:, 2] ** 2)).sum() # xy & yx - tens[0][1] = tens[1][0] = - (masses * pos[:, 0] * pos[:, 1]).sum() + tens[0][1] = tens[1][0] = -(masses * pos[:, 0] * pos[:, 1]).sum() # xz & zx - tens[0][2] = tens[2][0] = - (masses * pos[:, 0] * pos[:, 2]).sum() + tens[0][2] = tens[2][0] = -(masses * pos[:, 0] * pos[:, 2]).sum() # yy tens[1][1] = (masses * (pos[:, 0] ** 2 + pos[:, 2] ** 2)).sum() # yz + zy - tens[1][2] = tens[2][1] = - (masses * pos[:, 1] * pos[:, 2]).sum() + tens[1][2] = tens[2][1] = -(masses * pos[:, 1] * pos[:, 2]).sum() # zz tens[2][2] = (masses * (pos[:, 0] ** 2 + pos[:, 1] ** 2)).sum() return tens - transplants[GroupBase].append( - ('moment_of_inertia', moment_of_inertia)) + transplants[GroupBase].append(("moment_of_inertia", moment_of_inertia)) @warn_if_not_unique @_pbc_to_wrap @@ -1749,26 +1837,31 @@ def radius_of_gyration(group, wrap=False, **kwargs): else: recenteredpos = atomgroup.positions - com - rog_sq = np.einsum('i,i->',masses,np.einsum('ij,ij->i', - recenteredpos,recenteredpos))/atomgroup.total_mass() + rog_sq = ( + np.einsum( + "i,i->", + masses, + np.einsum("ij,ij->i", recenteredpos, recenteredpos), + ) + / atomgroup.total_mass() + ) return np.sqrt(rog_sq) - transplants[GroupBase].append( - ('radius_of_gyration', radius_of_gyration)) + transplants[GroupBase].append(("radius_of_gyration", radius_of_gyration)) @warn_if_not_unique @_pbc_to_wrap @check_atomgroup_not_empty - def gyration_moments(group, wrap=False, unwrap=False, compound='group'): + def gyration_moments(group, wrap=False, unwrap=False, compound="group"): r"""Moments of the gyration tensor. The moments are defined as the eigenvalues of the gyration tensor. .. math:: - - \mathsf{T} = \frac{1}{N} \sum_{i=1}^{N} (\mathbf{r}_\mathrm{i} - + + \mathsf{T} = \frac{1}{N} \sum_{i=1}^{N} (\mathbf{r}_\mathrm{i} - \mathbf{r}_\mathrm{COM})(\mathbf{r}_\mathrm{i} - \mathbf{r}_\mathrm{COM}) Where :math:`\mathbf{r}_\mathrm{COM}` is the center of mass. @@ -1800,60 +1893,65 @@ def gyration_moments(group, wrap=False, unwrap=False, compound='group'): def _gyration(recenteredpos, masses): if len(masses.shape) > 1: masses = np.squeeze(masses) - tensor = np.einsum( "ki,kj->ij", - recenteredpos, - np.einsum("ij,i->ij", recenteredpos, masses), - ) - return np.linalg.eigvalsh(tensor/np.sum(masses)) + tensor = np.einsum( + "ki,kj->ij", + recenteredpos, + np.einsum("ij,i->ij", recenteredpos, masses), + ) + return np.linalg.eigvalsh(tensor / np.sum(masses)) atomgroup = group.atoms masses = atomgroup.masses com = atomgroup.center_of_mass( - wrap=wrap, unwrap=unwrap, compound=compound) - - if compound == 'group': - if wrap: - recenteredpos = (atomgroup.pack_into_box(inplace=False) - com) - elif unwrap: - recenteredpos = (atomgroup.unwrap(inplace=False, - compound=compound, - reference=None, - ) - com) - else: - recenteredpos = (atomgroup.positions - com) - eig_vals = _gyration(recenteredpos, masses) + wrap=wrap, unwrap=unwrap, compound=compound + ) + + if compound == "group": + if wrap: + recenteredpos = atomgroup.pack_into_box(inplace=False) - com + elif unwrap: + recenteredpos = ( + atomgroup.unwrap( + inplace=False, + compound=compound, + reference=None, + ) + - com + ) + else: + recenteredpos = atomgroup.positions - com + eig_vals = _gyration(recenteredpos, masses) else: - (atom_masks, - compound_masks, - n_compounds) = atomgroup._split_by_compound_indices(compound) - - if unwrap: - coords = atomgroup.unwrap( - compound=compound, - reference=None, - inplace=False - ) - else: - coords = atomgroup.positions - - eig_vals = np.empty((n_compounds, 3), dtype=np.float64) - for compound_mask, atom_mask in zip(compound_masks, atom_masks): - eig_vals[compound_mask, :] = [_gyration( - coords[mask] - com[compound_mask][i], - masses[mask][:, None] - ) for i, mask in enumerate(atom_mask)] + (atom_masks, compound_masks, n_compounds) = ( + atomgroup._split_by_compound_indices(compound) + ) - return eig_vals + if unwrap: + coords = atomgroup.unwrap( + compound=compound, reference=None, inplace=False + ) + else: + coords = atomgroup.positions - transplants[GroupBase].append( - ('gyration_moments', gyration_moments)) + eig_vals = np.empty((n_compounds, 3), dtype=np.float64) + for compound_mask, atom_mask in zip(compound_masks, atom_masks): + eig_vals[compound_mask, :] = [ + _gyration( + coords[mask] - com[compound_mask][i], + masses[mask][:, None], + ) + for i, mask in enumerate(atom_mask) + ] + return eig_vals + + transplants[GroupBase].append(("gyration_moments", gyration_moments)) @warn_if_not_unique @_pbc_to_wrap @check_atomgroup_not_empty - def shape_parameter(group, wrap=False, unwrap=False, compound='group'): + def shape_parameter(group, wrap=False, unwrap=False, compound="group"): """Shape parameter. See [Dima2004a]_ for background information. @@ -1880,24 +1978,31 @@ def shape_parameter(group, wrap=False, unwrap=False, compound='group'): Added calculation for any `compound` type """ atomgroup = group.atoms - eig_vals = atomgroup.gyration_moments(wrap=wrap, unwrap=unwrap, compound=compound) + eig_vals = atomgroup.gyration_moments( + wrap=wrap, unwrap=unwrap, compound=compound + ) if len(eig_vals.shape) > 1: - shape = 27.0 * np.prod(eig_vals - np.mean(eig_vals, axis=1), axis=1 - ) / np.power(np.sum(eig_vals, axis=1), 3) + shape = ( + 27.0 + * np.prod(eig_vals - np.mean(eig_vals, axis=1), axis=1) + / np.power(np.sum(eig_vals, axis=1), 3) + ) else: - shape = 27.0 * np.prod(eig_vals - np.mean(eig_vals) - ) / np.power(np.sum(eig_vals), 3) + shape = ( + 27.0 + * np.prod(eig_vals - np.mean(eig_vals)) + / np.power(np.sum(eig_vals), 3) + ) return shape - transplants[GroupBase].append( - ('shape_parameter', shape_parameter)) + transplants[GroupBase].append(("shape_parameter", shape_parameter)) @warn_if_not_unique @_pbc_to_wrap @check_wrap_and_unwrap @check_atomgroup_not_empty - def asphericity(group, wrap=False, unwrap=False, compound='group'): + def asphericity(group, wrap=False, unwrap=False, compound="group"): """Asphericity. See [Dima2004b]_ for background information. @@ -1925,18 +2030,23 @@ def asphericity(group, wrap=False, unwrap=False, compound='group'): Added calculation for any `compound` type """ atomgroup = group.atoms - eig_vals = atomgroup.gyration_moments(wrap=wrap, unwrap=unwrap, compound=compound) + eig_vals = atomgroup.gyration_moments( + wrap=wrap, unwrap=unwrap, compound=compound + ) if len(eig_vals.shape) > 1: - shape = (3.0 / 2.0) * (np.sum((eig_vals - np.mean(eig_vals, axis=1))**2, axis=1) / - np.sum(eig_vals, axis=1)**2) + shape = (3.0 / 2.0) * ( + np.sum((eig_vals - np.mean(eig_vals, axis=1)) ** 2, axis=1) + / np.sum(eig_vals, axis=1) ** 2 + ) else: - shape = (3.0 / 2.0) * (np.sum((eig_vals - np.mean(eig_vals))**2) / - np.sum(eig_vals)**2) + shape = (3.0 / 2.0) * ( + np.sum((eig_vals - np.mean(eig_vals)) ** 2) + / np.sum(eig_vals) ** 2 + ) return shape - transplants[GroupBase].append( - ('asphericity', asphericity)) + transplants[GroupBase].append(("asphericity", asphericity)) @warn_if_not_unique @_pbc_to_wrap @@ -1987,8 +2097,7 @@ def principal_axes(group, wrap=False): return e_vec - transplants[GroupBase].append( - ('principal_axes', principal_axes)) + transplants[GroupBase].append(("principal_axes", principal_axes)) def align_principal_axis(group, axis, vector): """Align principal axis with index `axis` with `vector`. @@ -2017,16 +2126,23 @@ def align_principal_axis(group, axis, vector): return group.rotateby(angle, ax) transplants[GroupBase].append( - ('align_principal_axis', align_principal_axis)) + ("align_principal_axis", align_principal_axis) + ) # TODO: update docs to property doc class Charges(AtomAttr): - attrname = 'charges' - singular = 'charge' - per_object = 'atom' - target_classes = [AtomGroup, ResidueGroup, SegmentGroup, - Atom, Residue, Segment] + attrname = "charges" + singular = "charge" + per_object = "atom" + target_classes = [ + AtomGroup, + ResidueGroup, + SegmentGroup, + Atom, + Residue, + Segment, + ] transplants = defaultdict(list) dtype = float @@ -2062,7 +2178,7 @@ def get_segments(self, sg): @_pbc_to_wrap @check_wrap_and_unwrap @check_atomgroup_not_empty - def center_of_charge(group, wrap=False, unwrap=False, compound='group'): + def center_of_charge(group, wrap=False, unwrap=False, compound="group"): r"""Center of (absolute) charge of (compounds of) the group .. math:: @@ -2120,18 +2236,18 @@ def center_of_charge(group, wrap=False, unwrap=False, compound='group'): .. versionadded:: 2.2.0 """ atoms = group.atoms - return atoms.center(weights=atoms.charges.__abs__(), - wrap=wrap, - compound=compound, - unwrap=unwrap) - + return atoms.center( + weights=atoms.charges.__abs__(), + wrap=wrap, + compound=compound, + unwrap=unwrap, + ) - transplants[GroupBase].append( - ('center_of_charge', center_of_charge)) + transplants[GroupBase].append(("center_of_charge", center_of_charge)) @warn_if_not_unique @check_atomgroup_not_empty - def total_charge(group, compound='group'): + def total_charge(group, compound="group"): r"""Total charge of (compounds of) the group. Computes the total charge of :class:`Atoms` in the group. @@ -2163,17 +2279,14 @@ def total_charge(group, compound='group'): """ return group.accumulate("charges", compound=compound) - transplants[GroupBase].append( - ('total_charge', total_charge)) + transplants[GroupBase].append(("total_charge", total_charge)) @warn_if_not_unique @_pbc_to_wrap @check_wrap_and_unwrap - def dipole_vector(group, - wrap=False, - unwrap=False, - compound='group', - center="mass"): + def dipole_vector( + group, wrap=False, unwrap=False, compound="group", center="mass" + ): r"""Dipole vector of the group. .. math:: @@ -2239,54 +2352,61 @@ def dipole_vector(group, if center == "mass": masses = atomgroup.masses - ref = atomgroup.center_of_mass(wrap=wrap, - unwrap=unwrap, - compound=compound) + ref = atomgroup.center_of_mass( + wrap=wrap, unwrap=unwrap, compound=compound + ) elif center == "charge": - ref = atomgroup.center_of_charge(wrap=wrap, - unwrap=unwrap, - compound=compound) + ref = atomgroup.center_of_charge( + wrap=wrap, unwrap=unwrap, compound=compound + ) else: choices = ["mass", "charge"] raise ValueError( f"The dipole center, {center}, is not supported. Choose" - " one of: {choices}") + " one of: {choices}" + ) - if compound == 'group': + if compound == "group": if wrap: - recenteredpos = (atomgroup.pack_into_box(inplace=False) - ref) + recenteredpos = atomgroup.pack_into_box(inplace=False) - ref elif unwrap: - recenteredpos = (atomgroup.unwrap( - inplace=False, - compound=compound, - reference=None, - ) - ref) + recenteredpos = ( + atomgroup.unwrap( + inplace=False, + compound=compound, + reference=None, + ) + - ref + ) else: - recenteredpos = (atomgroup.positions - ref) - dipole_vector = np.einsum('ij,ij->j',recenteredpos, - charges[:, np.newaxis]) + recenteredpos = atomgroup.positions - ref + dipole_vector = np.einsum( + "ij,ij->j", recenteredpos, charges[:, np.newaxis] + ) else: - (atom_masks, compound_masks, - n_compounds) = atomgroup._split_by_compound_indices(compound) + (atom_masks, compound_masks, n_compounds) = ( + atomgroup._split_by_compound_indices(compound) + ) if unwrap: - coords = atomgroup.unwrap(compound=compound, - reference=None, - inplace=False) + coords = atomgroup.unwrap( + compound=compound, reference=None, inplace=False + ) else: coords = atomgroup.positions chgs = atomgroup.charges dipole_vector = np.empty((n_compounds, 3), dtype=np.float64) for compound_mask, atom_mask in zip(compound_masks, atom_masks): - dipole_vector[compound_mask] = np.einsum('ijk,ijk->ik', - (coords[atom_mask]- - ref[compound_mask][:, None, :]), - chgs[atom_mask][:, :, None]) + dipole_vector[compound_mask] = np.einsum( + "ijk,ijk->ik", + (coords[atom_mask] - ref[compound_mask][:, None, :]), + chgs[atom_mask][:, :, None], + ) return dipole_vector - transplants[GroupBase].append(('dipole_vector', dipole_vector)) + transplants[GroupBase].append(("dipole_vector", dipole_vector)) def dipole_moment(group, **kwargs): r"""Dipole moment of the group or compounds in a group. @@ -2346,22 +2466,24 @@ def dipole_moment(group, **kwargs): dipole_vector = atomgroup.dipole_vector(**kwargs) if len(dipole_vector.shape) > 1: - dipole_moment = np.sqrt(np.einsum('ij,ij->i',dipole_vector,dipole_vector)) + dipole_moment = np.sqrt( + np.einsum("ij,ij->i", dipole_vector, dipole_vector) + ) else: - dipole_moment = np.sqrt(np.einsum('i,i->',dipole_vector,dipole_vector)) + dipole_moment = np.sqrt( + np.einsum("i,i->", dipole_vector, dipole_vector) + ) return dipole_moment - transplants[GroupBase].append(('dipole_moment', dipole_moment)) + transplants[GroupBase].append(("dipole_moment", dipole_moment)) @warn_if_not_unique @_pbc_to_wrap @check_wrap_and_unwrap - def quadrupole_tensor(group, - wrap=False, - unwrap=False, - compound='group', - center="mass"): + def quadrupole_tensor( + group, wrap=False, unwrap=False, compound="group", center="mass" + ): r"""Traceless quadrupole tensor of the group or compounds. This tensor is first computed as the outer product of vectors from @@ -2426,8 +2548,7 @@ def quadrupole_tensor(group, """ def __quadrupole_tensor(recenteredpos, charges): - """ Calculate the traceless quadrupole tensor - """ + """Calculate the traceless quadrupole tensor""" if len(charges.shape) > 1: charges = np.squeeze(charges) tensor = np.einsum( @@ -2444,39 +2565,44 @@ def __quadrupole_tensor(recenteredpos, charges): if center == "mass": masses = atomgroup.masses - ref = atomgroup.center_of_mass(wrap=wrap, - unwrap=unwrap, - compound=compound) + ref = atomgroup.center_of_mass( + wrap=wrap, unwrap=unwrap, compound=compound + ) elif center == "charge": - ref = atomgroup.center_of_charge(wrap=wrap, - unwrap=unwrap, - compound=compound) + ref = atomgroup.center_of_charge( + wrap=wrap, unwrap=unwrap, compound=compound + ) else: choices = ["mass", "charge"] raise ValueError( f"The quadrupole center, {center}, is not supported. Choose" - " one of: {choices}") + " one of: {choices}" + ) - if compound == 'group': + if compound == "group": if wrap: - recenteredpos = (atomgroup.pack_into_box(inplace=False) - ref) + recenteredpos = atomgroup.pack_into_box(inplace=False) - ref elif unwrap: - recenteredpos = (atomgroup.unwrap( - inplace=False, - compound=compound, - reference=None, - ) - ref) + recenteredpos = ( + atomgroup.unwrap( + inplace=False, + compound=compound, + reference=None, + ) + - ref + ) else: - recenteredpos = (atomgroup.positions - ref) + recenteredpos = atomgroup.positions - ref quad_tensor = __quadrupole_tensor(recenteredpos, charges) else: - (atom_masks, compound_masks, - n_compounds) = atomgroup._split_by_compound_indices(compound) + (atom_masks, compound_masks, n_compounds) = ( + atomgroup._split_by_compound_indices(compound) + ) if unwrap: - coords = atomgroup.unwrap(compound=compound, - reference=None, - inplace=False) + coords = atomgroup.unwrap( + compound=compound, reference=None, inplace=False + ) else: coords = atomgroup.positions chgs = atomgroup.charges @@ -2484,14 +2610,16 @@ def __quadrupole_tensor(recenteredpos, charges): quad_tensor = np.empty((n_compounds, 3, 3), dtype=np.float64) for compound_mask, atom_mask in zip(compound_masks, atom_masks): quad_tensor[compound_mask, :, :] = [ - __quadrupole_tensor(coords[mask] - ref[compound_mask][i], - chgs[mask][:, None]) + __quadrupole_tensor( + coords[mask] - ref[compound_mask][i], + chgs[mask][:, None], + ) for i, mask in enumerate(atom_mask) ] return quad_tensor - transplants[GroupBase].append(('quadrupole_tensor', quadrupole_tensor)) + transplants[GroupBase].append(("quadrupole_tensor", quadrupole_tensor)) def quadrupole_moment(group, **kwargs): r"""Quadrupole moment of the group according to :footcite:p:`Gray1984`. @@ -2557,18 +2685,21 @@ def __quadrupole_moment(tensor): if len(quad_tensor.shape) == 2: quad_moment = __quadrupole_moment(quad_tensor) else: - quad_moment = np.array([__quadrupole_moment(x) for x in quad_tensor]) + quad_moment = np.array( + [__quadrupole_moment(x) for x in quad_tensor] + ) return quad_moment - transplants[GroupBase].append(('quadrupole_moment', quadrupole_moment)) + transplants[GroupBase].append(("quadrupole_moment", quadrupole_moment)) class FormalCharges(AtomAttr): """Formal charge on each atom""" - attrname = 'formalcharges' - singular = 'formalcharge' - per_object = 'atom' + + attrname = "formalcharges" + singular = "formalcharge" + per_object = "atom" dtype = int @staticmethod @@ -2578,9 +2709,9 @@ def _gen_initial_values(na, nr, ns): # TODO: update docs to property doc class Occupancies(AtomAttr): - attrname = 'occupancies' - singular = 'occupancy' - per_object = 'atom' + attrname = "occupancies" + singular = "occupancy" + per_object = "atom" dtype = float @staticmethod @@ -2591,21 +2722,23 @@ def _gen_initial_values(na, nr, ns): # TODO: update docs to property doc class AltLocs(AtomStringAttr): """AltLocs for each atom""" - attrname = 'altLocs' - singular = 'altLoc' - per_object = 'atom' + + attrname = "altLocs" + singular = "altLoc" + per_object = "atom" dtype = object @staticmethod def _gen_initial_values(na, nr, ns): - return np.array(['' for _ in range(na)], dtype=object) + return np.array(["" for _ in range(na)], dtype=object) class GBScreens(AtomAttr): """Generalized Born screening factor""" - attrname = 'gbscreens' - singular = 'gbscreen' - per_object = 'atom' + + attrname = "gbscreens" + singular = "gbscreen" + per_object = "atom" dtype = float @staticmethod @@ -2615,9 +2748,10 @@ def _gen_initial_values(na, nr, ns): class SolventRadii(AtomAttr): """Intrinsic solvation radius""" - attrname = 'solventradii' - singular = 'solventradius' - per_object = 'atom' + + attrname = "solventradii" + singular = "solventradius" + per_object = "atom" dtype = float @staticmethod @@ -2627,9 +2761,10 @@ def _gen_initial_values(na, nr, ns): class NonbondedIndices(AtomAttr): """Nonbonded index (AMBER)""" - attrname = 'nbindices' - singular = 'nbindex' - per_object = 'atom' + + attrname = "nbindices" + singular = "nbindex" + per_object = "atom" dtype = int @staticmethod @@ -2639,9 +2774,10 @@ def _gen_initial_values(na, nr, ns): class RMins(AtomAttr): """The Rmin/2 LJ parameter""" - attrname = 'rmins' - singular = 'rmin' - per_object = 'atom' + + attrname = "rmins" + singular = "rmin" + per_object = "atom" dtype = float @staticmethod @@ -2651,9 +2787,10 @@ def _gen_initial_values(na, nr, ns): class Epsilons(AtomAttr): """The epsilon LJ parameter""" - attrname = 'epsilons' - singular = 'epsilon' - per_object = 'atom' + + attrname = "epsilons" + singular = "epsilon" + per_object = "atom" dtype = float @staticmethod @@ -2663,9 +2800,10 @@ def _gen_initial_values(na, nr, ns): class RMin14s(AtomAttr): """The Rmin/2 LJ parameter for 1-4 interactions""" - attrname = 'rmin14s' - singular = 'rmin14' - per_object = 'atom' + + attrname = "rmin14s" + singular = "rmin14" + per_object = "atom" dtype = float @staticmethod @@ -2675,9 +2813,10 @@ def _gen_initial_values(na, nr, ns): class Epsilon14s(AtomAttr): """The epsilon LJ parameter for 1-4 interactions""" - attrname = 'epsilon14s' - singular = 'epsilon14' - per_object = 'atom' + + attrname = "epsilon14s" + singular = "epsilon14" + per_object = "atom" dtype = float @staticmethod @@ -2687,6 +2826,7 @@ def _gen_initial_values(na, nr, ns): class Aromaticities(AtomAttr): """Aromaticity""" + attrname = "aromaticities" singular = "aromaticity" per_object = "atom" @@ -2699,16 +2839,17 @@ def _gen_initial_values(na, nr, ns): class RSChirality(AtomAttr): """R/S chirality""" - attrname = 'chiralities' - singular= 'chirality' - dtype = 'U1' + + attrname = "chiralities" + singular = "chirality" + dtype = "U1" class ResidueAttr(TopologyAttr): - attrname = 'residueattrs' - singular = 'residueattr' + attrname = "residueattrs" + singular = "residueattr" target_classes = [AtomGroup, ResidueGroup, SegmentGroup, Atom, Residue] - per_object = 'residue' + per_object = "residue" def get_atoms(self, ag): rix = self.top.tt.atoms2residues(ag.ix) @@ -2740,21 +2881,21 @@ def set_segments(self, sg, values): # woe betide anyone who switches this inheritance order # Mixin needs to be first (L to R) to get correct __init__ and set_atoms class ResidueStringAttr(_StringInternerMixin, ResidueAttr): - @_check_length def set_residues(self, ag, values): return self._set_X(ag, values) @staticmethod def _gen_initial_values(na, nr, ns): - return np.full(nr, '', dtype=object) + return np.full(nr, "", dtype=object) # TODO: update docs to property doc class Resids(ResidueAttr): """Residue ID""" - attrname = 'resids' - singular = 'resid' + + attrname = "resids" + singular = "resid" dtype = int @staticmethod @@ -2764,14 +2905,14 @@ def _gen_initial_values(na, nr, ns): # TODO: update docs to property doc class Resnames(ResidueStringAttr): - attrname = 'resnames' - singular = 'resname' + attrname = "resnames" + singular = "resname" transplants = defaultdict(list) dtype = object @staticmethod def _gen_initial_values(na, nr, ns): - return np.array(['' for _ in range(nr)], dtype=object) + return np.array(["" for _ in range(nr)], dtype=object) def sequence(self, **kwargs): """Returns the amino acid sequence. @@ -2852,24 +2993,32 @@ def sequence(self, **kwargs): Biopython is now an optional dependency """ if not HAS_BIOPYTHON: - errmsg = ("The `sequence_alignment` method requires an " - "installation of `Biopython`. Please install " - "`Biopython` to use this method: " - "https://biopython.org/wiki/Download") + errmsg = ( + "The `sequence_alignment` method requires an " + "installation of `Biopython`. Please install " + "`Biopython` to use this method: " + "https://biopython.org/wiki/Download" + ) raise ImportError(errmsg) - formats = ('string', 'Seq', 'SeqRecord') + formats = ("string", "Seq", "SeqRecord") format = kwargs.pop("format", "SeqRecord") if format not in formats: - raise TypeError("Unknown format='{0}': must be one of: {1}".format( - format, ", ".join(formats))) + raise TypeError( + "Unknown format='{0}': must be one of: {1}".format( + format, ", ".join(formats) + ) + ) try: - sequence = "".join([convert_aa_code(r) - for r in self.residues.resnames]) + sequence = "".join( + [convert_aa_code(r) for r in self.residues.resnames] + ) except KeyError as err: - errmsg = (f"AtomGroup contains a residue name '{err.message}' that" - f" does not have a IUPAC protein 1-letter character") + errmsg = ( + f"AtomGroup contains a residue name '{err.message}' that" + f" does not have a IUPAC protein 1-letter character" + ) raise ValueError(errmsg) from None if format == "string": return sequence @@ -2878,14 +3027,13 @@ def sequence(self, **kwargs): return seq return Bio.SeqRecord.SeqRecord(seq, **kwargs) - transplants[ResidueGroup].append( - ('sequence', sequence)) + transplants[ResidueGroup].append(("sequence", sequence)) # TODO: update docs to property doc class Resnums(ResidueAttr): - attrname = 'resnums' - singular = 'resnum' + attrname = "resnums" + singular = "resnum" dtype = int @staticmethod @@ -2895,8 +3043,9 @@ def _gen_initial_values(na, nr, ns): class ICodes(ResidueStringAttr): """Insertion code for Atoms""" - attrname = 'icodes' - singular = 'icode' + + attrname = "icodes" + singular = "icode" dtype = object @@ -2905,16 +3054,17 @@ class Moltypes(ResidueStringAttr): Two molecules that share a molecule type share a common template topology. """ - attrname = 'moltypes' - singular = 'moltype' + + attrname = "moltypes" + singular = "moltype" dtype = object class Molnums(ResidueAttr): - """Index of molecule from 0 - """ - attrname = 'molnums' - singular = 'molnum' + """Index of molecule from 0""" + + attrname = "molnums" + singular = "molnum" dtype = np.intp @@ -2922,14 +3072,19 @@ class Molnums(ResidueAttr): class SegmentAttr(TopologyAttr): - """Base class for segment attributes. - - """ - attrname = 'segmentattrs' - singular = 'segmentattr' - target_classes = [AtomGroup, ResidueGroup, - SegmentGroup, Atom, Residue, Segment] - per_object = 'segment' + """Base class for segment attributes.""" + + attrname = "segmentattrs" + singular = "segmentattr" + target_classes = [ + AtomGroup, + ResidueGroup, + SegmentGroup, + Atom, + Residue, + Segment, + ] + per_object = "segment" def get_atoms(self, ag): six = self.top.tt.atoms2segments(ag.ix) @@ -2956,26 +3111,25 @@ def set_segments(self, sg, values): # woe betide anyone who switches this inheritance order # Mixin needs to be first (L to R) to get correct __init__ and set_atoms class SegmentStringAttr(_StringInternerMixin, SegmentAttr): - @_check_length def set_segments(self, ag, values): return self._set_X(ag, values) @staticmethod def _gen_initial_values(na, nr, ns): - return np.full(ns, '', dtype=object) + return np.full(ns, "", dtype=object) # TODO: update docs to property doc class Segids(SegmentStringAttr): - attrname = 'segids' - singular = 'segid' + attrname = "segids" + singular = "segid" transplants = defaultdict(list) dtype = object @staticmethod def _gen_initial_values(na, nr, ns): - return np.array(['' for _ in range(ns)], dtype=object) + return np.array(["" for _ in range(ns)], dtype=object) def _check_connection_values(func): @@ -2991,12 +3145,16 @@ def _check_connection_values(func): @functools.wraps(func) def wrapper(self, values, *args, **kwargs): - if not all(len(x) == self._n_atoms - and all(isinstance(y, (int, np.integer)) for y in x) - for x in values): - raise ValueError(("{} must be an iterable of tuples with {}" - " atom indices").format(self.attrname, - self._n_atoms)) + if not all( + len(x) == self._n_atoms + and all(isinstance(y, (int, np.integer)) for y in x) + for x in values + ): + raise ValueError( + ( + "{} must be an iterable of tuples with {}" " atom indices" + ).format(self.attrname, self._n_atoms) + ) clean = [] for v in values: if v[0] > v[-1]: @@ -3019,13 +3177,12 @@ class _ConnectionTopologyAttrMeta(_TopologyAttrMeta): def __init__(cls, name, bases, classdict): super().__init__(name, bases, classdict) - attrname = classdict.get('attrname') + attrname = classdict.get("attrname") if attrname is not None: def intra_connection(self, ag): - """Get connections only within this AtomGroup - """ + """Get connections only within this AtomGroup""" return ag.get_connections(attrname, outside=False) method = MethodType(intra_connection, cls) @@ -3058,22 +3215,25 @@ def __init__(self, values, types=None, guessed=False, order=None): def copy(self): """Return a deepcopy of this attribute""" - return self.__class__(copy.copy(self.values), - copy.copy(self.types), - copy.copy(self._guessed), - copy.copy(self.order)) + return self.__class__( + copy.copy(self.values), + copy.copy(self.types), + copy.copy(self._guessed), + copy.copy(self.order), + ) def __len__(self): return len(self._bondDict) @property - @cached('bd') + @cached("bd") def _bondDict(self): """Lazily built mapping of atoms:bonds""" bd = defaultdict(list) - for b, t, g, o in zip(self.values, self.types, - self._guessed, self.order): + for b, t, g, o in zip( + self.values, self.types, self._guessed, self.order + ): for a in b: bd[a].append((b, t, g, o)) return bd @@ -3092,8 +3252,9 @@ def get_atoms(self, ag): """ try: - unique_bonds = set(itertools.chain( - *[self._bondDict[a] for a in ag.ix])) + unique_bonds = set( + itertools.chain(*[self._bondDict[a] for a in ag.ix]) + ) except TypeError: # maybe we got passed an Atom unique_bonds = self._bondDict[ag.ix] @@ -3103,11 +3264,9 @@ def get_atoms(self, ag): types = types.ravel() guessed = guessed.ravel() order = order.ravel() - return TopologyGroup(bond_idx, ag.universe, - self.singular[:-1], - types, - guessed, - order) + return TopologyGroup( + bond_idx, ag.universe, self.singular[:-1], types, guessed, order + ) @_check_connection_values def _add_bonds(self, values, types=None, guessed=True, order=None): @@ -3126,7 +3285,7 @@ def _add_bonds(self, values, types=None, guessed=True, order=None): self.order.append(o) # kill the old cache of bond Dict try: - del self._cache['bd'] + del self._cache["bd"] except KeyError: pass @@ -3139,24 +3298,27 @@ def _delete_bonds(self, values): to_check = set(values) self_values = set(self.values) if not to_check.issubset(self_values): - missing = to_check-self_values - indices = ', '.join(map(str, missing)) - raise ValueError(('Cannot delete nonexistent ' - '{attrname} with atom indices:' - '{indices}').format(attrname=self.attrname, - indices=indices)) + missing = to_check - self_values + indices = ", ".join(map(str, missing)) + raise ValueError( + ( + "Cannot delete nonexistent " + "{attrname} with atom indices:" + "{indices}" + ).format(attrname=self.attrname, indices=indices) + ) # 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] - for attr in ('types', '_guessed', 'order'): - arr = np.array(getattr(self, attr), dtype='object') + for attr in ("types", "_guessed", "order"): + arr = np.array(getattr(self, attr), dtype="object") new = np.delete(arr, idx) setattr(self, attr, list(new)) # kill the old cache of bond Dict try: - del self._cache['bd'] + del self._cache["bd"] except KeyError: pass @@ -3171,10 +3333,11 @@ class Bonds(_Connection): Also adds the `bonded_atoms`, `fragment` and `fragments` attributes. """ - attrname = 'bonds' + + attrname = "bonds" # Singular is the same because one Atom might have # many bonds, so still asks for "bonds" in the plural - singular = 'bonds' + singular = "bonds" transplants = defaultdict(list) _n_atoms = 2 @@ -3186,8 +3349,11 @@ def bonded_atoms(self): return self.universe.atoms[idx] transplants[Atom].append( - ('bonded_atoms', property(bonded_atoms, None, None, - bonded_atoms.__doc__))) + ( + "bonded_atoms", + property(bonded_atoms, None, None, bonded_atoms.__doc__), + ) + ) def fragindex(self): """The index (ID) of the @@ -3199,7 +3365,7 @@ def fragindex(self): """ return self.universe._fragdict[self.ix].ix - @cached('fragindices', universe_validation=True) + @cached("fragindices", universe_validation=True) def fragindices(self): r"""The :class:`fragment indices` @@ -3233,7 +3399,7 @@ def fragment(self): """ return self.universe._fragdict[self.ix].fragment - @cached('fragments', universe_validation=True) + @cached("fragments", universe_validation=True) def fragments(self): """Read-only :class:`tuple` of :class:`fragments`. @@ -3259,8 +3425,12 @@ def fragments(self): .. versionadded:: 0.9.0 """ fragdict = self.universe._fragdict - return tuple(sorted(set(fragdict[aix].fragment for aix in self.ix), - key=lambda x: x[0].ix)) + return tuple( + sorted( + set(fragdict[aix].fragment for aix in self.ix), + key=lambda x: x[0].ix, + ) + ) def n_fragments(self): """The number of unique @@ -3274,24 +3444,24 @@ def n_fragments(self): return len(unique_int_1d(self.fragindices)) transplants[Atom].append( - ('fragment', property(fragment, None, None, - fragment.__doc__))) + ("fragment", property(fragment, None, None, fragment.__doc__)) + ) transplants[Atom].append( - ('fragindex', property(fragindex, None, None, - fragindex.__doc__))) + ("fragindex", property(fragindex, None, None, fragindex.__doc__)) + ) transplants[AtomGroup].append( - ('fragments', property(fragments, None, None, - fragments.__doc__))) + ("fragments", property(fragments, None, None, fragments.__doc__)) + ) transplants[AtomGroup].append( - ('fragindices', property(fragindices, None, None, - fragindices.__doc__))) + ("fragindices", property(fragindices, None, None, fragindices.__doc__)) + ) transplants[AtomGroup].append( - ('n_fragments', property(n_fragments, None, None, - n_fragments.__doc__))) + ("n_fragments", property(n_fragments, None, None, n_fragments.__doc__)) + ) class UreyBradleys(_Connection): @@ -3303,8 +3473,9 @@ class UreyBradleys(_Connection): .. versionadded:: 1.0.0 """ - attrname = 'ureybradleys' - singular = 'ureybradleys' + + attrname = "ureybradleys" + singular = "ureybradleys" transplants = defaultdict(list) _n_atoms = 2 @@ -3317,24 +3488,27 @@ class Angles(_Connection): These indices refer to the atom indices. """ - attrname = 'angles' - singular = 'angles' + + attrname = "angles" + singular = "angles" transplants = defaultdict(list) _n_atoms = 3 class Dihedrals(_Connection): """A connection between four sequential atoms""" - attrname = 'dihedrals' - singular = 'dihedrals' + + attrname = "dihedrals" + singular = "dihedrals" transplants = defaultdict(list) _n_atoms = 4 class Impropers(_Connection): """An imaginary dihedral between four atoms""" - attrname = 'impropers' - singular = 'impropers' + + attrname = "impropers" + singular = "impropers" transplants = defaultdict(list) _n_atoms = 4 @@ -3344,7 +3518,8 @@ class CMaps(_Connection): A connection between five atoms .. versionadded:: 1.0.0 """ - attrname = 'cmaps' - singular = 'cmaps' + + attrname = "cmaps" + singular = "cmaps" transplants = defaultdict(list) _n_atoms = 5 diff --git a/package/MDAnalysis/core/topologyobjects.py b/package/MDAnalysis/core/topologyobjects.py index 436ecc5dd5d..fcc37be6246 100644 --- a/package/MDAnalysis/core/topologyobjects.py +++ b/package/MDAnalysis/core/topologyobjects.py @@ -40,7 +40,6 @@ @functools.total_ordering class TopologyObject(object): - """Base class for all Topology items. Defines the behaviour by which Bonds/Angles/etc in MDAnalysis should @@ -55,6 +54,7 @@ class TopologyObject(object): .. versionchanged:: 2.6.0 Updated Atom ID representation order to match that of AtomGroup indices """ + __slots__ = ("_ix", "_u", "btype", "_bondtype", "_guessed", "order") def __init__(self, ix, universe, type=None, guessed=False, order=None): @@ -121,9 +121,8 @@ def __repr__(self): """Return representation in same order of AtomGroup indices""" return "<{cname} between: {conts}>".format( cname=self.__class__.__name__, - conts=", ".join([ - "Atom {0}".format(i) - for i in self.indices])) + conts=", ".join(["Atom {0}".format(i) for i in self.indices]), + ) def __contains__(self, other): """Check whether an atom is in this :class:`TopologyObject`""" @@ -133,8 +132,9 @@ def __eq__(self, other): """Check whether two bonds have identical contents""" if not self.universe == other.universe: return False - return (np.array_equal(self.indices, other.indices) or - np.array_equal(self.indices[::-1], other.indices)) + return np.array_equal(self.indices, other.indices) or np.array_equal( + self.indices[::-1], other.indices + ) def __ne__(self, other): return not self == other @@ -154,7 +154,6 @@ def __len__(self): class Bond(TopologyObject): - """A bond between two :class:`~MDAnalysis.core.groups.Atom` instances. Two :class:`Bond` instances can be compared with the ``==`` and @@ -173,7 +172,8 @@ class Bond(TopologyObject): Now a subclass of :class:`TopologyObject`. Changed class to use :attr:`__slots__` and stores atoms in :attr:`atoms` attribute. """ - btype = 'bond' + + btype = "bond" def partner(self, atom): """Bond.partner(Atom) @@ -206,7 +206,6 @@ def length(self, pbc=True): class Angle(TopologyObject): - """An angle between three :class:`~MDAnalysis.core.groups.Atom` instances. Atom 2 is the apex of the angle @@ -215,7 +214,8 @@ class Angle(TopologyObject): Now a subclass of :class:`TopologyObject`; now uses :attr:`__slots__` and stores atoms in :attr:`atoms` attribute """ - btype = 'angle' + + btype = "angle" def angle(self, pbc=True): """Returns the angle in degrees of this Angle. @@ -240,14 +240,16 @@ def angle(self, pbc=True): """ box = self.universe.dimensions if pbc else None - return np.rad2deg(distances.calc_angles( - self[0].position, self[1].position, self[2].position, box)) + return np.rad2deg( + distances.calc_angles( + self[0].position, self[1].position, self[2].position, box + ) + ) value = angle class Dihedral(TopologyObject): - """Dihedral (dihedral angle) between four :class:`~MDAnalysis.core.groups.Atom` instances. @@ -262,8 +264,9 @@ class Dihedral(TopologyObject): Renamed to Dihedral (was Torsion) """ + # http://cbio.bmt.tue.nl/pumma/uploads/Theory/dihedral.png - btype = 'dihedral' + btype = "dihedral" def dihedral(self, pbc=True): """Calculate the dihedral angle in degrees. @@ -290,8 +293,11 @@ def dihedral(self, pbc=True): box = self.universe.dimensions if pbc else None A, B, C, D = self.atoms - return np.rad2deg(distances.calc_dihedrals( - A.position, B.position, C.position, D.position, box)) + return np.rad2deg( + distances.calc_dihedrals( + A.position, B.position, C.position, D.position, box + ) + ) value = dihedral @@ -313,8 +319,9 @@ class ImproperDihedral(Dihedral): .. versionchanged:: 0.11.0 Renamed to ImproperDihedral (was Improper_Torsion) """ + # http://cbio.bmt.tue.nl/pumma/uploads/Theory/improper.png - btype = 'improper' + btype = "improper" def improper(self): """Improper dihedral angle in degrees. @@ -328,7 +335,6 @@ def improper(self): class UreyBradley(TopologyObject): - """A Urey-Bradley angle between two :class:`~MDAnalysis.core.groups.Atom` instances. Two :class:`UreyBradley` instances can be compared with the ``==`` and ``!=`` operators. A UreyBradley angle is equal to another if the same atom @@ -336,7 +342,8 @@ class UreyBradley(TopologyObject): .. versionadded:: 1.0.0 """ - btype = 'ureybradley' + + btype = "ureybradley" def partner(self, atom): """UreyBradley.partner(Atom) @@ -353,8 +360,7 @@ def partner(self, atom): raise ValueError("Unrecognised Atom") def distance(self, pbc=True): - """Distance between the atoms. - """ + """Distance between the atoms.""" box = self.universe.dimensions if pbc else None return distances.calc_bonds(self[0].position, self[1].position, box) @@ -363,16 +369,16 @@ def distance(self, pbc=True): class CMap(TopologyObject): """ - Coupled-torsion correction map term between five + Coupled-torsion correction map term between five :class:`~MDAnalysis.core.groups.Atom` instances. .. versionadded:: 1.0.0 """ - btype = 'cmap' + btype = "cmap" -class TopologyDict(object): +class TopologyDict(object): """A customised dictionary designed for sorting the bonds, angles and dihedrals present in a group of atoms. @@ -490,7 +496,8 @@ def __iter__(self): def __repr__(self): return "".format( - num=len(self), type=self.toptype) + num=len(self), type=self.toptype + ) def __getitem__(self, key): """Returns a TopologyGroup matching the criteria if possible, @@ -517,12 +524,17 @@ def __contains__(self, other): return other in self.dict or other[::-1] in self.dict -_BTYPE_TO_SHAPE = {'bond': 2, 'ureybradley': 2, 'angle': 3, - 'dihedral': 4, 'improper': 4, 'cmap': 5} +_BTYPE_TO_SHAPE = { + "bond": 2, + "ureybradley": 2, + "angle": 3, + "dihedral": 4, + "improper": 4, + "cmap": 5, +} class TopologyGroup(object): - """A container for a groups of bonds. All bonds of a certain types can be retrieved from within the @@ -577,20 +589,30 @@ class TopologyGroup(object): ``type``, ``guessed``, and ``order`` are no longer reshaped to arrays with an extra dimension """ - def __init__(self, bondidx, universe, btype=None, type=None, guessed=None, - order=None): + + def __init__( + self, + bondidx, + universe, + btype=None, + type=None, + guessed=None, + order=None, + ): if btype is None: # guess what I am # difference between dihedral and improper # not really important - self.btype = {2: 'bond', - 3: 'angle', - 4: 'dihedral'}[len(bondidx[0])] + self.btype = {2: "bond", 3: "angle", 4: "dihedral"}[ + len(bondidx[0]) + ] elif btype in _BTYPE_TO_SHAPE: self.btype = btype else: - raise ValueError("Unsupported btype, use one of '{}'" - "".format(', '.join(_BTYPE_TO_SHAPE))) + raise ValueError( + "Unsupported btype, use one of '{}'" + "".format(", ".join(_BTYPE_TO_SHAPE)) + ) bondidx = np.asarray(bondidx) nbonds = len(bondidx) @@ -615,8 +637,10 @@ def __init__(self, bondidx, universe, btype=None, type=None, guessed=None, self._order = order[uniq_idx] # Create vertical AtomGroups - self._ags = [universe.atoms[self._bix[:, i]] - for i in range(self._bix.shape[1])] + self._ags = [ + universe.atoms[self._bix[:, i]] + for i in range(self._bix.shape[1]) + ] else: # Empty TopologyGroup self._bix = np.array([]) @@ -652,7 +676,7 @@ def types(self): return list(self.topDict.keys()) @property - @cached('dict') + @cached("dict") def topDict(self): """ Returns the TopologyDict for this topology group. @@ -689,7 +713,7 @@ def atomgroup_intersection(self, ag, **kwargs): # Strict requires all items in a row to be seen, # otherwise any item in a row - func = np.all if kwargs.get('strict', False) else np.any + func = np.all if kwargs.get("strict", False) else np.any atom_idx = ag.indices # Create a list of boolean arrays, @@ -752,9 +776,11 @@ def __add__(self, other): """ # check addition is sane if not isinstance(other, (TopologyObject, TopologyGroup)): - raise TypeError("Can only combine TopologyObject or " - "TopologyGroup to TopologyGroup, not {0}" - "".format(type(other))) + raise TypeError( + "Can only combine TopologyObject or " + "TopologyGroup to TopologyGroup, not {0}" + "".format(type(other)) + ) # cases where either other or self is empty TG if not other: # adding empty TG to me @@ -762,37 +788,43 @@ def __add__(self, other): if not self: if isinstance(other, TopologyObject): # Reshape indices to be 2d array - return TopologyGroup(other.indices[None, :], - other.universe, - btype=other.btype, - type=np.array([other._bondtype]), - guessed=np.array([other.is_guessed]), - order=np.array([other.order]), - ) + return TopologyGroup( + other.indices[None, :], + other.universe, + btype=other.btype, + type=np.array([other._bondtype]), + guessed=np.array([other.is_guessed]), + order=np.array([other.order]), + ) else: - return TopologyGroup(other.indices, - other.universe, - btype=other.btype, - type=other._bondtypes, - guessed=other._guessed, - order=other._order, - ) + return TopologyGroup( + other.indices, + other.universe, + btype=other.btype, + type=other._bondtypes, + guessed=other._guessed, + order=other._order, + ) else: if not other.btype == self.btype: - raise TypeError("Cannot add different types of " - "TopologyObjects together") + raise TypeError( + "Cannot add different types of " "TopologyObjects together" + ) if isinstance(other, TopologyObject): # add TO to me return TopologyGroup( np.concatenate([self.indices, other.indices[None, :]]), self.universe, btype=self.btype, - type=np.concatenate([self._bondtypes, - np.array([other._bondtype])]), - guessed=np.concatenate([self._guessed, - np.array([other.is_guessed])]), - order=np.concatenate([self._order, - np.array([other.order])]), + type=np.concatenate( + [self._bondtypes, np.array([other._bondtype])] + ), + guessed=np.concatenate( + [self._guessed, np.array([other.is_guessed])] + ), + order=np.concatenate( + [self._order, np.array([other.order])] + ), ) else: # add TG to me @@ -814,25 +846,31 @@ def __getitem__(self, item): """ # Grab a single Item, similar to Atom/AtomGroup relationship if isinstance(item, numbers.Integral): - outclass = {'bond': Bond, - 'angle': Angle, - 'dihedral': Dihedral, - 'improper': ImproperDihedral, - 'ureybradley': UreyBradley, - 'cmap': CMap}[self.btype] - return outclass(self._bix[item], - self._u, - type=self._bondtypes[item], - guessed=self._guessed[item], - order=self._order[item]) + outclass = { + "bond": Bond, + "angle": Angle, + "dihedral": Dihedral, + "improper": ImproperDihedral, + "ureybradley": UreyBradley, + "cmap": CMap, + }[self.btype] + return outclass( + self._bix[item], + self._u, + type=self._bondtypes[item], + guessed=self._guessed[item], + order=self._order[item], + ) else: # Slice my index array with the item - return self.__class__(self._bix[item], - self._u, - btype=self.btype, - type=self._bondtypes[item], - guessed=self._guessed[item], - order=self._order[item],) + return self.__class__( + self._bix[item], + self._u, + btype=self.btype, + type=self._bondtypes[item], + guessed=self._guessed[item], + order=self._order[item], + ) def __contains__(self, item): """Tests if this TopologyGroup contains a bond""" @@ -840,7 +878,8 @@ def __contains__(self, item): def __repr__(self): return "".format( - num=len(self), type=self.btype) + num=len(self), type=self.btype + ) def __eq__(self, other): """Test if contents of TopologyGroups are equal""" @@ -869,8 +908,10 @@ def atom3(self): return self._ags[2] except IndexError: nvert = _BTYPE_TO_SHAPE[self.btype] - errmsg = (f"TopologyGroup of {self.btype}s only has {nvert} " - f"vertical AtomGroups") + errmsg = ( + f"TopologyGroup of {self.btype}s only has {nvert} " + f"vertical AtomGroups" + ) raise IndexError(errmsg) from None @property @@ -880,8 +921,10 @@ def atom4(self): return self._ags[3] except IndexError: nvert = _BTYPE_TO_SHAPE[self.btype] - errmsg = (f"TopologyGroup of {self.btype}s only has {nvert} " - f"vertical AtomGroups") + errmsg = ( + f"TopologyGroup of {self.btype}s only has {nvert} " + f"vertical AtomGroups" + ) raise IndexError(errmsg) from None # Distance calculation methods below @@ -899,13 +942,13 @@ def values(self, **kwargs): .. versionadded:: 0.11.0 """ - if self.btype == 'bond': + if self.btype == "bond": return self.bonds(**kwargs) - elif self.btype == 'angle': + elif self.btype == "angle": return self.angles(**kwargs) - elif self.btype == 'dihedral': + elif self.btype == "dihedral": return self.dihedrals(**kwargs) - elif self.btype == 'improper': + elif self.btype == "improper": return self.dihedrals(**kwargs) def _calc_connection_values(self, func, *btypes, result=None, pbc=False): @@ -931,8 +974,9 @@ def bonds(self, pbc=False, result=None): Uses cython implementation """ - return self._calc_connection_values(distances.calc_bonds, "bond", - pbc=pbc, result=result) + return self._calc_connection_values( + distances.calc_bonds, "bond", pbc=pbc, result=result + ) def angles(self, result=None, pbc=False): """Calculates the angle in radians formed between a bond @@ -956,8 +1000,9 @@ def angles(self, result=None, pbc=False): Added *pbc* option (default ``False``) """ - return self._calc_connection_values(distances.calc_angles, "angle", - pbc=pbc, result=result) + return self._calc_connection_values( + distances.calc_angles, "angle", pbc=pbc, result=result + ) def dihedrals(self, result=None, pbc=False): """Calculate the dihedral angle in radians for this topology @@ -983,6 +1028,10 @@ def dihedrals(self, result=None, pbc=False): .. versionchanged:: 0.9.0 Added *pbc* option (default ``False``) """ - return self._calc_connection_values(distances.calc_dihedrals, - "dihedral", "improper", - pbc=pbc, result=result) + return self._calc_connection_values( + distances.calc_dihedrals, + "dihedral", + "improper", + pbc=pbc, + result=result, + ) diff --git a/package/pyproject.toml b/package/pyproject.toml index 8be16c7f61a..04e76fdbfed 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -142,6 +142,7 @@ tables\.py | MDAnalysis/coordinates/.*\.py | MDAnalysis/tests/.*\.py | MDAnalysis/selections/.*\.py +| MDAnalysis/core/.*\.py ) ''' extend-exclude = ''' diff --git a/testsuite/MDAnalysisTests/core/test_accessors.py b/testsuite/MDAnalysisTests/core/test_accessors.py index 9b3d22ffd0e..8084fa105ae 100644 --- a/testsuite/MDAnalysisTests/core/test_accessors.py +++ b/testsuite/MDAnalysisTests/core/test_accessors.py @@ -25,8 +25,9 @@ from MDAnalysisTests.util import import_not_available -requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), - reason="requires RDKit") +requires_rdkit = pytest.mark.skipif( + import_not_available("rdkit"), reason="requires RDKit" +) @requires_rdkit @@ -52,20 +53,25 @@ def test_convert_to_lib_method_kwargs(self, u): class TestAccessor: def test_access_from_class(self): - assert (mda.core.AtomGroup.convert_to is - mda.core.accessors.ConverterWrapper) + assert ( + mda.core.AtomGroup.convert_to + is mda.core.accessors.ConverterWrapper + ) class TestConverterWrapper: def test_raises_valueerror(self): u = mda.Universe.empty(1) - with pytest.raises(ValueError, - match="No 'mdanalysis' converter found"): + with pytest.raises( + ValueError, match="No 'mdanalysis' converter found" + ): u.atoms.convert_to("mdanalysis") @requires_rdkit def test_single_instance(self): u1 = mda.Universe.from_smiles("C") u2 = mda.Universe.from_smiles("CC") - assert (u1.atoms.convert_to.rdkit.__wrapped__ is - u2.atoms.convert_to.rdkit.__wrapped__) + assert ( + u1.atoms.convert_to.rdkit.__wrapped__ + is u2.atoms.convert_to.rdkit.__wrapped__ + ) diff --git a/testsuite/MDAnalysisTests/core/test_accumulate.py b/testsuite/MDAnalysisTests/core/test_accumulate.py index b93a458d06e..2aa1fd29099 100644 --- a/testsuite/MDAnalysisTests/core/test_accumulate.py +++ b/testsuite/MDAnalysisTests/core/test_accumulate.py @@ -29,7 +29,7 @@ from MDAnalysisTests.core.util import UnWrapUniverse import pytest -levels = ('atoms', 'residues', 'segments') +levels = ("atoms", "residues", "segments") class TestAccumulate(object): @@ -41,18 +41,26 @@ def group(self, request): return getattr(u, request.param) def test_accumulate_str_attribute(self, group): - assert_almost_equal(group.accumulate("masses"), np.sum(group.atoms.masses)) + assert_almost_equal( + group.accumulate("masses"), np.sum(group.atoms.masses) + ) def test_accumulate_different_func(self, group): assert_almost_equal( group.accumulate("masses", function=np.prod), - np.prod(group.atoms.masses)) - - @pytest.mark.parametrize('name, compound', (('resindices', 'residues'), - ('segindices', 'segments'), - ('molnums', 'molecules'), - ('fragindices', 'fragments'))) - @pytest.mark.parametrize('level', levels) + np.prod(group.atoms.masses), + ) + + @pytest.mark.parametrize( + "name, compound", + ( + ("resindices", "residues"), + ("segindices", "segments"), + ("molnums", "molecules"), + ("fragindices", "fragments"), + ), + ) + @pytest.mark.parametrize("level", levels) def test_accumulate_str_attribute_compounds(self, name, compound, level): u = UnWrapUniverse() group = getattr(u, level) @@ -68,13 +76,13 @@ def test_accumulate_wrongcomponent(self, group): with pytest.raises(ValueError): group.accumulate("masses", compound="foo") - @pytest.mark.parametrize('level', levels) + @pytest.mark.parametrize("level", levels) def test_accumulate_nobonds(self, level): group = getattr(mda.Universe(GRO), level) with pytest.raises(NoDataError): group.accumulate("masses", compound="fragments") - @pytest.mark.parametrize('level', levels) + @pytest.mark.parametrize("level", levels) def test_accumulate_nomolnums(self, level): group = getattr(mda.Universe(GRO), level) with pytest.raises(NoDataError): @@ -88,16 +96,29 @@ def test_accumulate_array_attribute_wrongshape(self, group): with pytest.raises(ValueError): group.accumulate(np.ones(len(group.atoms) - 1)) - @pytest.mark.parametrize('name, compound', (('resindices', 'residues'), - ('segindices', 'segments'), - ('molnums', 'molecules'), - ('fragindices', 'fragments'))) - @pytest.mark.parametrize('level', levels) + @pytest.mark.parametrize( + "name, compound", + ( + ("resindices", "residues"), + ("segindices", "segments"), + ("molnums", "molecules"), + ("fragindices", "fragments"), + ), + ) + @pytest.mark.parametrize("level", levels) def test_accumulate_array_attribute_compounds(self, name, compound, level): u = UnWrapUniverse() group = getattr(u, level) - ref = [np.ones((len(a), 2, 5)).sum(axis=0) for a in group.atoms.groupby(name).values()] - assert_equal(group.accumulate(np.ones((len(group.atoms), 2, 5)), compound=compound), ref) + ref = [ + np.ones((len(a), 2, 5)).sum(axis=0) + for a in group.atoms.groupby(name).values() + ] + assert_equal( + group.accumulate( + np.ones((len(group.atoms), 2, 5)), compound=compound + ), + ref, + ) class TestTotals(object): @@ -113,9 +134,14 @@ def group(self, request): def test_total_charge(self, group): assert_almost_equal(group.total_charge(), -4.0, decimal=4) - @pytest.mark.parametrize('name, compound', - (('resids', 'residues'), ('segids', 'segments'), - ('fragindices', 'fragments'))) + @pytest.mark.parametrize( + "name, compound", + ( + ("resids", "residues"), + ("segids", "segments"), + ("fragindices", "fragments"), + ), + ) def test_total_charge_compounds(self, group, name, compound): ref = [sum(a.charges) for a in group.atoms.groupby(name).values()] assert_almost_equal(group.total_charge(compound=compound), ref) @@ -135,9 +161,14 @@ def test_total_charge_duplicates(self, group): def test_total_mass(self, group): assert_almost_equal(group.total_mass(), 23582.043) - @pytest.mark.parametrize('name, compound', - (('resids', 'residues'), ('segids', 'segments'), - ('fragindices', 'fragments'))) + @pytest.mark.parametrize( + "name, compound", + ( + ("resids", "residues"), + ("segids", "segments"), + ("fragindices", "fragments"), + ), + ) def test_total_mass_compounds(self, group, name, compound): ref = [sum(a.masses) for a in group.atoms.groupby(name).values()] assert_almost_equal(group.total_mass(compound=compound), ref) @@ -160,7 +191,7 @@ class TestMultipole(object): and quadrupole_moment. """ - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(self): u = mda.Universe(PDB_multipole) u.add_TopologyAttr( @@ -184,20 +215,22 @@ def u(self): 0.037, -0.25, 0.034, - 0.034 - ]) # acetate [12:] + 0.034, + ], + ) # acetate [12:] lx, ly, lz = np.max(u.atoms.positions, axis=0) - np.min( - u.atoms.positions, axis=0) + u.atoms.positions, axis=0 + ) u.dimensions = np.array([lx, ly, lz, 90, 90, 90], dtype=float) return u - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def group(self, u): group = u.select_atoms("all") return group - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def methane(self, u): group = u.select_atoms("resname CH4") return group @@ -209,42 +242,44 @@ def test_dipole_moment_com(self, methane): methane.dipole_moment(wrap=True), methane.dipole_moment(), ] - assert_almost_equal(dipoles, [0., 0.2493469, 0.]) + assert_almost_equal(dipoles, [0.0, 0.2493469, 0.0]) def test_dipole_moment_no_center(self, group): try: group.dipole_moment(unwrap=True, center="not supported") except ValueError as e: - assert 'not supported' in e.args[0] + assert "not supported" in e.args[0] def test_dipole_moment_residues_com_coc(self, group): compound = "residues" (_, _, n_compounds) = group.atoms._split_by_compound_indices(compound) dipoles_com = group.dipole_moment(compound=compound, unwrap=False) - dipoles_coc = group.dipole_moment(compound=compound, - unwrap=False, - center="charge") + dipoles_coc = group.dipole_moment( + compound=compound, unwrap=False, center="charge" + ) - assert_almost_equal(dipoles_com, - np.array([0., 0.0010198, 0.1209898, 0.5681058])) + assert_almost_equal( + dipoles_com, np.array([0.0, 0.0010198, 0.1209898, 0.5681058]) + ) assert_almost_equal(dipoles_com[:3], dipoles_coc[:3]) assert dipoles_com[3] != dipoles_coc[3] assert len(dipoles_com) == n_compounds def test_dipole_moment_segment(self, methane): - compound = 'segments' - (_, _, - n_compounds) = methane.atoms._split_by_compound_indices(compound) + compound = "segments" + (_, _, n_compounds) = methane.atoms._split_by_compound_indices( + compound + ) dipoles = methane.dipole_moment(compound=compound, unwrap=True) - assert_almost_equal(dipoles, [0.]) and len(dipoles) == n_compounds + assert_almost_equal(dipoles, [0.0]) and len(dipoles) == n_compounds def test_dipole_moment_fragments(self, group): - compound = 'fragments' + compound = "fragments" (_, _, n_compounds) = group.atoms._split_by_compound_indices(compound) dipoles = group.dipole_moment(compound=compound, unwrap=False) - assert_almost_equal(dipoles, - np.array([0., 0.0010198, 0.1209898, 0.5681058 - ])) and len(dipoles) == n_compounds + assert_almost_equal( + dipoles, np.array([0.0, 0.0010198, 0.1209898, 0.5681058]) + ) and len(dipoles) == n_compounds # Quadrupole def test_quadrupole_moment_com(self, methane): @@ -253,41 +288,44 @@ def test_quadrupole_moment_com(self, methane): methane.quadrupole_moment(wrap=True), methane.quadrupole_moment(), ] - assert_almost_equal(quadrupoles, [0., 0.4657596, 0.]) + assert_almost_equal(quadrupoles, [0.0, 0.4657596, 0.0]) def test_quadrupole_moment_coc(self, group): assert_almost_equal( group.quadrupole_moment(unwrap=False, center="charge"), - 0.9769951421535777) + 0.9769951421535777, + ) def test_quadrupole_moment_no_center(self, group): try: group.quadrupole_moment(unwrap=True, center="not supported") except ValueError as e: - assert 'not supported' in e.args[0] + assert "not supported" in e.args[0] def test_quadrupole_moment_residues(self, group): compound = "residues" (_, _, n_compounds) = group.atoms._split_by_compound_indices(compound) quadrupoles = group.quadrupole_moment(compound=compound, unwrap=False) - assert_almost_equal(quadrupoles, - np.array([0., 0.0011629, 0.1182701, 0.6891748 - ])) and len(quadrupoles) == n_compounds + assert_almost_equal( + quadrupoles, np.array([0.0, 0.0011629, 0.1182701, 0.6891748]) + ) and len(quadrupoles) == n_compounds def test_quadrupole_moment_segment(self, methane): compound = "segments" - (_, _, - n_compounds) = methane.atoms._split_by_compound_indices(compound) + (_, _, n_compounds) = methane.atoms._split_by_compound_indices( + compound + ) quadrupoles = methane.quadrupole_moment(compound=compound, unwrap=True) - assert_almost_equal(quadrupoles, - [0.]) and len(quadrupoles) == n_compounds + assert_almost_equal(quadrupoles, [0.0]) and len( + quadrupoles + ) == n_compounds def test_quadrupole_moment_fragments(self, group): compound = "fragments" (_, _, n_compounds) = group.atoms._split_by_compound_indices(compound) quadrupoles = group.quadrupole_moment(compound=compound, unwrap=False) - assert_almost_equal(quadrupoles, - np.array([0., 0.0011629, 0.1182701, 0.6891748 - ])) and len(quadrupoles) == n_compounds + assert_almost_equal( + quadrupoles, np.array([0.0, 0.0011629, 0.1182701, 0.6891748]) + ) and len(quadrupoles) == n_compounds diff --git a/testsuite/MDAnalysisTests/core/test_atom.py b/testsuite/MDAnalysisTests/core/test_atom.py index 24479783d91..1d114c5a176 100644 --- a/testsuite/MDAnalysisTests/core/test_atom.py +++ b/testsuite/MDAnalysisTests/core/test_atom.py @@ -27,7 +27,8 @@ import MDAnalysis as mda from MDAnalysis import NoDataError from MDAnalysisTests.datafiles import ( - PSF, DCD, + PSF, + DCD, XYZ_mini, ) from numpy.testing import assert_almost_equal @@ -52,16 +53,16 @@ def atom(universe): def test_attributes_names(self, atom): a = atom - assert a.name == 'CG' - assert a.resname == 'LEU' + assert a.name == "CG" + assert a.resname == "LEU" def test_setting_attribute_name(self, atom): - atom.name = 'AA' - assert atom.name == 'AA' + atom.name = "AA" + assert atom.name == "AA" def test_setting_attribute_type(self, atom): - atom.type = 'Z' - assert atom.type == 'Z' + atom.type = "Z" + assert atom.type == "Z" def test_setting_attribute_mass(self, atom): atom.mass = 13 @@ -72,7 +73,9 @@ def test_setting_attributes_charge(self, atom): assert atom.charge == 6 def test_attributes_positions(self, atom): - known_pos = np.array([3.94543672, -12.4060812, -7.26820087], dtype=np.float32) + known_pos = np.array( + [3.94543672, -12.4060812, -7.26820087], dtype=np.float32 + ) a = atom # new position property (mutable) assert_almost_equal(a.position, known_pos) @@ -81,13 +84,13 @@ def test_attributes_positions(self, atom): assert_almost_equal(a.position, pos) def test_atom_selection(self, universe, atom): - asel = universe.select_atoms('atom 4AKE 67 CG').atoms[0] + asel = universe.select_atoms("atom 4AKE 67 CG").atoms[0] assert atom == asel def test_hierarchy(self, universe, atom): u = universe a = atom - assert a.segment == u.select_atoms('segid 4AKE').segments[0] + assert a.segment == u.select_atoms("segid 4AKE").segments[0] assert a.residue == u.residues[66] def test_bad_add(self, atom): @@ -136,16 +139,16 @@ def a(): def test_velocity_fail(self, a): with pytest.raises(NoDataError): - getattr(a, 'velocity') + getattr(a, "velocity") def test_force_fail(self, a): with pytest.raises(NoDataError): - getattr(a, 'force') + getattr(a, "force") def test_velocity_set_fail(self, a): with pytest.raises(NoDataError): - setattr(a, 'velocity', [1.0, 1.0, 1.0]) + setattr(a, "velocity", [1.0, 1.0, 1.0]) def test_force_set_fail(self, a): with pytest.raises(NoDataError): - setattr(a, 'force', [1.0, 1.0, 1.0]) + setattr(a, "force", [1.0, 1.0, 1.0]) diff --git a/testsuite/MDAnalysisTests/core/test_atomgroup.py b/testsuite/MDAnalysisTests/core/test_atomgroup.py index fdb23b682e4..38432e65c42 100644 --- a/testsuite/MDAnalysisTests/core/test_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_atomgroup.py @@ -44,12 +44,16 @@ ) from MDAnalysisTests.datafiles import ( - PSF, DCD, - TRZ_psf, TRZ, + PSF, + DCD, + TRZ_psf, + TRZ, two_water_gro, - TPR_xvf, TRR_xvf, - GRO, GRO_MEMPROT, - TPR + TPR_xvf, + TRR_xvf, + GRO, + GRO_MEMPROT, + TPR, ) from MDAnalysisTests import make_Universe, no_deprecated_call from MDAnalysisTests.core.util import UnWrapUniverse @@ -58,6 +62,7 @@ class TestAtomGroupToTopology(object): """Test the conversion of AtomGroup to TopologyObjects""" + @pytest.fixture() def u(self): return mda.Universe(PSF, DCD) @@ -82,12 +87,9 @@ def test_improper(self, u): imp = ag.improper assert isinstance(imp, ImproperDihedral) - @pytest.mark.parametrize('btype,', [ - 'bond', - 'angle', - 'dihedral', - 'improper' - ]) + @pytest.mark.parametrize( + "btype,", ["bond", "angle", "dihedral", "improper"] + ) def test_VE(self, btype, u): ag = u.atoms[:10] with pytest.raises(ValueError): @@ -103,7 +105,7 @@ def u(self): def test_write_no_args(self, u, tmpdir): with tmpdir.as_cwd(): u.atoms.write() - files = glob('*') + files = glob("*") assert len(files) == 1 name = path.splitext(path.basename(DCD))[0] @@ -112,86 +114,99 @@ def test_write_no_args(self, u, tmpdir): def test_raises_unknown_format(self, u, tmpdir): with tmpdir.as_cwd(): with pytest.raises(ValueError): - u.atoms.write('useless.format123') + u.atoms.write("useless.format123") def test_write_coordinates(self, u, tmpdir): with tmpdir.as_cwd(): u.atoms.write("test.xtc") - @pytest.mark.parametrize('frames', ( - [4], - [2, 3, 3, 1], - slice(2, 6, 1), - )) + @pytest.mark.parametrize( + "frames", + ( + [4], + [2, 3, 3, 1], + slice(2, 6, 1), + ), + ) def test_write_frames(self, u, tmpdir, frames): - destination = str(tmpdir / 'test.dcd') + destination = str(tmpdir / "test.dcd") selection = u.trajectory[frames] ref_positions = np.stack([ts.positions.copy() for ts in selection]) u.atoms.write(destination, frames=frames) u_new = mda.Universe(destination, to_guess=()) - new_positions = np.stack([ts.positions.copy() for ts in u_new.trajectory]) + new_positions = np.stack( + [ts.positions.copy() for ts in u_new.trajectory] + ) assert_array_almost_equal(new_positions, ref_positions) - @pytest.mark.parametrize('frames', ( - [4], - [2, 3, 3, 1], - slice(2, 6, 1), - )) + @pytest.mark.parametrize( + "frames", + ( + [4], + [2, 3, 3, 1], + slice(2, 6, 1), + ), + ) def test_write_frame_iterator(self, u, tmpdir, frames): - destination = str(tmpdir / 'test.dcd') + destination = str(tmpdir / "test.dcd") selection = u.trajectory[frames] ref_positions = np.stack([ts.positions.copy() for ts in selection]) u.atoms.write(destination, frames=selection) u_new = mda.Universe(destination, to_guess=()) - new_positions = np.stack([ts.positions.copy() for ts in u_new.trajectory]) + new_positions = np.stack( + [ts.positions.copy() for ts in u_new.trajectory] + ) assert_array_almost_equal(new_positions, ref_positions) - @pytest.mark.parametrize('extension', ('xtc', 'dcd', 'pdb', 'xyz', 'PDB')) - @pytest.mark.parametrize('compression', ('', '.gz', '.bz2')) + @pytest.mark.parametrize("extension", ("xtc", "dcd", "pdb", "xyz", "PDB")) + @pytest.mark.parametrize("compression", ("", ".gz", ".bz2")) def test_write_frame_none(self, u, tmpdir, extension, compression): - destination = str(tmpdir / 'test.' + extension + compression) + destination = str(tmpdir / "test." + extension + compression) u.atoms.write(destination, frames=None) 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, ...], - new_positions, decimal=2) + assert_array_almost_equal( + u.atoms.positions[None, ...], new_positions, decimal=2 + ) - @pytest.mark.parametrize('compression', ('', '.gz', '.bz2')) + @pytest.mark.parametrize("compression", ("", ".gz", ".bz2")) def test_write_frames_all(self, u, tmpdir, compression): - destination = str(tmpdir / 'test.dcd' + compression) - u.atoms.write(destination, frames='all') + destination = str(tmpdir / "test.dcd" + compression) + u.atoms.write(destination, frames="all") 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]) + new_positions = np.stack( + [ts.positions.copy() for ts in u_new.trajectory] + ) assert_array_almost_equal(new_positions, ref_positions) - @pytest.mark.parametrize('frames', ('invalid', 8, True, False, 3.2)) + @pytest.mark.parametrize("frames", ("invalid", 8, True, False, 3.2)) def test_write_frames_invalid(self, u, tmpdir, frames): - destination = str(tmpdir / 'test.dcd') + destination = str(tmpdir / "test.dcd") with pytest.raises(TypeError): u.atoms.write(destination, frames=frames) def test_incompatible_arguments(self, u, tmpdir): - destination = str(tmpdir / 'test.dcd') + destination = str(tmpdir / "test.dcd") with pytest.raises(ValueError): u.atoms.write(destination, frames=[0, 1, 2], multiframe=False) def test_incompatible_trajectories(self, tmpdir): - destination = str(tmpdir / 'test.dcd') + destination = str(tmpdir / "test.dcd") u1 = make_Universe(trajectory=True) u2 = make_Universe(trajectory=True) - destination = str(tmpdir / 'test.dcd') + destination = str(tmpdir / "test.dcd") with pytest.raises(ValueError): u1.atoms.write(destination, frames=u2.trajectory) def test_write_no_traj_move(self, u, tmpdir): - destination = str(tmpdir / 'test.dcd') + destination = str(tmpdir / "test.dcd") u.trajectory[10] u.atoms.write(destination, frames=[1, 2, 3]) assert u.trajectory.ts.frame == 10 @@ -204,7 +219,7 @@ def test_bogus_kwarg_pdb(self, u, tmpdir): # test for resolution of Issue 877 with tmpdir.as_cwd(): with pytest.raises(TypeError): - u.atoms.write('dummy.pdb', bogus="what?") + u.atoms.write("dummy.pdb", bogus="what?") class _WriteAtoms(object): @@ -224,63 +239,83 @@ def outfile(self, tmpdir): def universe_from_tmp(self, outfile): return mda.Universe(outfile, convert_units=True) - @pytest.mark.parametrize('compression', ('', '.gz', '.bz2')) + @pytest.mark.parametrize("compression", ("", ".gz", ".bz2")) def test_write_atoms(self, universe, outfile, compression): outname = outfile + compression universe.atoms.write(outname) u2 = self.universe_from_tmp(outname) assert_almost_equal( - universe.atoms.positions, u2.atoms.positions, + universe.atoms.positions, + u2.atoms.positions, self.precision, - err_msg=("atom coordinate mismatch between original and {0!s} " - "file".format(self.ext + compression))) + err_msg=( + "atom coordinate mismatch between original and {0!s} " + "file".format(self.ext + compression) + ), + ) def test_write_empty_atomgroup(self, universe, outfile): - sel = universe.select_atoms('name doesntexist') + sel = universe.select_atoms("name doesntexist") with pytest.raises(IndexError): sel.write(outfile) - @pytest.mark.parametrize('selection', ('name CA', - 'segid 4AKE and resname LEU', - 'segid 4AKE')) + @pytest.mark.parametrize( + "selection", ("name CA", "segid 4AKE and resname LEU", "segid 4AKE") + ) def test_write_selection(self, universe, outfile, selection): sel = universe.select_atoms(selection) sel.write(outfile) u2 = self.universe_from_tmp(outfile) # check EVERYTHING, otherwise we might get false positives! sel2 = u2.atoms - assert len(u2.atoms) == len(sel.atoms), ("written selection does not " - "match original selection") + assert len(u2.atoms) == len(sel.atoms), ( + "written selection does not " "match original selection" + ) assert_almost_equal( - sel2.positions, sel.positions, self.precision, - err_msg="written coordinates do not agree with original") + sel2.positions, + sel.positions, + self.precision, + err_msg="written coordinates do not agree with original", + ) def test_write_Residue(self, universe, outfile): - G = universe.select_atoms('segid 4AKE and resname ARG' - ).residues[-2].atoms # 2nd to last Arg + G = ( + universe.select_atoms("segid 4AKE and resname ARG") + .residues[-2] + .atoms + ) # 2nd to last Arg G.write(outfile) u2 = self.universe_from_tmp(outfile) # check EVERYTHING, otherwise we might get false positives! G2 = u2.atoms - assert len(u2.atoms) == len(G.atoms), ("written R206 Residue does not " - "match original ResidueGroup") + assert len(u2.atoms) == len(G.atoms), ( + "written R206 Residue does not " "match original ResidueGroup" + ) assert_almost_equal( - G2.positions, G.positions, self.precision, + G2.positions, + G.positions, + self.precision, err_msg="written Residue R206 coordinates do not " - "agree with original") + "agree with original", + ) def test_write_Universe(self, universe, outfile): U = universe with mda.Writer(outfile) as W: W.write(U) u2 = self.universe_from_tmp(outfile) - assert len(u2.atoms) == len(U.atoms), ("written 4AKE universe does " - "not match original universe " - "in size") + assert len(u2.atoms) == len(U.atoms), ( + "written 4AKE universe does " + "not match original universe " + "in size" + ) assert_almost_equal( - u2.atoms.positions, U.atoms.positions, self.precision, + u2.atoms.positions, + U.atoms.positions, + self.precision, err_msg="written universe 4AKE coordinates do not " - "agree with original") + "agree with original", + ) class TestWritePDB(_WriteAtoms): @@ -341,16 +376,18 @@ def test_rotate(self, u, coords): ag.positions = vec.copy() res_ag = ag.rotate(R[:3, :3]) assert_equal(ag, res_ag) - assert_almost_equal(ag.positions[0], [np.cos(angle), - np.sin(angle), - 0]) + assert_almost_equal( + ag.positions[0], [np.cos(angle), np.sin(angle), 0] + ) ag.positions = vec.copy() ag.rotate(R[:3, :3], vec[0]) assert_almost_equal(ag.positions[0], vec[0]) - assert_almost_equal(ag.positions[1], [-2*np.cos(angle) + 1, - -2*np.sin(angle), - 0], decimal=6) + assert_almost_equal( + ag.positions[1], + [-2 * np.cos(angle) + 1, -2 * np.sin(angle), 0], + decimal=6, + ) def test_rotateby(self, u, coords): R = np.eye(3) @@ -368,16 +405,17 @@ def test_rotateby(self, u, coords): # needs to be rotated about origin res_ag = ag.rotateby(np.rad2deg(angle), axis) assert_equal(res_ag, ag) - assert_almost_equal(ag.positions[0], [np.cos(angle), - np.sin(angle), - 0]) + assert_almost_equal( + ag.positions[0], [np.cos(angle), np.sin(angle), 0] + ) ag.positions = vec.copy() ag.rotateby(np.rad2deg(angle), axis, point=vec[0]) assert_almost_equal(ag.positions[0], vec[0]) - assert_almost_equal(ag.positions[1], [-2*np.cos(angle) + 1, - -2*np.sin(angle), - 0]) + assert_almost_equal( + ag.positions[1], + [-2 * np.cos(angle) + 1, -2 * np.sin(angle), 0], + ) def test_transform_rotation_only(self, u, coords): R = np.eye(3) @@ -394,9 +432,9 @@ def test_transform_rotation_only(self, u, coords): R = transformations.rotation_matrix(angle, axis) ag.positions = vec.copy() ag.transform(R) - assert_almost_equal(ag.positions[0], [np.cos(angle), - np.sin(angle), - 0]) + assert_almost_equal( + ag.positions[0], [np.cos(angle), np.sin(angle), 0] + ) def test_transform_translation_only(self, u, center_of_geometry): disp = np.ones(3) @@ -419,9 +457,9 @@ def test_transform_translation_and_rotation(self, u): ag.positions = [[1, 0, 0], [-1, 0, 0]] ag.transform(T) - assert_almost_equal(ag.positions[0], [np.cos(angle) + 1, - np.sin(angle) + 1, - 1]) + assert_almost_equal( + ag.positions[0], [np.cos(angle) + 1, np.sin(angle) + 1, 1] + ) class TestCenter(object): @@ -438,12 +476,12 @@ def test_center_1(self, ag): def test_center_2(self, ag): weights = np.zeros(ag.n_atoms) - weights[:4] = 1. / 4. + weights[:4] = 1.0 / 4.0 assert_almost_equal(ag.center(weights), ag.positions[:4].mean(axis=0)) def test_center_duplicates(self, ag): weights = np.ones(ag.n_atoms) - weights[0] = 2. + weights[0] = 2.0 ref = ag.center(weights) ag2 = ag + ag[0] with pytest.warns(DuplicateWarning): @@ -461,28 +499,30 @@ def test_center_wrong_shape(self, ag): with pytest.raises(ValueError): ag.center(weights) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_center_unwrap(self, level, compound, is_triclinic): u = UnWrapUniverse(is_triclinic=is_triclinic) # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 - elif compound == 'segments': - group = u.atoms[23:47] # molecules 10, 11, 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 + elif compound == "segments": + group = u.atoms[23:47] # molecules 10, 11, 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # get the expected results - center = group.center(weights=None, wrap=False, - compound=compound, unwrap=True) + center = group.center( + weights=None, wrap=False, compound=compound, unwrap=True + ) ref_center = u.center(compound=compound) assert_almost_equal(ref_center, center, decimal=4) @@ -492,8 +532,9 @@ def test_center_unwrap_wrap_true_group(self): # select group appropriate for compound: group = u.atoms[39:47] # molecule 12 with pytest.raises(ValueError): - group.center(weights=None, compound='group', - unwrap=True, wrap=True) + group.center( + weights=None, compound="group", unwrap=True, wrap=True + ) class TestSplit(object): @@ -501,11 +542,12 @@ class TestSplit(object): @pytest.fixture() def ag(self): universe = mda.Universe(PSF, DCD) - return universe.select_atoms("resid 1:50 and not resname LYS and " - "name CA CB") + return universe.select_atoms( + "resid 1:50 and not resname LYS and " "name CA CB" + ) def test_split_atoms(self, ag): - sg = ag.split('atom') + sg = ag.split("atom") assert len(sg) == len(ag) for g, ref_atom in zip(sg, ag): atom = g[0] @@ -533,7 +575,7 @@ def test_split_segments(self, ag): def test_split_VE(self, ag): with pytest.raises(ValueError): - ag.split('something') + ag.split("something") class TestAtomGroupProperties(object): @@ -545,47 +587,65 @@ class TestAtomGroupProperties(object): - setting the property on Atom changes AG - _unique_restore_mask works correctly """ + @staticmethod def get_new(att_type): """Return enough values to change the small g""" - if att_type == 'string': - return ['A', 'B', 'C', 'D', 'E', 'F'] - elif att_type == 'float': - return np.array([0.001, 0.002, 0.003, 0.005, 0.012, 0.025], - dtype=np.float32) - elif att_type == 'int': + if att_type == "string": + return ["A", "B", "C", "D", "E", "F"] + elif att_type == "float": + return np.array( + [0.001, 0.002, 0.003, 0.005, 0.012, 0.025], dtype=np.float32 + ) + elif att_type == "int": return [4, 6, 8, 1, 5, 4] @pytest.fixture def ag(self): - u = make_Universe(('names', 'resids', 'segids', 'types', 'altLocs', - 'charges', 'masses', 'radii', 'bfactors', - 'occupancies')) + u = make_Universe( + ( + "names", + "resids", + "segids", + "types", + "altLocs", + "charges", + "masses", + "radii", + "bfactors", + "occupancies", + ) + ) u.atoms.occupancies = 1.0 main = u.atoms idx = [0, 1, 4, 7, 11, 14] return main[idx] - attributes = (('name', 'names', 'string'), - ('type', 'types', 'string'), - ('altLoc', 'altLocs', 'string'), - ('charge', 'charges', 'float'), - ('mass', 'masses', 'float'), - ('radius', 'radii', 'float'), - ('bfactor', 'bfactors', 'float'), - ('occupancy', 'occupancies', 'float')) + attributes = ( + ("name", "names", "string"), + ("type", "types", "string"), + ("altLoc", "altLocs", "string"), + ("charge", "charges", "float"), + ("mass", "masses", "float"), + ("radius", "radii", "float"), + ("bfactor", "bfactors", "float"), + ("occupancy", "occupancies", "float"), + ) - @pytest.mark.parametrize('att, atts, att_type', attributes) + @pytest.mark.parametrize("att, atts, att_type", attributes) def test_ag_matches_atom(self, att, atts, ag, att_type): """Checking Atomgroup property matches Atoms""" # Check that accessing via AtomGroup is identical to doing # a list comprehension over AG ref = [getattr(atom, att) for atom in ag] - assert_equal(ref, getattr(ag, atts), - err_msg="AtomGroup doesn't match Atoms for property: " - "{0}".format(att)) + assert_equal( + ref, + getattr(ag, atts), + err_msg="AtomGroup doesn't match Atoms for property: " + "{0}".format(att), + ) - @pytest.mark.parametrize('att, atts, att_type', attributes) + @pytest.mark.parametrize("att, atts, att_type", attributes) def test_atom_check_ag(self, att, atts, ag, att_type): """Changing Atom, checking AtomGroup matches this""" vals = self.get_new(att_type) @@ -595,21 +655,24 @@ def test_atom_check_ag(self, att, atts, ag, att_type): # Check that AtomGroup returns new values other = getattr(ag, atts) - assert_equal(vals, other, - err_msg="Change to Atoms not reflected in AtomGroup for " - "property: {0}".format(att)) + assert_equal( + vals, + other, + err_msg="Change to Atoms not reflected in AtomGroup for " + "property: {0}".format(att), + ) def test_ag_unique_restore_mask(self, ag): # assert that ag is unique: assert ag.isunique # assert restore mask cache is empty: with pytest.raises(KeyError): - _ = ag._cache['unique_restore_mask'] + _ = ag._cache["unique_restore_mask"] # access unique property: uag = ag.asunique() # assert restore mask cache is still empty since ag is unique: with pytest.raises(KeyError): - _ = ag._cache['unique_restore_mask'] + _ = ag._cache["unique_restore_mask"] # make sure that accessing the restore mask of the unique AtomGroup # raises a RuntimeError: with pytest.raises(RuntimeError): @@ -621,11 +684,11 @@ def test_ag_unique_restore_mask(self, ag): assert not ag.isunique # assert cache is empty: with pytest.raises(KeyError): - _ = ag._cache['unique_restore_mask'] + _ = ag._cache["unique_restore_mask"] # access unique property: uag = ag.unique # check if caching works as expected: - assert ag._cache['unique_restore_mask'] is ag._unique_restore_mask + assert ag._cache["unique_restore_mask"] is ag._unique_restore_mask # assert that restore mask cache of ag.unique hasn't been set: with pytest.raises(RuntimeError): _ = ag.unique._unique_restore_mask @@ -640,6 +703,7 @@ class TestOrphans(object): - should have access to Universe - should be able to use the Reader (coordinates) """ + def test_atom(self): u = mda.Universe(two_water_gro) @@ -669,13 +733,14 @@ def getter(): class TestCrossUniverse(object): """Test behaviour when we mix Universes""" + @pytest.mark.parametrize( # Checks Atom to Atom, Atom to AG, AG to Atom and AG to AG - 'index_u1, index_u2', - itertools.product([0, 1], repeat=2) + "index_u1, index_u2", + itertools.product([0, 1], repeat=2), ) def test_add_mixed_universes(self, index_u1, index_u2): - """ Issue #532 + """Issue #532 Checks that adding objects from different universes doesn't proceed quietly. """ @@ -689,7 +754,7 @@ def test_add_mixed_universes(self, index_u1, index_u2): A[index_u1] + B[index_u2] def test_adding_empty_ags(self): - """ Check that empty AtomGroups don't trip up on the Universe check """ + """Check that empty AtomGroups don't trip up on the Universe check""" u = mda.Universe(two_water_gro) assert len(u.atoms[[]] + u.atoms[:3]) == 3 @@ -700,12 +765,12 @@ class TestDihedralSelections(object): dih_prec = 2 @staticmethod - @pytest.fixture(scope='module') + @pytest.fixture(scope="module") def GRO(): return mda.Universe(GRO) @staticmethod - @pytest.fixture(scope='module') + @pytest.fixture(scope="module") def PSFDCD(): return mda.Universe(PSF, DCD) @@ -720,34 +785,37 @@ def memprot(): return mda.Universe(GRO_MEMPROT) @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def resgroup(GRO): return GRO.segments[0].residues[8:10] def test_phi_selection(self, GRO): phisel = GRO.segments[0].residues[9].phi_selection() - assert_equal(phisel.names, ['C', 'N', 'CA', 'C']) + assert_equal(phisel.names, ["C", "N", "CA", "C"]) assert_equal(phisel.residues.resids, [9, 10]) - assert_equal(phisel.residues.resnames, ['PRO', 'GLY']) + assert_equal(phisel.residues.resnames, ["PRO", "GLY"]) - @pytest.mark.parametrize('kwargs,names', [ - ({'c_name': 'O'}, ['O', 'N', 'CA', 'O']), - ({'n_name': 'O'}, ['C', 'O', 'CA', 'C']), - ({'ca_name': 'O'}, ['C', 'N', 'O', 'C']) - ]) + @pytest.mark.parametrize( + "kwargs,names", + [ + ({"c_name": "O"}, ["O", "N", "CA", "O"]), + ({"n_name": "O"}, ["C", "O", "CA", "C"]), + ({"ca_name": "O"}, ["C", "N", "O", "C"]), + ], + ) def test_phi_selection_name(self, GRO, kwargs, names): phisel = GRO.segments[0].residues[9].phi_selection(**kwargs) assert_equal(phisel.names, names) assert_equal(phisel.residues.resids, [9, 10]) - assert_equal(phisel.residues.resnames, ['PRO', 'GLY']) + assert_equal(phisel.residues.resnames, ["PRO", "GLY"]) def test_phi_selections_single(self, GRO): rgsel = GRO.segments[0].residues[[9]].phi_selections() assert len(rgsel) == 1 phisel = rgsel[0] - assert_equal(phisel.names, ['C', 'N', 'CA', 'C']) + assert_equal(phisel.names, ["C", "N", "CA", "C"]) assert_equal(phisel.residues.resids, [9, 10]) - assert_equal(phisel.residues.resnames, ['PRO', 'GLY']) + assert_equal(phisel.residues.resnames, ["PRO", "GLY"]) def test_phi_selections_empty(self, GRO): rgsel = GRO.segments[0].residues[[]].phi_selections() @@ -758,11 +826,14 @@ def test_phi_selections(self, resgroup): rssel = [r.phi_selection() for r in resgroup] assert_equal(rgsel, rssel) - @pytest.mark.parametrize('kwargs,names', [ - ({'c_name': 'O'}, ['O', 'N', 'CA', 'O']), - ({'n_name': 'O'}, ['C', 'O', 'CA', 'C']), - ({'ca_name': 'O'}, ['C', 'N', 'O', 'C']) - ]) + @pytest.mark.parametrize( + "kwargs,names", + [ + ({"c_name": "O"}, ["O", "N", "CA", "O"]), + ({"n_name": "O"}, ["C", "O", "CA", "C"]), + ({"ca_name": "O"}, ["C", "N", "O", "C"]), + ], + ) def test_phi_selections_name(self, resgroup, kwargs, names): rgsel = resgroup.phi_selections(**kwargs) for ag in rgsel: @@ -770,28 +841,31 @@ def test_phi_selections_name(self, resgroup, kwargs, names): def test_psi_selection(self, GRO): psisel = GRO.segments[0].residues[9].psi_selection() - assert_equal(psisel.names, ['N', 'CA', 'C', 'N']) + assert_equal(psisel.names, ["N", "CA", "C", "N"]) assert_equal(psisel.residues.resids, [10, 11]) - assert_equal(psisel.residues.resnames, ['GLY', 'ALA']) + assert_equal(psisel.residues.resnames, ["GLY", "ALA"]) - @pytest.mark.parametrize('kwargs,names', [ - ({'c_name': 'O'}, ['N', 'CA', 'O', 'N']), - ({'n_name': 'O'}, ['O', 'CA', 'C', 'O']), - ({'ca_name': 'O'}, ['N', 'O', 'C', 'N']), - ]) + @pytest.mark.parametrize( + "kwargs,names", + [ + ({"c_name": "O"}, ["N", "CA", "O", "N"]), + ({"n_name": "O"}, ["O", "CA", "C", "O"]), + ({"ca_name": "O"}, ["N", "O", "C", "N"]), + ], + ) def test_psi_selection_name(self, GRO, kwargs, names): psisel = GRO.segments[0].residues[9].psi_selection(**kwargs) assert_equal(psisel.names, names) assert_equal(psisel.residues.resids, [10, 11]) - assert_equal(psisel.residues.resnames, ['GLY', 'ALA']) + assert_equal(psisel.residues.resnames, ["GLY", "ALA"]) def test_psi_selections_single(self, GRO): rgsel = GRO.segments[0].residues[[9]].psi_selections() assert len(rgsel) == 1 psisel = rgsel[0] - assert_equal(psisel.names, ['N', 'CA', 'C', 'N']) + assert_equal(psisel.names, ["N", "CA", "C", "N"]) assert_equal(psisel.residues.resids, [10, 11]) - assert_equal(psisel.residues.resnames, ['GLY', 'ALA']) + assert_equal(psisel.residues.resnames, ["GLY", "ALA"]) def test_psi_selections_empty(self, GRO): rgsel = GRO.segments[0].residues[[]].psi_selections() @@ -802,11 +876,14 @@ def test_psi_selections(self, resgroup): rssel = [r.psi_selection() for r in resgroup] assert_equal(rgsel, rssel) - @pytest.mark.parametrize('kwargs,names', [ - ({'c_name': 'O'}, ['N', 'CA', 'O', 'N']), - ({'n_name': 'O'}, ['O', 'CA', 'C', 'O']), - ({'ca_name': 'O'}, ['N', 'O', 'C', 'N']), - ]) + @pytest.mark.parametrize( + "kwargs,names", + [ + ({"c_name": "O"}, ["N", "CA", "O", "N"]), + ({"n_name": "O"}, ["O", "CA", "C", "O"]), + ({"ca_name": "O"}, ["N", "O", "C", "N"]), + ], + ) def test_psi_selections_name(self, resgroup, kwargs, names): rgsel = resgroup.psi_selections(**kwargs) for ag in rgsel: @@ -814,20 +891,23 @@ def test_psi_selections_name(self, resgroup, kwargs, names): def test_omega_selection(self, GRO): osel = GRO.segments[0].residues[7].omega_selection() - assert_equal(osel.names, ['CA', 'C', 'N', 'CA']) + assert_equal(osel.names, ["CA", "C", "N", "CA"]) assert_equal(osel.residues.resids, [8, 9]) - assert_equal(osel.residues.resnames, ['ALA', 'PRO']) + assert_equal(osel.residues.resnames, ["ALA", "PRO"]) - @pytest.mark.parametrize('kwargs,names', [ - ({'c_name': 'O'}, ['CA', 'O', 'N', 'CA']), - ({'n_name': 'O'}, ['CA', 'C', 'O', 'CA']), - ({'ca_name': 'O'}, ['O', 'C', 'N', 'O']), - ]) + @pytest.mark.parametrize( + "kwargs,names", + [ + ({"c_name": "O"}, ["CA", "O", "N", "CA"]), + ({"n_name": "O"}, ["CA", "C", "O", "CA"]), + ({"ca_name": "O"}, ["O", "C", "N", "O"]), + ], + ) def test_omega_selection_name(self, GRO, kwargs, names): osel = GRO.segments[0].residues[7].omega_selection(**kwargs) assert_equal(osel.names, names) assert_equal(osel.residues.resids, [8, 9]) - assert_equal(osel.residues.resnames, ['ALA', 'PRO']) + assert_equal(osel.residues.resnames, ["ALA", "PRO"]) def test_omega_selections_empty(self, GRO): rgsel = GRO.segments[0].residues[[]].omega_selections() @@ -837,20 +917,23 @@ def test_omega_selections_single(self, GRO): rgsel = GRO.segments[0].residues[[7]].omega_selections() assert len(rgsel) == 1 osel = rgsel[0] - assert_equal(osel.names, ['CA', 'C', 'N', 'CA']) + assert_equal(osel.names, ["CA", "C", "N", "CA"]) assert_equal(osel.residues.resids, [8, 9]) - assert_equal(osel.residues.resnames, ['ALA', 'PRO']) + assert_equal(osel.residues.resnames, ["ALA", "PRO"]) def test_omega_selections(self, resgroup): rgsel = resgroup.omega_selections() rssel = [r.omega_selection() for r in resgroup] assert_equal(rgsel, rssel) - @pytest.mark.parametrize('kwargs,names', [ - ({'c_name': 'O'}, ['CA', 'O', 'N', 'CA']), - ({'n_name': 'O'}, ['CA', 'C', 'O', 'CA']), - ({'ca_name': 'O'}, ['O', 'C', 'N', 'O']), - ]) + @pytest.mark.parametrize( + "kwargs,names", + [ + ({"c_name": "O"}, ["CA", "O", "N", "CA"]), + ({"n_name": "O"}, ["CA", "C", "O", "CA"]), + ({"ca_name": "O"}, ["O", "C", "N", "O"]), + ], + ) def test_omega_selections_name(self, resgroup, kwargs, names): rgsel = resgroup.omega_selections(**kwargs) for ag in rgsel: @@ -858,29 +941,32 @@ def test_omega_selections_name(self, resgroup, kwargs, names): def test_chi1_selection(self, GRO): sel = GRO.segments[0].residues[12].chi1_selection() # LYS - assert_equal(sel.names, ['N', 'CA', 'CB', 'CG']) + assert_equal(sel.names, ["N", "CA", "CB", "CG"]) assert_equal(sel.residues.resids, [13]) - assert_equal(sel.residues.resnames, ['LYS']) - - @pytest.mark.parametrize('kwargs,names', [ - ({'n_name': 'O'}, ['O', 'CA', 'CB', 'CG']), - ({'ca_name': 'O'}, ['N', 'O', 'CB', 'CG']), - ({'cb_name': 'O'}, ['N', 'CA', 'O', 'CG']), - ({'cg_name': 'O'}, ['N', 'CA', 'CB', 'O']), - ]) + assert_equal(sel.residues.resnames, ["LYS"]) + + @pytest.mark.parametrize( + "kwargs,names", + [ + ({"n_name": "O"}, ["O", "CA", "CB", "CG"]), + ({"ca_name": "O"}, ["N", "O", "CB", "CG"]), + ({"cb_name": "O"}, ["N", "CA", "O", "CG"]), + ({"cg_name": "O"}, ["N", "CA", "CB", "O"]), + ], + ) def test_chi1_selection_name(self, GRO, kwargs, names): sel = GRO.segments[0].residues[12].chi1_selection(**kwargs) # LYS assert_equal(sel.names, names) assert_equal(sel.residues.resids, [13]) - assert_equal(sel.residues.resnames, ['LYS']) + assert_equal(sel.residues.resnames, ["LYS"]) def test_chi1_selections_single(self, GRO): rgsel = GRO.segments[0].residues[[12]].chi1_selections() assert len(rgsel) == 1 sel = rgsel[0] - assert_equal(sel.names, ['N', 'CA', 'CB', 'CG']) + assert_equal(sel.names, ["N", "CA", "CB", "CG"]) assert_equal(sel.residues.resids, [13]) - assert_equal(sel.residues.resnames, ['LYS']) + assert_equal(sel.residues.resnames, ["LYS"]) def test_chi1_selections_empty(self, GRO): rgsel = GRO.segments[0].residues[[]].chi1_selections() @@ -911,10 +997,28 @@ def test_chi1_selection_non_cg_charmm(self, resname, PSFDCD): res = resgroup[len(resgroup) // 2] assert res.chi1_selection() is not None - @pytest.mark.parametrize("resname", ["ARG", "ASP", "CYS", "GLN", "GLU", - "HIS", "ILE", "LEU", "LYS", "MET", - "PHE", "PRO", "SER", "THR", "TRP", - "TYR", "VAL"]) + @pytest.mark.parametrize( + "resname", + [ + "ARG", + "ASP", + "CYS", + "GLN", + "GLU", + "HIS", + "ILE", + "LEU", + "LYS", + "MET", + "PHE", + "PRO", + "SER", + "THR", + "TRP", + "TYR", + "VAL", + ], + ) def test_chi1_selection_all_res(self, resname, memprot): resgroup = memprot.select_atoms(f"resname {resname}").residues # get middle one @@ -1005,56 +1109,79 @@ class TestUnwrapFlag(object): prec = 3 ref_noUnwrap_residues = { - 'center_of_geometry': np.array([[21.356, 28.52, 36.762], - [32.062, 36.16, 27.679], - [27.071, 29.997, 28.506]], - dtype=np.float32), - 'center_of_mass': np.array([[21.286, 28.407, 36.629], - [31.931, 35.814, 27.916], - [26.817, 29.41, 29.05]], - dtype=np.float32), - 'moment_of_inertia': - np.array([[7333.79167791, -211.8997285, -721.50785456], - [-211.8997285, 7059.07470427, -91.32156884], - [-721.50785456, -91.32156884, 6509.31735029]]), - 'asphericity': np.array([0.135, 0.047, 0.094]), - 'shape_parameter': np.array([-0.112, -0.004, 0.02]), + "center_of_geometry": np.array( + [ + [21.356, 28.52, 36.762], + [32.062, 36.16, 27.679], + [27.071, 29.997, 28.506], + ], + dtype=np.float32, + ), + "center_of_mass": np.array( + [ + [21.286, 28.407, 36.629], + [31.931, 35.814, 27.916], + [26.817, 29.41, 29.05], + ], + dtype=np.float32, + ), + "moment_of_inertia": np.array( + [ + [7333.79167791, -211.8997285, -721.50785456], + [-211.8997285, 7059.07470427, -91.32156884], + [-721.50785456, -91.32156884, 6509.31735029], + ] + ), + "asphericity": np.array([0.135, 0.047, 0.094]), + "shape_parameter": np.array([-0.112, -0.004, 0.02]), } ref_Unwrap_residues = { - 'center_of_geometry': np.array([[21.356, 41.685, 40.501], - [44.577, 43.312, 79.039], - [2.204, 27.722, 54.023]], - dtype=np.float32), - 'center_of_mass': np.array([[21.286, 41.664, 40.465], - [44.528, 43.426, 78.671], - [2.111, 27.871, 53.767]], - dtype=np.float32), - 'moment_of_inertia': np.array([[16687.941, -1330.617, 2925.883], - [-1330.617, 19256.178, 3354.832], - [2925.883, 3354.832, 8989.946]]), - 'asphericity': np.array([0.61 , 0.701, 0.381]), - 'shape_parameter': np.array([-0.461, 0.35 , 0.311]), + "center_of_geometry": np.array( + [ + [21.356, 41.685, 40.501], + [44.577, 43.312, 79.039], + [2.204, 27.722, 54.023], + ], + dtype=np.float32, + ), + "center_of_mass": np.array( + [ + [21.286, 41.664, 40.465], + [44.528, 43.426, 78.671], + [2.111, 27.871, 53.767], + ], + dtype=np.float32, + ), + "moment_of_inertia": np.array( + [ + [16687.941, -1330.617, 2925.883], + [-1330.617, 19256.178, 3354.832], + [2925.883, 3354.832, 8989.946], + ] + ), + "asphericity": np.array([0.61, 0.701, 0.381]), + "shape_parameter": np.array([-0.461, 0.35, 0.311]), } ref_noUnwrap = { - 'center_of_geometry': np.array([5.1, 7.5, 7.], dtype=np.float32), - 'center_of_mass': np.array([6.48785, 7.5, 7.0], dtype=np.float32), - 'moment_of_inertia': np.array([[0.0, 0.0, 0.0], - [0.0, 98.6542, 0.0], - [0.0, 0.0, 98.65421327]]), - 'asphericity': 1.0, - 'shape_parameter': 1.0, + "center_of_geometry": np.array([5.1, 7.5, 7.0], dtype=np.float32), + "center_of_mass": np.array([6.48785, 7.5, 7.0], dtype=np.float32), + "moment_of_inertia": np.array( + [[0.0, 0.0, 0.0], [0.0, 98.6542, 0.0], [0.0, 0.0, 98.65421327]] + ), + "asphericity": 1.0, + "shape_parameter": 1.0, } ref_Unwrap = { - 'center_of_geometry': np.array([10.1, 7.5, 7.], dtype=np.float32), - 'center_of_mass': np.array([6.8616, 7.5, 7.], dtype=np.float32), - 'moment_of_inertia': np.array([[0.0, 0.0, 0.0], - [0.0, 132.673, 0.0], - [0.0, 0.0, 132.673]]), - 'asphericity': 1.0, - 'shape_parameter': 1.0, + "center_of_geometry": np.array([10.1, 7.5, 7.0], dtype=np.float32), + "center_of_mass": np.array([6.8616, 7.5, 7.0], dtype=np.float32), + "moment_of_inertia": np.array( + [[0.0, 0.0, 0.0], [0.0, 132.673, 0.0], [0.0, 0.0, 132.673]] + ), + "asphericity": 1.0, + "shape_parameter": 1.0, } @pytest.fixture(params=[False, True]) # params indicate shuffling @@ -1076,28 +1203,41 @@ def unwrap_group(self): group.masses = [100.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] return group - @pytest.mark.parametrize('unwrap, ref', ((True, ref_Unwrap_residues), - (False, ref_noUnwrap_residues))) - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass', - 'moment_of_inertia', - 'asphericity', - 'shape_parameter')) + @pytest.mark.parametrize( + "unwrap, ref", + ((True, ref_Unwrap_residues), (False, ref_noUnwrap_residues)), + ) + @pytest.mark.parametrize( + "method_name", + ( + "center_of_geometry", + "center_of_mass", + "moment_of_inertia", + "asphericity", + "shape_parameter", + ), + ) def test_residues(self, ag, unwrap, ref, method_name): method = getattr(ag, method_name) if unwrap: - result = method(compound='residues', unwrap=unwrap) + result = method(compound="residues", unwrap=unwrap) else: # We test unwrap=False as the default behavior - result = method(compound='residues') + result = method(compound="residues") assert_almost_equal(result, ref[method_name], self.prec) - @pytest.mark.parametrize('unwrap, ref', ((True, ref_Unwrap), - (False, ref_noUnwrap))) - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass', - 'moment_of_inertia', - 'asphericity')) + @pytest.mark.parametrize( + "unwrap, ref", ((True, ref_Unwrap), (False, ref_noUnwrap)) + ) + @pytest.mark.parametrize( + "method_name", + ( + "center_of_geometry", + "center_of_mass", + "moment_of_inertia", + "asphericity", + ), + ) def test_group(self, unwrap_group, unwrap, ref, method_name): method = getattr(unwrap_group, method_name) if unwrap: @@ -1113,46 +1253,73 @@ class TestPBCFlag(object): prec = 3 ref_noPBC = { - 'center_of_geometry': np.array([4.23789883, 0.62429816, 2.43123484], - dtype=np.float32), - 'center_of_mass': np.array([4.1673783, 0.70507009, 2.21175832]), - 'radius_of_gyration': 119.30368949900134, - 'shape_parameter': 0.6690026954813445, - 'asphericity': 0.5305456387833748, - 'moment_of_inertia': - np.array([[152117.06620921, 55149.54042136, -26630.46034023], - [55149.54042136, 72869.64061494, 21998.1778074], - [-26630.46034023, 21998.1778074, 162388.70002471]]), - 'bbox': np.array([[-75.74159241, -144.86634827, -94.47974396], - [95.83090973, 115.11561584, 88.09812927]], - dtype=np.float32), - 'bsphere': (173.40482, - np.array([4.23789883, 0.62429816, 2.43123484], - dtype=np.float32)), - 'principal_axes': np.array([[0.78787867, 0.26771575, -0.55459488], - [-0.40611024, -0.45112859, -0.7947059], - [-0.46294889, 0.85135849, -0.24671249]]) + "center_of_geometry": np.array( + [4.23789883, 0.62429816, 2.43123484], dtype=np.float32 + ), + "center_of_mass": np.array([4.1673783, 0.70507009, 2.21175832]), + "radius_of_gyration": 119.30368949900134, + "shape_parameter": 0.6690026954813445, + "asphericity": 0.5305456387833748, + "moment_of_inertia": np.array( + [ + [152117.06620921, 55149.54042136, -26630.46034023], + [55149.54042136, 72869.64061494, 21998.1778074], + [-26630.46034023, 21998.1778074, 162388.70002471], + ] + ), + "bbox": np.array( + [ + [-75.74159241, -144.86634827, -94.47974396], + [95.83090973, 115.11561584, 88.09812927], + ], + dtype=np.float32, + ), + "bsphere": ( + 173.40482, + np.array([4.23789883, 0.62429816, 2.43123484], dtype=np.float32), + ), + "principal_axes": np.array( + [ + [0.78787867, 0.26771575, -0.55459488], + [-0.40611024, -0.45112859, -0.7947059], + [-0.46294889, 0.85135849, -0.24671249], + ] + ), } ref_PBC = { - 'center_of_geometry': np.array([26.82960892, 31.5592289, 30.98238945], - dtype=np.float32), - 'center_of_mass': np.array([26.67781143, 31.2104336, 31.19796289]), - 'radius_of_gyration': 27.713008969174918, - 'shape_parameter': 0.0017390512580463542, - 'asphericity': 0.020601215358731016, - 'moment_of_inertia': - np.array([[7333.79167791, -211.8997285, -721.50785456], - [-211.8997285, 7059.07470427, -91.32156884], - [-721.50785456, -91.32156884, 6509.31735029]]), - 'bbox': np.array([[0.145964116, 0.0185623169, 0.0431785583], - [55.3314018, 55.4227829, 55.4158211]], - dtype=np.float32), - 'bsphere': (47.923367, np.array([26.82960892, 31.5592289, 30.98238945], - dtype=np.float32)), - 'principal_axes': np.array([[0.85911708, -0.19258726, -0.4741603], - [0.07520116, 0.96394227, -0.25526473], - [0.50622389, 0.18364489, 0.84262206]]) + "center_of_geometry": np.array( + [26.82960892, 31.5592289, 30.98238945], dtype=np.float32 + ), + "center_of_mass": np.array([26.67781143, 31.2104336, 31.19796289]), + "radius_of_gyration": 27.713008969174918, + "shape_parameter": 0.0017390512580463542, + "asphericity": 0.020601215358731016, + "moment_of_inertia": np.array( + [ + [7333.79167791, -211.8997285, -721.50785456], + [-211.8997285, 7059.07470427, -91.32156884], + [-721.50785456, -91.32156884, 6509.31735029], + ] + ), + "bbox": np.array( + [ + [0.145964116, 0.0185623169, 0.0431785583], + [55.3314018, 55.4227829, 55.4158211], + ], + dtype=np.float32, + ), + "bsphere": ( + 47.923367, + np.array([26.82960892, 31.5592289, 30.98238945], dtype=np.float32), + ), + "principal_axes": np.array( + [ + [0.85911708, -0.19258726, -0.4741603], + [0.07520116, 0.96394227, -0.25526473], + [0.50622389, 0.18364489, 0.84262206], + ] + ), } @pytest.fixture() @@ -1160,17 +1327,23 @@ def ag(self): universe = mda.Universe(TRZ_psf, TRZ) return universe.residues[0:3] - @pytest.mark.parametrize('wrap, ref', ((True, ref_PBC), - (False, ref_noPBC))) - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass', - 'radius_of_gyration', - 'shape_parameter', - 'asphericity', - 'moment_of_inertia', - 'bbox', - 'bsphere', - 'principal_axes')) + @pytest.mark.parametrize( + "wrap, ref", ((True, ref_PBC), (False, ref_noPBC)) + ) + @pytest.mark.parametrize( + "method_name", + ( + "center_of_geometry", + "center_of_mass", + "radius_of_gyration", + "shape_parameter", + "asphericity", + "moment_of_inertia", + "bbox", + "bsphere", + "principal_axes", + ), + ) def test_wrap(self, ag, wrap, ref, method_name): method = getattr(ag, method_name) if wrap: @@ -1179,7 +1352,7 @@ def test_wrap(self, ag, wrap, ref, method_name): # Test no-wrap as the default behaviour result = method() - if method_name == 'bsphere': + if method_name == "bsphere": assert_almost_equal(result[0], ref[method_name][0], self.prec) assert_almost_equal(result[1], ref[method_name][1], self.prec) else: @@ -1192,6 +1365,7 @@ class TestAtomGroup(object): These are from before the big topology rework (aka #363) but are still valid. There is likely lots of duplication between here and other tests. """ + dih_prec = 2 @pytest.fixture() @@ -1222,25 +1396,22 @@ def test_getitem_int(self, universe): assert_equal(universe.atoms[0].ix, universe.atoms.ix[0]) def test_getitem_slice(self, universe): - assert_equal(universe.atoms[0:4].ix, - universe.atoms.ix[:4]) + assert_equal(universe.atoms[0:4].ix, universe.atoms.ix[:4]) def test_getitem_slice2(self, universe): - assert_equal(universe.atoms[0:8:2].ix, - universe.atoms.ix[0:8:2]) + assert_equal(universe.atoms[0:8:2].ix, universe.atoms.ix[0:8:2]) def test_getitem_IE(self, universe): - d = {'A': 1} + d = {"A": 1} with pytest.raises(IndexError): universe.atoms.__getitem__(d) def test_bad_make(self): with pytest.raises(TypeError): - mda.core.groups.AtomGroup(['these', 'are', 'not', 'atoms']) + mda.core.groups.AtomGroup(["these", "are", "not", "atoms"]) def test_invalid_index_initialisation(self, universe): - indices = [[1, 2, 3], - [4, 5, 6]] + indices = [[1, 2, 3], [4, 5, 6]] with pytest.raises(IndexError): mda.core.groups.AtomGroup(indices, universe) @@ -1279,15 +1450,22 @@ def test_len(self, ag): assert len(ag) == ag.n_atoms, "len and n_atoms disagree" def test_center_of_geometry(self, ag): - assert_almost_equal(ag.center_of_geometry(), - [-0.04223963, 0.0141824, -0.03505163], decimal=5) + assert_almost_equal( + ag.center_of_geometry(), + [-0.04223963, 0.0141824, -0.03505163], + decimal=5, + ) def test_center_of_mass(self, ag): - assert_almost_equal(ag.center_of_mass(), - [-0.01094035, 0.05727601, -0.12885778], decimal=5) + assert_almost_equal( + ag.center_of_mass(), + [-0.01094035, 0.05727601, -0.12885778], + decimal=5, + ) - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass')) + @pytest.mark.parametrize( + "method_name", ("center_of_geometry", "center_of_mass") + ) def test_center_duplicates(self, ag, method_name): ag2 = ag + ag[0] ref = getattr(ag, method_name)() @@ -1295,69 +1473,88 @@ def test_center_duplicates(self, ag, method_name): assert not np.allclose(getattr(ag2, method_name)(), ref) assert len(w) == 1 - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass')) - @pytest.mark.parametrize('name, compound', (('resids', 'residues'), - ('segids', 'segments'))) + @pytest.mark.parametrize( + "method_name", ("center_of_geometry", "center_of_mass") + ) + @pytest.mark.parametrize( + "name, compound", (("resids", "residues"), ("segids", "segments")) + ) def test_center_compounds(self, ag, name, compound, method_name): ref = [getattr(a, method_name)() for a in ag.groupby(name).values()] vals = getattr(ag, method_name)(wrap=False, compound=compound) assert_almost_equal(vals, ref, decimal=5) - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass')) - @pytest.mark.parametrize('name, compound', (('resids', 'residues'), - ('segids', 'segments'))) - @pytest.mark.parametrize('unwrap', (True, False)) - def test_center_compounds_pbc(self, ag, name, compound, - unwrap, method_name): + @pytest.mark.parametrize( + "method_name", ("center_of_geometry", "center_of_mass") + ) + @pytest.mark.parametrize( + "name, compound", (("resids", "residues"), ("segids", "segments")) + ) + @pytest.mark.parametrize("unwrap", (True, False)) + def test_center_compounds_pbc( + self, ag, name, compound, unwrap, method_name + ): ag.dimensions = [50, 50, 50, 90, 90, 90] - ref = [getattr(a, method_name)(unwrap=unwrap) - for a in ag.groupby(name).values()] - vals = getattr(ag, method_name)(compound=compound, - unwrap=unwrap) + ref = [ + getattr(a, method_name)(unwrap=unwrap) + for a in ag.groupby(name).values() + ] + vals = getattr(ag, method_name)(compound=compound, unwrap=unwrap) assert_almost_equal(vals, ref, decimal=5) - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass')) - @pytest.mark.parametrize('name, compound', (('molnums', 'molecules'), - ('fragindices', 'fragments'))) - def test_center_compounds_special(self, ag_molfrg, name, - compound, method_name): - ref = [getattr(a, method_name)() - for a in ag_molfrg.groupby(name).values()] + @pytest.mark.parametrize( + "method_name", ("center_of_geometry", "center_of_mass") + ) + @pytest.mark.parametrize( + "name, compound", + (("molnums", "molecules"), ("fragindices", "fragments")), + ) + def test_center_compounds_special( + self, ag_molfrg, name, compound, method_name + ): + ref = [ + getattr(a, method_name)() for a in ag_molfrg.groupby(name).values() + ] vals = getattr(ag_molfrg, method_name)(wrap=False, compound=compound) assert_almost_equal(vals, ref, decimal=5) - @pytest.mark.parametrize('method_name', ('center_of_geometry', - 'center_of_mass')) - @pytest.mark.parametrize('name, compound', (('molnums', 'molecules'), - ('fragindices', 'fragments'))) - @pytest.mark.parametrize('unwrap', (True, False)) - def test_center_compounds_special_pbc(self, ag_molfrg, name, compound, - unwrap, method_name): + @pytest.mark.parametrize( + "method_name", ("center_of_geometry", "center_of_mass") + ) + @pytest.mark.parametrize( + "name, compound", + (("molnums", "molecules"), ("fragindices", "fragments")), + ) + @pytest.mark.parametrize("unwrap", (True, False)) + def test_center_compounds_special_pbc( + self, ag_molfrg, name, compound, unwrap, method_name + ): ag_molfrg.dimensions = [50, 50, 50, 90, 90, 90] - ref = [getattr(a, method_name)(unwrap=unwrap) - for a in ag_molfrg.groupby(name).values()] - vals = getattr(ag_molfrg, method_name)(compound=compound, - unwrap=unwrap) + ref = [ + getattr(a, method_name)(unwrap=unwrap) + for a in ag_molfrg.groupby(name).values() + ] + vals = getattr(ag_molfrg, method_name)( + compound=compound, unwrap=unwrap + ) assert_almost_equal(vals, ref, decimal=5) def test_center_wrong_compound(self, ag): with pytest.raises(ValueError): ag.center(weights=None, compound="foo") - @pytest.mark.parametrize('compound', ('molecules', 'fragments')) + @pytest.mark.parametrize("compound", ("molecules", "fragments")) def test_center_compounds_special_fail(self, ag_no_molfrg, compound): with pytest.raises(NoDataError): ag_no_molfrg.center(weights=None, compound=compound) - @pytest.mark.parametrize('weights', (None, - np.array([0.0]), - np.array([2.0]))) - @pytest.mark.parametrize('compound', ('group', 'residues', 'segments', - 'molecules', 'fragments')) - @pytest.mark.parametrize('wrap', (False, True)) + @pytest.mark.parametrize( + "weights", (None, np.array([0.0]), np.array([2.0])) + ) + @pytest.mark.parametrize( + "compound", ("group", "residues", "segments", "molecules", "fragments") + ) + @pytest.mark.parametrize("wrap", (False, True)) def test_center_compounds_single(self, ag_molfrg, wrap, weights, compound): at = ag_molfrg[0] if weights is None or weights[0] != 0.0: @@ -1367,54 +1564,72 @@ def test_center_compounds_single(self, ag_molfrg, wrap, weights, compound): else: ref = at.position.astype(np.float64) else: - ref = np.full((3,), np.nan,np.float64) - if compound != 'group': + ref = np.full((3,), np.nan, np.float64) + if compound != "group": ref = ref.reshape((1, 3)) ag_s = mda.AtomGroup([at]) assert_equal(ref, ag_s.center(weights, wrap=wrap, compound=compound)) - @pytest.mark.parametrize('wrap', (False, True)) - @pytest.mark.parametrize('weights', (None, np.array([]))) - @pytest.mark.parametrize('compound', ('group', 'residues', 'segments', - 'molecules', 'fragments')) + @pytest.mark.parametrize("wrap", (False, True)) + @pytest.mark.parametrize("weights", (None, np.array([]))) + @pytest.mark.parametrize( + "compound", ("group", "residues", "segments", "molecules", "fragments") + ) def test_center_compounds_empty(self, ag_molfrg, wrap, weights, compound): ref = np.empty((0, 3), dtype=np.float64) ag_e = mda.AtomGroup([], ag_molfrg.universe) assert_equal(ref, ag_e.center(weights, wrap=wrap, compound=compound)) - @pytest.mark.parametrize('wrap', (False, True)) - @pytest.mark.parametrize('name, compound', (('', 'group'), - ('resids', 'residues'), - ('segids', 'segments'), - ('molnums', 'molecules'), - ('fragindices', 'fragments'))) - def test_center_compounds_zero_weights(self, ag_molfrg, wrap, name, - compound): - if compound == 'group': + @pytest.mark.parametrize("wrap", (False, True)) + @pytest.mark.parametrize( + "name, compound", + ( + ("", "group"), + ("resids", "residues"), + ("segids", "segments"), + ("molnums", "molecules"), + ("fragindices", "fragments"), + ), + ) + def test_center_compounds_zero_weights( + self, ag_molfrg, wrap, name, compound + ): + if compound == "group": ref = np.full((3,), np.nan) else: n_compounds = len(ag_molfrg.groupby(name)) ref = np.full((n_compounds, 3), np.nan, dtype=np.float64) weights = np.zeros(len(ag_molfrg)) - assert_equal(ref, ag_molfrg.center(weights, wrap=wrap, - compound=compound)) + assert_equal( + ref, ag_molfrg.center(weights, wrap=wrap, compound=compound) + ) def test_coordinates(self, ag): assert_almost_equal( ag.positions[1000:2000:200], - np.array([[3.94543672, -12.4060812, -7.26820087], - [13.21632767, 5.879035, -14.67914867], - [12.07735443, -9.00604534, 4.09301519], - [11.35541916, 7.0690732, -0.32511973], - [-13.26763439, 4.90658951, 10.6880455]], - dtype=np.float32)) + np.array( + [ + [3.94543672, -12.4060812, -7.26820087], + [13.21632767, 5.879035, -14.67914867], + [12.07735443, -9.00604534, 4.09301519], + [11.35541916, 7.0690732, -0.32511973], + [-13.26763439, 4.90658951, 10.6880455], + ], + dtype=np.float32, + ), + ) def test_principal_axes(self, ag): assert_almost_equal( ag.principal_axes(), - np.array([[1.53389276e-03, 4.41386224e-02, 9.99024239e-01], - [1.20986911e-02, 9.98951474e-01, -4.41539838e-02], - [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04],])) + np.array( + [ + [1.53389276e-03, 4.41386224e-02, 9.99024239e-01], + [1.20986911e-02, 9.98951474e-01, -4.41539838e-02], + [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], + ] + ), + ) def test_principal_axes_duplicates(self, ag): ag2 = ag + ag[0] @@ -1424,7 +1639,7 @@ def test_principal_axes_duplicates(self, ag): assert len(w) == 1 def test_moment_of_inertia_duplicates(self, universe): - ag = universe.select_atoms('segid 4AKE') + ag = universe.select_atoms("segid 4AKE") ag2 = ag + ag[0] ref = ag.moment_of_inertia() with pytest.warns(DuplicateWarning) as w: @@ -1432,7 +1647,7 @@ def test_moment_of_inertia_duplicates(self, universe): assert len(w) == 1 def test_radius_of_gyration_duplicates(self, universe): - ag = universe.select_atoms('segid 4AKE') + ag = universe.select_atoms("segid 4AKE") ag2 = ag + ag[0] ref = ag.radius_of_gyration() with pytest.warns(DuplicateWarning) as w: @@ -1490,12 +1705,15 @@ def test_charges_ndarray(self, ag): assert isinstance(ag.charges, np.ndarray) def test_charges(self, ag): - assert_almost_equal(ag.charges[1000:2000:200], - np.array([-0.09, 0.09, -0.47, 0.51, 0.09])) + assert_almost_equal( + ag.charges[1000:2000:200], + np.array([-0.09, 0.09, -0.47, 0.51, 0.09]), + ) def test_bad_add_AG(self, ag): def bad_add(): return ag + [1, 2, 3] + with pytest.raises(TypeError): bad_add() @@ -1522,7 +1740,7 @@ def test_set_resnum_single(self, universe): def test_set_resname_single(self, universe): ag = universe.atoms[:3] - new = 'abc' + new = "abc" ag.residues.resnames = new for at in ag: assert_equal(at.resname, new) @@ -1532,7 +1750,7 @@ def test_packintobox_badshape(self, universe): ag = universe.atoms[:10] box = np.zeros(9, dtype=np.float32).reshape(3, 3) with pytest.raises(ValueError): - ag.pack_into_box(box = box) + ag.pack_into_box(box=box) def test_packintobox_noshape(self, universe): ag = universe.atoms[:10] @@ -1550,41 +1768,49 @@ def test_packintobox(self, universe): u = universe ag = u.atoms[1000:2000:200] # Provide arbitrary box - box = np.array([5., 5., 5., 90., 90., 90.], dtype=np.float32) + box = np.array([5.0, 5.0, 5.0, 90.0, 90.0, 90.0], dtype=np.float32) # Expected folded coordinates - packed_coords = np.array([[3.94543672, 2.5939188, 2.73179913], - [3.21632767, 0.879035, 0.32085133], - [2.07735443, 0.99395466, 4.09301519], - [1.35541916, 2.0690732, 4.67488003], - [1.73236561, 4.90658951, 0.6880455]], - dtype=np.float32) + packed_coords = np.array( + [ + [3.94543672, 2.5939188, 2.73179913], + [3.21632767, 0.879035, 0.32085133], + [2.07735443, 0.99395466, 4.09301519], + [1.35541916, 2.0690732, 4.67488003], + [1.73236561, 4.90658951, 0.6880455], + ], + dtype=np.float32, + ) ag.pack_into_box(box=box) assert_almost_equal(ag.positions, packed_coords) # Check with duplicates: ag += ag ag.pack_into_box(box=box) - assert_almost_equal(ag.positions, - np.vstack((packed_coords, packed_coords))) + assert_almost_equal( + ag.positions, np.vstack((packed_coords, packed_coords)) + ) def test_residues(self, universe): u = universe - assert_equal(u.residues[100].atoms.ix, - u.select_atoms('resname ILE and resid 101').atoms.ix, - "Direct selection from residue group does not match " - "expected I101.") + assert_equal( + u.residues[100].atoms.ix, + u.select_atoms("resname ILE and resid 101").atoms.ix, + "Direct selection from residue group does not match " + "expected I101.", + ) def test_index_integer(self, universe): u = universe a = u.atoms[100] - assert isinstance(a, mda.core.groups.Atom), ("integer index did not " - "return Atom") + assert isinstance(a, mda.core.groups.Atom), ( + "integer index did not " "return Atom" + ) def test_index_slice(self, universe): u = universe a = u.atoms[100:200:10] - assert isinstance(a, mda.core.groups.AtomGroup), ("slice index did " - "not return " - "AtomGroup") + assert isinstance(a, mda.core.groups.AtomGroup), ( + "slice index did " "not return " "AtomGroup" + ) def test_index_slice_empty(self, universe): u = universe @@ -1594,20 +1820,20 @@ def test_index_advancedslice(self, universe): u = universe aslice = [0, 10, 20, -1, 10] ag = u.atoms[aslice] - assert isinstance(ag, mda.core.groups.AtomGroup), ("advanced slicing " - "does not produce " - "an AtomGroup") + assert isinstance(ag, mda.core.groups.AtomGroup), ( + "advanced slicing " "does not produce " "an AtomGroup" + ) assert_equal(ag[1], ag[-1], "advanced slicing does not preserve order") def test_2d_indexing_caught(self, universe): u = universe - index_2d = [[1, 2, 3], - [4, 5, 6]] + index_2d = [[1, 2, 3], [4, 5, 6]] with pytest.raises(IndexError): u.atoms[index_2d] - @pytest.mark.parametrize('sel', (np.array([True, False, True]), - [True, False, True])) + @pytest.mark.parametrize( + "sel", (np.array([True, False, True]), [True, False, True]) + ) def test_boolean_indexing_2(self, universe, sel): # index an array with a sequence of bools # issue #282 @@ -1629,62 +1855,90 @@ def test_dihedral_ValueError(self, universe): 4 atoms given""" nodih = universe.select_atoms("resid 3:10") with pytest.raises(ValueError): - getattr(nodih, 'dihedral') + getattr(nodih, "dihedral") nodih = universe.select_atoms("resid 3:5") with pytest.raises(ValueError): - getattr(nodih, 'dihedral') + getattr(nodih, "dihedral") def test_improper(self, universe): u = universe - peptbond = u.select_atoms("atom 4AKE 20 C", "atom 4AKE 21 CA", - "atom 4AKE 21 N", "atom 4AKE 21 HN") - assert_almost_equal(peptbond.improper.value(), 168.52952575683594, - self.dih_prec, - "Peptide bond improper dihedral for M21 " - "calculated wrongly.") + peptbond = u.select_atoms( + "atom 4AKE 20 C", + "atom 4AKE 21 CA", + "atom 4AKE 21 N", + "atom 4AKE 21 HN", + ) + assert_almost_equal( + peptbond.improper.value(), + 168.52952575683594, + self.dih_prec, + "Peptide bond improper dihedral for M21 " "calculated wrongly.", + ) def test_dihedral_equals_improper(self, universe): u = universe - peptbond = u.select_atoms("atom 4AKE 20 C", "atom 4AKE 21 CA", - "atom 4AKE 21 N", "atom 4AKE 21 HN") - assert_equal(peptbond.improper.value(), peptbond.dihedral.value(), - "improper() and proper dihedral() give different results") + peptbond = u.select_atoms( + "atom 4AKE 20 C", + "atom 4AKE 21 CA", + "atom 4AKE 21 N", + "atom 4AKE 21 HN", + ) + assert_equal( + peptbond.improper.value(), + peptbond.dihedral.value(), + "improper() and proper dihedral() give different results", + ) def test_bond(self, universe): - sel2 = universe.select_atoms('segid 4AKE and resid 98' - ).select_atoms("name OE1", "name OE2") - assert_almost_equal(sel2.bond.value(), 2.1210737228393555, 3, - "distance of Glu98 OE1--OE2 wrong") + sel2 = universe.select_atoms("segid 4AKE and resid 98").select_atoms( + "name OE1", "name OE2" + ) + assert_almost_equal( + sel2.bond.value(), + 2.1210737228393555, + 3, + "distance of Glu98 OE1--OE2 wrong", + ) def test_bond_pbc(self, universe): - sel2 = universe.select_atoms('segid 4AKE and resid 98' - ).select_atoms("name OE1", "name OE2") - assert_almost_equal(sel2.bond.value(pbc=True), 2.1210737228393555, 3, - "distance of Glu98 OE1--OE2 wrong") + sel2 = universe.select_atoms("segid 4AKE and resid 98").select_atoms( + "name OE1", "name OE2" + ) + assert_almost_equal( + sel2.bond.value(pbc=True), + 2.1210737228393555, + 3, + "distance of Glu98 OE1--OE2 wrong", + ) def test_bond_ValueError(self, universe): ag = universe.atoms[:4] with pytest.raises(ValueError): - getattr(ag, 'bond') + getattr(ag, "bond") def test_angle(self, universe): - sel3 = universe.select_atoms('segid 4AKE and resid 98').select_atoms( - 'name OE1', 'name CD', 'name OE2') - assert_almost_equal(sel3.angle.value(), 117.46187591552734, 3, - "angle of Glu98 OE1-CD-OE2 wrong") + sel3 = universe.select_atoms("segid 4AKE and resid 98").select_atoms( + "name OE1", "name CD", "name OE2" + ) + assert_almost_equal( + sel3.angle.value(), + 117.46187591552734, + 3, + "angle of Glu98 OE1-CD-OE2 wrong", + ) def test_angle_ValueError(self, universe): ag = universe.atoms[:2] with pytest.raises(ValueError): - getattr(ag, 'angle') + getattr(ag, "angle") def test_shape_parameter(self, universe): - s = universe.select_atoms('segid 4AKE').shape_parameter() + s = universe.select_atoms("segid 4AKE").shape_parameter() assert_almost_equal(s, 0.00240753939086033, 6) def test_shape_parameter_duplicates(self, universe): - ag = universe.select_atoms('segid 4AKE') + ag = universe.select_atoms("segid 4AKE") ag2 = ag + ag[0] ref = ag.shape_parameter() with pytest.warns(DuplicateWarning) as w: @@ -1692,11 +1946,11 @@ def test_shape_parameter_duplicates(self, universe): assert len(w) == 1 def test_asphericity(self, universe): - a = universe.select_atoms('segid 4AKE').asphericity() + a = universe.select_atoms("segid 4AKE").asphericity() assert_almost_equal(a, 0.020227504542775828, 6) def test_asphericity_duplicates(self, universe): - ag = universe.select_atoms('segid 4AKE') + ag = universe.select_atoms("segid 4AKE") ag2 = ag + ag[0] ref = ag.asphericity() with pytest.warns(DuplicateWarning) as w: @@ -1708,9 +1962,11 @@ def test_positions(self, universe): pos = ag.positions + 3.14 ag.positions = pos # should work - assert_almost_equal(ag.positions, pos, - err_msg="failed to update atoms 12:42 position " - "to new position") + assert_almost_equal( + ag.positions, + pos, + err_msg="failed to update atoms 12:42 position " "to new position", + ) rg = np.random.RandomState(121989) # create wrong size array @@ -1720,14 +1976,13 @@ def test_positions(self, universe): def test_set_names(self, universe): ag = universe.atoms[:2] - names = ['One', 'Two'] + names = ["One", "Two"] ag.names = names for a, b in zip(ag, names): assert_equal(a.name, b) def test_atom_order(self, universe): - assert_equal(universe.atoms.indices, - sorted(universe.atoms.indices)) + assert_equal(universe.atoms.indices, sorted(universe.atoms.indices)) class TestAtomGroupTimestep(object): @@ -1740,20 +1995,24 @@ def universe(self): return mda.Universe(TRZ_psf, TRZ) def test_partial_timestep(self, universe): - ag = universe.select_atoms('name Ca') + ag = universe.select_atoms("name Ca") idx = ag.indices assert len(ag.ts._pos) == len(ag) for ts in universe.trajectory[0:20:5]: - assert_almost_equal(ts.positions[idx], - ag.ts.positions, - self.prec, - err_msg="Partial timestep coordinates wrong") - assert_almost_equal(ts.velocities[idx], - ag.ts.velocities, - self.prec, - err_msg="Partial timestep coordinates wrong") + assert_almost_equal( + ts.positions[idx], + ag.ts.positions, + self.prec, + err_msg="Partial timestep coordinates wrong", + ) + assert_almost_equal( + ts.velocities[idx], + ag.ts.velocities, + self.prec, + err_msg="Partial timestep coordinates wrong", + ) class TestAtomGroupSort(object): @@ -1769,16 +2028,16 @@ def universe(self): residue_segindex=np.array([0, 0, 1]), trajectory=True, velocities=True, - forces=True + forces=True, ) attributes = ["id", "charge", "mass", "tempfactor"] - for i in (attributes): + for i in attributes: u.add_TopologyAttr(i, [6, 5, 4, 3, 2, 1, 0]) - u.add_TopologyAttr('resid', [2, 1, 0]) - u.add_TopologyAttr('segid', [1, 0]) - u.add_TopologyAttr('bonds', [(0, 1)]) + u.add_TopologyAttr("resid", [2, 1, 0]) + u.add_TopologyAttr("segid", [1, 0]) + u.add_TopologyAttr("bonds", [(0, 1)]) return u @@ -1789,13 +2048,13 @@ def ag(self, universe): return ag test_ids = [ - "ix", - "ids", - "resids", - "segids", - "charges", - "masses", - "tempfactors" + "ix", + "ids", + "resids", + "segids", + "charges", + "masses", + "tempfactors", ] test_data = [ @@ -1814,8 +2073,9 @@ def test_sort(self, ag, inputs, expected): assert np.array_equal(expected, agsort.ix) def test_sort_bonds(self, ag): - with pytest.raises(ValueError, match=r"The array returned by the " - "attribute"): + with pytest.raises( + ValueError, match=r"The array returned by the " "attribute" + ): ag.sort("bonds") def test_sort_positions_2D(self, ag): @@ -1823,8 +2083,11 @@ def test_sort_positions_2D(self, ag): ag.sort("positions", keyfunc=lambda x: x) def test_sort_position_no_keyfunc(self, ag): - with pytest.raises(NameError, match=r"The .* attribute returns a " - "multidimensional array. In order to sort it, "): + with pytest.raises( + NameError, + match=r"The .* attribute returns a " + "multidimensional array. In order to sort it, ", + ): ag.sort("positions") def test_sort_position(self, ag): diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 51b395f94cb..2a8f30cafaf 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -20,44 +20,49 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # +import itertools import os import textwrap from io import StringIO -import itertools -import numpy as np -from numpy.lib import NumpyVersion -from numpy.testing import( - assert_equal, -) import MDAnalysis import MDAnalysis as mda import MDAnalysis.core.selection -from MDAnalysis.lib.distances import distance_array -from MDAnalysis.core.selection import Parser +import numpy as np +import pytest from MDAnalysis import SelectionError, SelectionWarning - +from MDAnalysis.core.selection import Parser +from MDAnalysis.lib.distances import distance_array from MDAnalysis.tests.datafiles import ( - PSF, DCD, - PRMpbc, TRJpbc_bz2, - PSF_NAMD, PDB_NAMD, - GRO, RNA_PSF, NUCLsel, TPR, XTC, - TRZ_psf, TRZ, - PDB_icodes, + DCD, + GRO, PDB_HOLE, - PDB_helix, - PDB_elements, + PDB_NAMD, + PSF, + PSF_NAMD, + RNA_PSF, + TPR, + TRZ, + XTC, + NUCLsel, PDB_charges, - waterPSF, + PDB_elements, PDB_full, + PDB_helix, + PDB_icodes, + PRMpbc, + TRJpbc_bz2, + TRZ_psf, + waterPSF, ) -from MDAnalysisTests import make_Universe +from numpy.lib import NumpyVersion +from numpy.testing import assert_equal -import pytest +from MDAnalysisTests import make_Universe class TestSelectionsCHARMM(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): """Set up the standard AdK system in implicit solvent. Geometry here is orthogonal @@ -69,22 +74,28 @@ def universe_copy(self, universe): return MDAnalysis.Universe(PSF, DCD) def test_segid(self, universe): - sel = universe.select_atoms('segid 4AKE') + sel = universe.select_atoms("segid 4AKE") assert_equal(sel.n_atoms, 3341, "failed to select segment 4AKE") - assert_equal(sorted(sel.indices), - sorted(universe.select_atoms('segid 4AKE').indices), - "selected segment 4AKE is not the same as auto-generated segment s4AKE") + assert_equal( + sorted(sel.indices), + sorted(universe.select_atoms("segid 4AKE").indices), + "selected segment 4AKE is not the same as auto-generated segment s4AKE", + ) def test_protein(self, universe): - sel = universe.select_atoms('protein') + sel = universe.select_atoms("protein") assert_equal(sel.n_atoms, 3341, "failed to select protein") - assert_equal(sorted(sel.indices), - sorted(universe.select_atoms('segid 4AKE').indices), - "selected protein is not the same as auto-generated protein segment s4AKE") + assert_equal( + sorted(sel.indices), + sorted(universe.select_atoms("segid 4AKE").indices), + "selected protein is not the same as auto-generated protein segment s4AKE", + ) - @pytest.mark.parametrize('resname', sorted(MDAnalysis.core.selection.ProteinSelection.prot_res)) + @pytest.mark.parametrize( + "resname", sorted(MDAnalysis.core.selection.ProteinSelection.prot_res) + ) def test_protein_resnames(self, resname): - u = make_Universe(('resnames',)) + u = make_Universe(("resnames",)) # set half the residues' names to the resname we're testing myprot = u.residues[::2] # Windows note: the parametrized test input string objects @@ -92,78 +103,88 @@ def test_protein_resnames(self, resname): # proper is needed for unit test on Windows myprot.resnames = str(resname) # select protein - sel = u.select_atoms('protein') + sel = u.select_atoms("protein") # check that contents (atom indices) are identical afterwards assert_equal(myprot.atoms.ix, sel.ix) def test_backbone(self, universe): - sel = universe.select_atoms('backbone') + sel = universe.select_atoms("backbone") assert_equal(sel.n_atoms, 855) def test_resid_single(self, universe): - sel = universe.select_atoms('resid 100') + sel = universe.select_atoms("resid 100") assert_equal(sel.n_atoms, 7) - assert_equal(sel.residues.resnames, ['GLY']) + assert_equal(sel.residues.resnames, ["GLY"]) def test_resid_range(self, universe): - sel = universe.select_atoms('resid 100:105') + sel = universe.select_atoms("resid 100:105") assert_equal(sel.n_atoms, 89) - assert_equal(sel.residues.resnames, - ['GLY', 'ILE', 'ASN', 'VAL', 'ASP', 'TYR']) + assert_equal( + sel.residues.resnames, ["GLY", "ILE", "ASN", "VAL", "ASP", "TYR"] + ) def test_selgroup(self, universe): - sel = universe.select_atoms('not resid 100') - sel2 = universe.select_atoms('not group notr100', notr100=sel) + sel = universe.select_atoms("not resid 100") + sel2 = universe.select_atoms("not group notr100", notr100=sel) assert_equal(sel2.n_atoms, 7) - assert_equal(sel2.residues.resnames, ['GLY']) + assert_equal(sel2.residues.resnames, ["GLY"]) def test_fullselgroup(self, universe): - sel1 = universe.select_atoms('resid 101') - sel2 = universe.select_atoms('resid 100') - sel3 = sel1.select_atoms('global group r100', r100=sel2) + sel1 = universe.select_atoms("resid 101") + sel2 = universe.select_atoms("resid 100") + sel3 = sel1.select_atoms("global group r100", r100=sel2) assert_equal(sel2.n_atoms, 7) - assert_equal(sel2.residues.resnames, ['GLY']) + assert_equal(sel2.residues.resnames, ["GLY"]) # resnum selections are boring here because we haven't really a mechanism yet # to assign the canonical PDB resnums def test_resnum_single(self, universe): - sel = universe.select_atoms('resnum 100') + sel = universe.select_atoms("resnum 100") assert_equal(sel.n_atoms, 7) assert_equal(sel.residues.resids, [100]) - assert_equal(sel.residues.resnames, ['GLY']) + assert_equal(sel.residues.resnames, ["GLY"]) def test_resnum_range(self, universe): - sel = universe.select_atoms('resnum 100:105') + sel = universe.select_atoms("resnum 100:105") assert_equal(sel.n_atoms, 89) assert_equal(sel.residues.resids, range(100, 106)) - assert_equal(sel.residues.resnames, - ['GLY', 'ILE', 'ASN', 'VAL', 'ASP', 'TYR']) + assert_equal( + sel.residues.resnames, ["GLY", "ILE", "ASN", "VAL", "ASP", "TYR"] + ) def test_resname(self, universe): - sel = universe.select_atoms('resname LEU') - assert_equal(sel.n_atoms, 304, - "Failed to find all 'resname LEU' atoms.") - assert_equal(sel.n_residues, 16, - "Failed to find all 'resname LEU' residues.") - assert_equal(sorted(sel.indices), - sorted(universe.select_atoms('segid 4AKE and resname LEU').indices), - "selected 'resname LEU' atoms are not the same as auto-generated s4AKE.LEU") + sel = universe.select_atoms("resname LEU") + assert_equal( + sel.n_atoms, 304, "Failed to find all 'resname LEU' atoms." + ) + assert_equal( + sel.n_residues, 16, "Failed to find all 'resname LEU' residues." + ) + assert_equal( + sorted(sel.indices), + sorted( + universe.select_atoms("segid 4AKE and resname LEU").indices + ), + "selected 'resname LEU' atoms are not the same as auto-generated s4AKE.LEU", + ) def test_name(self, universe): - sel = universe.select_atoms('name CA') + sel = universe.select_atoms("name CA") assert_equal(sel.n_atoms, 214) def test_atom(self, universe): - sel = universe.select_atoms('atom 4AKE 100 CA') + sel = universe.select_atoms("atom 4AKE 100 CA") assert_equal(len(sel), 1) - assert_equal(sel.resnames, ['GLY']) + assert_equal(sel.resnames, ["GLY"]) assert_equal( sel.positions, - np.array([[20.38685226, -3.44224262, -5.92158318]], - dtype=np.float32)) + np.array( + [[20.38685226, -3.44224262, -5.92158318]], dtype=np.float32 + ), + ) def test_atom_empty(self, universe): - sel = universe.select_atoms('atom 4AKE 100 XX') # Does not exist + sel = universe.select_atoms("atom 4AKE 100 XX") # Does not exist assert_equal(len(sel), 0) def test_type(self, universe): @@ -171,124 +192,126 @@ def test_type(self, universe): assert_equal(len(sel), 253) def test_and(self, universe): - sel = universe.select_atoms('resname GLY and resid 100') + sel = universe.select_atoms("resname GLY and resid 100") assert_equal(len(sel), 7) def test_or(self, universe): - sel = universe.select_atoms('resname LYS or resname ARG') + sel = universe.select_atoms("resname LYS or resname ARG") assert_equal(sel.n_residues, 31) def test_not(self, universe): - sel = universe.select_atoms('not backbone') + sel = universe.select_atoms("not backbone") assert_equal(len(sel), 2486) - @pytest.mark.parametrize('selstr', [ - 'around 4.0 bynum 1943', - 'around 4.0 index 1942' - ]) + @pytest.mark.parametrize( + "selstr", ["around 4.0 bynum 1943", "around 4.0 index 1942"] + ) def test_around(self, universe, selstr): sel = universe.select_atoms(selstr) assert_equal(len(sel), 32) - @pytest.mark.parametrize('selstr', [ - 'sphlayer 4.0 6.0 bynum 1281', - 'sphlayer 4.0 6.0 index 1280' - ]) + @pytest.mark.parametrize( + "selstr", + ["sphlayer 4.0 6.0 bynum 1281", "sphlayer 4.0 6.0 index 1280"], + ) def test_sphlayer(self, universe, selstr): sel = universe.select_atoms(selstr) assert_equal(len(sel), 66) - @pytest.mark.parametrize('selstr', [ - 'isolayer 4.0 6.0 bynum 1281', - 'isolayer 4.0 6.0 index 1280' - ]) + @pytest.mark.parametrize( + "selstr", + ["isolayer 4.0 6.0 bynum 1281", "isolayer 4.0 6.0 index 1280"], + ) def test_isolayer(self, universe, selstr): sel = universe.select_atoms(selstr) assert_equal(len(sel), 66) - @pytest.mark.parametrize('selstr', [ - 'sphzone 6.0 bynum 1281', - 'sphzone 6.0 index 1280' - ]) + @pytest.mark.parametrize( + "selstr", ["sphzone 6.0 bynum 1281", "sphzone 6.0 index 1280"] + ) def test_sphzone(self, universe, selstr): sel = universe.select_atoms(selstr) assert_equal(len(sel), 86) - @pytest.mark.parametrize('selstr', [ - 'cylayer 4.0 6.0 10 -10 bynum 1281', - 'cylayer 4.0 6.0 10 -10 index 1280' - ]) + @pytest.mark.parametrize( + "selstr", + [ + "cylayer 4.0 6.0 10 -10 bynum 1281", + "cylayer 4.0 6.0 10 -10 index 1280", + ], + ) def test_cylayer(self, universe, selstr): sel = universe.select_atoms(selstr) assert_equal(len(sel), 88) def test_empty_cylayer(self, universe): - empty = universe.select_atoms('cylayer 4.0 6.0 10 -10 name NOT_A_NAME') + empty = universe.select_atoms("cylayer 4.0 6.0 10 -10 name NOT_A_NAME") assert_equal(len(empty), 0) - @pytest.mark.parametrize('selstr', [ - 'cyzone 6.0 10 -10 bynum 1281', - 'cyzone 6.0 10 -10 index 1280' - ]) + @pytest.mark.parametrize( + "selstr", + ["cyzone 6.0 10 -10 bynum 1281", "cyzone 6.0 10 -10 index 1280"], + ) def test_cyzone(self, universe, selstr): sel = universe.select_atoms(selstr) assert_equal(len(sel), 166) def test_empty_cyzone(self, universe): - empty = universe.select_atoms('cyzone 6.0 10 -10 name NOT_A_NAME') + empty = universe.select_atoms("cyzone 6.0 10 -10 name NOT_A_NAME") assert_equal(len(empty), 0) def test_point(self, universe): - ag = universe.select_atoms('point 5.0 5.0 5.0 3.5') + ag = universe.select_atoms("point 5.0 5.0 5.0 3.5") - d = distance_array(np.array([[5.0, 5.0, 5.0]], dtype=np.float32), - universe.atoms.positions, - box=universe.dimensions) + d = distance_array( + np.array([[5.0, 5.0, 5.0]], dtype=np.float32), + universe.atoms.positions, + box=universe.dimensions, + ) idx = np.where(d < 3.5)[1] assert_equal(set(ag.indices), set(idx)) def test_prop(self, universe): - sel = universe.select_atoms('prop y <= 16') - sel2 = universe.select_atoms('prop abs z < 8') + sel = universe.select_atoms("prop y <= 16") + sel2 = universe.select_atoms("prop abs z < 8") assert_equal(len(sel), 3194) assert_equal(len(sel2), 2001) def test_bynum(self, universe): "Tests the bynum selection, also from AtomGroup instances (Issue 275)" - sel = universe.select_atoms('bynum 5') + sel = universe.select_atoms("bynum 5") assert_equal(sel[0].index, 4) - sel = universe.select_atoms('bynum 1:10') + sel = universe.select_atoms("bynum 1:10") assert_equal(len(sel), 10) assert_equal(sel[0].index, 0) assert_equal(sel[-1].index, 9) - subsel = sel.select_atoms('bynum 5') + subsel = sel.select_atoms("bynum 5") assert_equal(subsel[0].index, 4) - subsel = sel.select_atoms('bynum 2:5') + subsel = sel.select_atoms("bynum 2:5") assert_equal(len(subsel), 4) assert_equal(subsel[0].index, 1) assert_equal(subsel[-1].index, 4) def test_index(self, universe): "Tests the index selection, also from AtomGroup instances (Issue 275)" - sel = universe.select_atoms('index 4') + sel = universe.select_atoms("index 4") assert_equal(sel[0].index, 4) - sel = universe.select_atoms('index 0:9') + sel = universe.select_atoms("index 0:9") assert_equal(len(sel), 10) assert_equal(sel[0].index, 0) assert_equal(sel[-1].index, 9) - subsel = sel.select_atoms('index 4') + subsel = sel.select_atoms("index 4") assert_equal(subsel[0].index, 4) - subsel = sel.select_atoms('index 1:4') + subsel = sel.select_atoms("index 1:4") assert_equal(len(subsel), 4) assert_equal(subsel[0].index, 1) assert_equal(subsel[-1].index, 4) - @pytest.mark.parametrize('selstr,expected', ( - ['byres bynum 0:5', 19], - ['byres index 0:19', 43] - )) + @pytest.mark.parametrize( + "selstr,expected", (["byres bynum 0:5", 19], ["byres index 0:19", 43]) + ) def test_byres(self, universe, selstr, expected): sel = universe.select_atoms(selstr) assert_equal(len(sel), expected) @@ -296,23 +319,35 @@ def test_byres(self, universe, selstr, expected): def test_same_resname(self, universe): """Test the 'same ... as' construct (Issue 217)""" sel = universe.select_atoms("same resname as resid 10 or resid 11") - assert_equal(len(sel), 331, - ("Found a wrong number of atoms with same resname as " - "resids 10 or 11")) - target_resids = np.array([7, 8, 10, 11, 12, 14, 17, 25, 32, 37, 38, - 42, 46, 49, 55, 56, 66, 73, 80, 85, 93, 95, - 99, 100, 122, 127, 130, 144, 150, 176, 180, - 186, 188, 189, 194, 198, 203, 207, 214]) - assert_equal(sel.residues.resids, target_resids, - ("Found wrong residues with same resname as " - "resids 10 or 11")) + assert_equal( + len(sel), + 331, + ( + "Found a wrong number of atoms with same resname as " + "resids 10 or 11" + ), + ) + # fmt: off + target_resids = np.array( + [ + 7, 8, 10, 11, 12, 14, 17, 25, 32, 37, 38, 42, 46, 49, 55, 56, + 66, 73, 80, 85, 93, 95, 99, 100, 122, 127, 130, 144, 150, 176, + 180, 186, 188, 189, 194, 198, 203, 207, 214, + ] + ) + # fmt: on + assert_equal( + sel.residues.resids, + target_resids, + ("Found wrong residues with same resname as " "resids 10 or 11"), + ) def test_same_segment(self, universe_copy): """Test the 'same ... as' construct (Issue 217)""" - SNew_A = universe_copy.add_Segment(segid='A') - SNew_B = universe_copy.add_Segment(segid='B') - SNew_C = universe_copy.add_Segment(segid='C') + SNew_A = universe_copy.add_Segment(segid="A") + SNew_B = universe_copy.add_Segment(segid="B") + SNew_C = universe_copy.add_Segment(segid="C") universe_copy.residues[:100].segments = SNew_A universe_copy.residues[100:150].segments = SNew_B @@ -320,103 +355,128 @@ def test_same_segment(self, universe_copy): target_resids = np.arange(100) + 1 sel = universe_copy.select_atoms("same segment as resid 10") - assert_equal(len(sel), 1520, - "Found a wrong number of atoms in the same segment of resid 10") - assert_equal(sel.residues.resids, - target_resids, - "Found wrong residues in the same segment of resid 10") + assert_equal( + len(sel), + 1520, + "Found a wrong number of atoms in the same segment of resid 10", + ) + assert_equal( + sel.residues.resids, + target_resids, + "Found wrong residues in the same segment of resid 10", + ) target_resids = np.arange(100, 150) + 1 sel = universe_copy.select_atoms("same segment as resid 110") - assert_equal(len(sel), 797, - "Found a wrong number of atoms in the same segment of resid 110") - assert_equal(sel.residues.resids, target_resids, - "Found wrong residues in the same segment of resid 110") + assert_equal( + len(sel), + 797, + "Found a wrong number of atoms in the same segment of resid 110", + ) + assert_equal( + sel.residues.resids, + target_resids, + "Found wrong residues in the same segment of resid 110", + ) target_resids = np.arange(150, universe_copy.atoms.n_residues) + 1 sel = universe_copy.select_atoms("same segment as resid 160") - assert_equal(len(sel), 1024, - "Found a wrong number of atoms in the same segment of resid 160") - assert_equal(sel.residues.resids, target_resids, - "Found wrong residues in the same segment of resid 160") + assert_equal( + len(sel), + 1024, + "Found a wrong number of atoms in the same segment of resid 160", + ) + assert_equal( + sel.residues.resids, + target_resids, + "Found wrong residues in the same segment of resid 160", + ) def test_empty_same(self, universe): - ag = universe.select_atoms('resname MET') + ag = universe.select_atoms("resname MET") # No GLY, so 'as resname GLY' is empty - ag2 = ag.select_atoms('same mass as resname GLY') + ag2 = ag.select_atoms("same mass as resname GLY") assert len(ag2) == 0 def test_empty_selection(self, universe): """Test that empty selection can be processed (see Issue 12)""" # no Trp in AdK - assert_equal(len(universe.select_atoms('resname TRP')), 0) + assert_equal(len(universe.select_atoms("resname TRP")), 0) def test_parenthesized_expression(self, universe): - sel = universe.select_atoms( - '( name CA or name CB ) and resname LEU') + sel = universe.select_atoms("( name CA or name CB ) and resname LEU") assert_equal(len(sel), 32) def test_no_space_around_parentheses(self, universe): """Test that no space is needed around parentheses (Issue 43).""" # note: will currently be ERROR because it throws a ParseError - sel = universe.select_atoms('(name CA or name CB) and resname LEU') + sel = universe.select_atoms("(name CA or name CB) and resname LEU") assert_equal(len(sel), 32) # TODO: # test for checking ordering and multiple comma-separated selections def test_concatenated_selection(self, universe): - E151 = universe.select_atoms('segid 4AKE').select_atoms('resid 151') + E151 = universe.select_atoms("segid 4AKE").select_atoms("resid 151") # note that this is not quite phi... HN should be C of prec. residue - phi151 = E151.atoms.select_atoms('name HN', 'name N', 'name CA', - 'name CB') + phi151 = E151.atoms.select_atoms( + "name HN", "name N", "name CA", "name CB" + ) assert_equal(len(phi151), 4) - assert_equal(phi151[0].name, 'HN', - "wrong ordering in selection, should be HN-N-CA-CB") + assert_equal( + phi151[0].name, + "HN", + "wrong ordering in selection, should be HN-N-CA-CB", + ) def test_global(self, universe): """Test the `global` modifier keyword (Issue 268)""" ag = universe.select_atoms("resname LYS and name NZ") # Lys amines within 4 angstrom of the backbone. ag1 = universe.select_atoms( - "resname LYS and name NZ and around 4 backbone") + "resname LYS and name NZ and around 4 backbone" + ) ag2 = ag.select_atoms("around 4 global backbone") assert_equal(ag2.indices, ag1.indices) - @pytest.mark.parametrize('selstring, wildstring', [ - ('resname TYR THR', 'resname T*R'), - ('resname ASN GLN', 'resname *N'), - ('resname ASN ASP', 'resname AS*'), - ('resname TYR THR', 'resname T?R'), - ('resname ASN ASP HSD', 'resname *S?'), - ('resname LEU LYS', 'resname L**'), - ('resname MET', 'resname *M*'), - ('resname GLN GLY', 'resname GL[NY]'), - ('resname GLU', 'resname GL[!NY]'), - ]) + @pytest.mark.parametrize( + "selstring, wildstring", + [ + ("resname TYR THR", "resname T*R"), + ("resname ASN GLN", "resname *N"), + ("resname ASN ASP", "resname AS*"), + ("resname TYR THR", "resname T?R"), + ("resname ASN ASP HSD", "resname *S?"), + ("resname LEU LYS", "resname L**"), + ("resname MET", "resname *M*"), + ("resname GLN GLY", "resname GL[NY]"), + ("resname GLU", "resname GL[!NY]"), + ], + ) def test_wildcard_selection(self, universe, selstring, wildstring): ag = universe.select_atoms(selstring) ag_wild = universe.select_atoms(wildstring) assert ag == ag_wild + class TestSelectionsAMBER(object): @pytest.fixture() def universe(self): return MDAnalysis.Universe(PRMpbc, TRJpbc_bz2) def test_protein(self, universe): - sel = universe.select_atoms('protein') + sel = universe.select_atoms("protein") assert_equal(sel.n_atoms, 22, "failed to select protein") def test_backbone(self, universe): - sel = universe.select_atoms('backbone') + sel = universe.select_atoms("backbone") assert_equal(sel.n_atoms, 7) def test_type(self, universe): - sel = universe.select_atoms('type HC') + sel = universe.select_atoms("type HC") assert_equal(len(sel), 6) - assert_equal(sel.names, ['HH31', 'HH32', 'HH33', 'HB1', 'HB2', 'HB3']) + assert_equal(sel.names, ["HH31", "HH32", "HH33", "HB1", "HB2", "HB3"]) class TestSelectionsNAMD(object): @@ -426,23 +486,24 @@ def universe(self): def test_protein(self, universe): # must include non-standard residues - sel = universe.select_atoms( - 'protein or resname HAO or resname ORT') - assert_equal(sel.n_atoms, universe.atoms.n_atoms, - "failed to select peptide") - assert_equal(sel.n_residues, 6, - "failed to select all peptide residues") + sel = universe.select_atoms("protein or resname HAO or resname ORT") + assert_equal( + sel.n_atoms, universe.atoms.n_atoms, "failed to select peptide" + ) + assert_equal( + sel.n_residues, 6, "failed to select all peptide residues" + ) def test_resid_single(self, universe): - sel = universe.select_atoms('resid 12') + sel = universe.select_atoms("resid 12") assert_equal(sel.n_atoms, 26) - assert_equal(sel.residues.resnames, ['HAO']) + assert_equal(sel.residues.resnames, ["HAO"]) def test_type(self, universe): - sel = universe.select_atoms('type H') + sel = universe.select_atoms("type H") assert_equal(len(sel), 5) # note 4th HH - assert_equal(sel.names, ['HN', 'HN', 'HN', 'HH', 'HN']) + assert_equal(sel.names, ["HN", "HN", "HN", "HH", "HN"]) class TestSelectionsGRO(object): @@ -451,76 +512,84 @@ def universe(self): return MDAnalysis.Universe(GRO) def test_protein(self, universe): - sel = universe.select_atoms('protein') + sel = universe.select_atoms("protein") assert_equal(sel.n_atoms, 3341, "failed to select protein") def test_backbone(self, universe): - sel = universe.select_atoms('backbone') + sel = universe.select_atoms("backbone") assert_equal(sel.n_atoms, 855) def test_resid_single(self, universe): - sel = universe.select_atoms('resid 100') + sel = universe.select_atoms("resid 100") assert_equal(sel.n_atoms, 7) - assert_equal(sel.residues.resnames, ['GLY']) - - @pytest.mark.parametrize('selstr', [ - 'same x as bynum 1 or bynum 10', - 'same x as bynum 1 10', - 'same x as index 0 or index 9', - 'same x as index 0 9' - ]) + assert_equal(sel.residues.resnames, ["GLY"]) + + @pytest.mark.parametrize( + "selstr", + [ + "same x as bynum 1 or bynum 10", + "same x as bynum 1 10", + "same x as index 0 or index 9", + "same x as index 0 9", + ], + ) def test_same_coordinate(self, universe, selstr): """Test the 'same ... as' construct (Issue 217)""" sel = universe.select_atoms(selstr) - errmsg = ("Found a wrong number of atoms with same " - "x as ids 1 or 10") - assert_equal(len(sel), 12, errmsg) - target_ids = np.array([0, 8, 9, 224, 643, 3515, 11210, 14121, - 18430, 25418, 35811, 43618]) - assert_equal(sel.indices, target_ids, - "Found wrong atoms with same x as ids 1 or 10") - - @pytest.mark.parametrize('selstr', [ - 'cylayer 10 20 20 -20 bynum 3554', - 'cylayer 10 20 20 -20 index 3553' - ]) + errmsg = "Found a wrong number of atoms with same " "x as ids 1 or 10" + assert_equal(len(sel), 12, errmsg) + target_ids = np.array( + [0, 8, 9, 224, 643, 3515, 11210, 14121, 18430, 25418, 35811, 43618] + ) + assert_equal( + sel.indices, + target_ids, + "Found wrong atoms with same x as ids 1 or 10", + ) + + @pytest.mark.parametrize( + "selstr", + ["cylayer 10 20 20 -20 bynum 3554", "cylayer 10 20 20 -20 index 3553"], + ) def test_cylayer(self, universe, selstr): """Cylinder layer selections with tricilinic periodicity (Issue 274)""" - atgp = universe.select_atoms('name OW') + atgp = universe.select_atoms("name OW") sel = atgp.select_atoms(selstr) assert_equal(len(sel), 1155) - @pytest.mark.parametrize('selstr', [ - 'cyzone 20 20 -20 bynum 3554', - 'cyzone 20 20 -20 index 3553' - ]) + @pytest.mark.parametrize( + "selstr", + ["cyzone 20 20 -20 bynum 3554", "cyzone 20 20 -20 index 3553"], + ) def test_cyzone(self, universe, selstr): """Cylinder zone selections with tricilinic periodicity (Issue 274)""" - atgp = universe.select_atoms('name OW') + atgp = universe.select_atoms("name OW") sel = atgp.select_atoms(selstr) assert_equal(len(sel), 1556) class TestSelectionsTPR(object): @staticmethod - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(): return MDAnalysis.Universe(TPR, XTC, tpr_resid_from_one=False) - @pytest.mark.parametrize('selstr', [ - 'same fragment as bynum 1', - 'same fragment as index 0' - ]) + @pytest.mark.parametrize( + "selstr", ["same fragment as bynum 1", "same fragment as index 0"] + ) def test_same_fragment(self, universe, selstr): """Test the 'same ... as' construct (Issue 217)""" # This test comes here because it's a system with solvent, # and thus multiple fragments. sel = universe.select_atoms(selstr) - errmsg = ("Found a wrong number of atoms " - "on the same fragment as id 1") + errmsg = ( + "Found a wrong number of atoms " "on the same fragment as id 1" + ) assert_equal(len(sel), 3341, errmsg) - errmsg = ("Found a differ set of atoms when using the 'same " - "fragment as' construct vs. the .fragment property") + errmsg = ( + "Found a differ set of atoms when using the 'same " + "fragment as' construct vs. the .fragment property" + ) assert_equal(sel.indices, universe.atoms[0].fragment.indices, errmsg) def test_moltype(self, universe): @@ -529,11 +598,27 @@ def test_moltype(self, universe): assert_equal(sel.ids, ref) @pytest.mark.parametrize( - 'selection_string,reference', - (('molnum 1', [3341, 3342, 3343, 3344]), - ('molnum 2:4', [3345, 3346, 3347, 3348, 3349, 3350, - 3351, 3352, 3353, 3354, 3355, 3356]), - ) + "selection_string,reference", + ( + ("molnum 1", [3341, 3342, 3343, 3344]), + ( + "molnum 2:4", + [ + 3345, + 3346, + 3347, + 3348, + 3349, + 3350, + 3351, + 3352, + 3353, + 3354, + 3355, + 3356, + ], + ), + ), ) def test_molnum(self, universe, selection_string, reference): sel = universe.select_atoms(selection_string) @@ -550,8 +635,9 @@ def setup_class(self): @pytest.fixture def u(self): smi = "Cc1cNcc1" - u = MDAnalysis.Universe.from_smiles(smi, addHs=False, - generate_coordinates=False) + u = MDAnalysis.Universe.from_smiles( + smi, addHs=False, generate_coordinates=False + ) return u @pytest.fixture @@ -559,29 +645,35 @@ def u2(self): u = MDAnalysis.Universe.from_smiles("Nc1cc(C[C@H]([O-])C=O)c[nH]1") return u - @pytest.mark.parametrize("sel_str, n_atoms", [ - ("aromatic", 5), - ("not aromatic", 1), - ("type N and aromatic", 1), - ("type C and aromatic", 4), - ]) + @pytest.mark.parametrize( + "sel_str, n_atoms", + [ + ("aromatic", 5), + ("not aromatic", 1), + ("type N and aromatic", 1), + ("type C and aromatic", 4), + ], + ) def test_aromatic_selection(self, u, sel_str, n_atoms): sel = u.select_atoms(sel_str) assert sel.n_atoms == n_atoms - @pytest.mark.parametrize("sel_str, indices", [ - ("smarts n", [10]), - ("smarts [#7]", [0, 10]), - ("smarts a", [1, 2, 3, 9, 10]), - ("smarts c", [1, 2, 3, 9]), - ("smarts [*-]", [6]), - ("smarts [$([!#1]);$([!R][R])]", [0, 4]), - ("smarts [$([C@H](-[CH2])(-[O-])-C=O)]", [5]), - ("smarts [$([C@@H](-[CH2])(-[O-])-C=O)]", []), - ("smarts a and type C", [1, 2, 3, 9]), - ("(smarts a) and (type C)", [1, 2, 3, 9]), - ("smarts a and type N", [10]), - ]) + @pytest.mark.parametrize( + "sel_str, indices", + [ + ("smarts n", [10]), + ("smarts [#7]", [0, 10]), + ("smarts a", [1, 2, 3, 9, 10]), + ("smarts c", [1, 2, 3, 9]), + ("smarts [*-]", [6]), + ("smarts [$([!#1]);$([!R][R])]", [0, 4]), + ("smarts [$([C@H](-[CH2])(-[O-])-C=O)]", [5]), + ("smarts [$([C@@H](-[CH2])(-[O-])-C=O)]", []), + ("smarts a and type C", [1, 2, 3, 9]), + ("(smarts a) and (type C)", [1, 2, 3, 9]), + ("smarts a and type N", [10]), + ], + ) def test_smarts_selection(self, u2, sel_str, indices): sel = u2.select_atoms(sel_str) assert_equal(sel.indices, indices) @@ -599,7 +691,8 @@ def test_passing_max_matches_to_converter(self, u2): with pytest.warns(UserWarning, match="Your smarts-based") as wsmg: sel = u2.select_atoms("smarts C", smarts_kwargs=dict(maxMatches=2)) sel2 = u2.select_atoms( - "smarts C", smarts_kwargs=dict(maxMatches=1000)) + "smarts C", smarts_kwargs=dict(maxMatches=1000) + ) assert sel.n_atoms == 2 assert sel2.n_atoms == 3 @@ -610,27 +703,28 @@ def test_passing_use_chirality_to_converter(self): u = mda.Universe.from_smiles("CC[C@H](C)O") sel3 = u.select_atoms("byres smarts CC[C@@H](C)O") assert sel3.n_atoms == 0 - sel4 = u.select_atoms("byres smarts CC[C@@H](C)O", smarts_kwargs={"useChirality": False}) + sel4 = u.select_atoms( + "byres smarts CC[C@@H](C)O", smarts_kwargs={"useChirality": False} + ) assert sel4.n_atoms == 15 class TestSelectionsNucleicAcids(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return MDAnalysis.Universe(RNA_PSF) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe2(self): return MDAnalysis.Universe(NUCLsel) - def test_nucleic(self, universe): rna = universe.select_atoms("nucleic") assert_equal(rna.n_atoms, 739) assert_equal(rna.n_residues, 23) def test_nucleic_all(self, universe2): - sel = universe2.select_atoms('nucleic') + sel = universe2.select_atoms("nucleic") assert len(sel) == 34 def test_nucleicbackbone(self, universe): @@ -653,15 +747,15 @@ def test_nucleicsugar(self, universe): class TestSelectionsWater(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe(self): return MDAnalysis.Universe(GRO) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe2(self): return MDAnalysis.Universe(waterPSF) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def universe3(self): return MDAnalysis.Universe(PDB_full) @@ -673,7 +767,7 @@ def test_water_gro(self, universe): def test_water_tip3(self, universe2): # Test TIP3 water with 3 atoms - water_tip3 = universe2.select_atoms('water') + water_tip3 = universe2.select_atoms("water") assert_equal(water_tip3.n_atoms, 15) assert_equal(water_tip3.n_residues, 5) @@ -695,9 +789,10 @@ class BaseDistanceSelection(object): Cylindrical methods don't use KDTree """ - @pytest.mark.parametrize('periodic', (True, False)) + + @pytest.mark.parametrize("periodic", (True, False)) def test_around(self, u, periodic): - sel = Parser.parse('around 5.0 resid 1', u.atoms) + sel = Parser.parse("around 5.0 resid 1", u.atoms) if periodic: sel.periodic = True else: @@ -705,21 +800,20 @@ def test_around(self, u, periodic): result = sel.apply(u.atoms) - r1 = u.select_atoms('resid 1') + r1 = u.select_atoms("resid 1") cog = r1.center_of_geometry().reshape(1, 3) box = u.dimensions if periodic else None - d = distance_array(u.atoms.positions, r1.positions, - box=box) + d = distance_array(u.atoms.positions, r1.positions, box=box) ref = set(np.where(d < 5.0)[0]) # Around doesn't include atoms from the reference group ref.difference_update(set(r1.indices)) assert ref == set(result.indices) - @pytest.mark.parametrize('periodic', (True, False)) + @pytest.mark.parametrize("periodic", (True, False)) def test_spherical_layer(self, u, periodic): - sel = Parser.parse('sphlayer 2.4 6.0 resid 1', u.atoms) + sel = Parser.parse("sphlayer 2.4 6.0 resid 1", u.atoms) if periodic: sel.periodic = True else: @@ -727,7 +821,7 @@ def test_spherical_layer(self, u, periodic): result = sel.apply(u.atoms) - r1 = u.select_atoms('resid 1') + r1 = u.select_atoms("resid 1") box = u.dimensions if periodic else None cog = r1.center_of_geometry().reshape(1, 3) d = distance_array(u.atoms.positions, cog, box=box) @@ -735,7 +829,7 @@ def test_spherical_layer(self, u, periodic): assert ref == set(result.indices) - @pytest.mark.parametrize('periodic', (True, False)) + @pytest.mark.parametrize("periodic", (True, False)) def test_isolayer(self, u, periodic): rmin, rmax = 2.4, 3 @@ -744,7 +838,10 @@ def test_isolayer(self, u, periodic): else: r1 = u.select_atoms("resid 1 or resid 2 and type O") - sel = Parser.parse("isolayer {} {} (index {} or index {})".format(rmin, rmax, *r1.ix), u.atoms) + sel = Parser.parse( + "isolayer {} {} (index {} or index {})".format(rmin, rmax, *r1.ix), + u.atoms, + ) if periodic: sel.periodic = True else: @@ -757,15 +854,15 @@ def test_isolayer(self, u, periodic): cog2 = r1[1].position.reshape(1, 3) d2 = distance_array(u.atoms.positions, cog2, box=box) - ref_inner = set(np.where((d1 < rmin) | (d2 < rmin))[0]) - ref_outer = set(np.where((d1 < rmax) | (d2 < rmax))[0]) + ref_inner = set(np.where((d1 < rmin) | (d2 < rmin))[0]) + ref_outer = set(np.where((d1 < rmax) | (d2 < rmax))[0]) ref_outer -= ref_inner assert ref_outer == set(result.indices) and len(list(ref_outer)) > 0 - @pytest.mark.parametrize('periodic', (True, False)) + @pytest.mark.parametrize("periodic", (True, False)) def test_spherical_zone(self, u, periodic): - sel = Parser.parse('sphzone 5.0 resid 1', u.atoms) + sel = Parser.parse("sphzone 5.0 resid 1", u.atoms) if periodic: sel.periodic = True else: @@ -773,7 +870,7 @@ def test_spherical_zone(self, u, periodic): result = sel.apply(u.atoms) - r1 = u.select_atoms('resid 1') + r1 = u.select_atoms("resid 1") box = u.dimensions if periodic else None cog = r1.center_of_geometry().reshape(1, 3) d = distance_array(u.atoms.positions, cog, box=box) @@ -781,9 +878,9 @@ def test_spherical_zone(self, u, periodic): assert ref == set(result.indices) - @pytest.mark.parametrize('periodic', (True, False)) + @pytest.mark.parametrize("periodic", (True, False)) def test_point(self, u, periodic): - sel = Parser.parse('point 5.0 5.0 5.0 3.0', u.atoms) + sel = Parser.parse("point 5.0 5.0 5.0 3.0", u.atoms) if periodic: sel.periodic = True else: @@ -791,9 +888,11 @@ def test_point(self, u, periodic): result = sel.apply(u.atoms) box = u.dimensions if periodic else None - d = distance_array(np.array([[5.0, 5.0, 5.0]], dtype=np.float32), - u.atoms.positions, - box=box) + d = distance_array( + np.array([[5.0, 5.0, 5.0]], dtype=np.float32), + u.atoms.positions, + box=box, + ) ref = set(np.where(d < 3.0)[1]) assert ref == set(result.indices) @@ -804,16 +903,15 @@ class TestOrthogonalDistanceSelections(BaseDistanceSelection): def u(self): return mda.Universe(TRZ_psf, TRZ) - @pytest.mark.parametrize('meth, periodic', [ - ('distmat', True), - ('distmat', False) - ]) + @pytest.mark.parametrize( + "meth, periodic", [("distmat", True), ("distmat", False)] + ) def test_cyzone(self, u, meth, periodic): - sel = Parser.parse('cyzone 5 4 -4 resid 2', u.atoms) + sel = Parser.parse("cyzone 5 4 -4 resid 2", u.atoms) sel.periodic = periodic result = sel.apply(u.atoms) - other = u.select_atoms('resid 2') + other = u.select_atoms("resid 2") pos = other.center_of_geometry() vecs = u.atoms.positions - pos @@ -824,15 +922,15 @@ def test_cyzone(self, u, meth, periodic): mask = (vecs[:, 2] > -4) & (vecs[:, 2] < 4) radii = vecs[:, 0] ** 2 + vecs[:, 1] ** 2 - mask &= radii < 5 ** 2 + mask &= radii < 5**2 ref = set(u.atoms[mask].indices) assert ref == set(result.indices) - @pytest.mark.parametrize('periodic,expected', ([True, 33], [False, 25])) + @pytest.mark.parametrize("periodic,expected", ([True, 33], [False, 25])) def test_sphzone(self, u, periodic, expected): - sel = u.select_atoms('sphzone 5.0 resid 1', periodic=periodic) + sel = u.select_atoms("sphzone 5.0 resid 1", periodic=periodic) assert len(sel) == expected @@ -854,9 +952,9 @@ def u(self): return mda.Universe(GRO) def test_around(self, u): - r1 = u.select_atoms('resid 1') + r1 = u.select_atoms("resid 1") - ag = u.select_atoms('around 5.0 resid 1') + ag = u.select_atoms("around 5.0 resid 1") d = distance_array(u.atoms.positions, r1.positions, box=u.dimensions) idx = set(np.where(d < 5.0)[0]) @@ -866,10 +964,10 @@ def test_around(self, u): assert idx == set(ag.indices) def test_sphlayer(self, u): - r1 = u.select_atoms('resid 1') + r1 = u.select_atoms("resid 1") cog = r1.center_of_geometry().reshape(1, 3) - ag = u.select_atoms('sphlayer 2.4 6.0 resid 1') + ag = u.select_atoms("sphlayer 2.4 6.0 resid 1") d = distance_array(u.atoms.positions, cog, box=u.dimensions) idx = set(np.where((d > 2.4) & (d < 6.0))[0]) @@ -877,18 +975,18 @@ def test_sphlayer(self, u): assert idx == set(ag.indices) def test_empty_sphlayer(self, u): - empty = u.select_atoms('sphlayer 2.4 6.0 name NOT_A_NAME') + empty = u.select_atoms("sphlayer 2.4 6.0 name NOT_A_NAME") assert len(empty) == 0 def test_empty_isolayer(self, u): - empty = u.select_atoms('isolayer 2.4 6.0 name NOT_A_NAME') + empty = u.select_atoms("isolayer 2.4 6.0 name NOT_A_NAME") assert len(empty) == 0 def test_sphzone(self, u): - r1 = u.select_atoms('resid 1') + r1 = u.select_atoms("resid 1") cog = r1.center_of_geometry().reshape(1, 3) - ag = u.select_atoms('sphzone 5.0 resid 1') + ag = u.select_atoms("sphzone 5.0 resid 1") d = distance_array(u.atoms.positions, cog, box=u.dimensions) idx = set(np.where(d < 5.0)[0]) @@ -896,16 +994,18 @@ def test_sphzone(self, u): assert idx == set(ag.indices) def test_empty_sphzone(self, u): - empty = u.select_atoms('sphzone 5.0 name NOT_A_NAME') + empty = u.select_atoms("sphzone 5.0 name NOT_A_NAME") assert len(empty) == 0 def test_point_1(self, u): # The example selection - ag = u.select_atoms('point 5.0 5.0 5.0 3.5') + ag = u.select_atoms("point 5.0 5.0 5.0 3.5") - d = distance_array(np.array([[5.0, 5.0, 5.0]], dtype=np.float32), - u.atoms.positions, - box=u.dimensions) + d = distance_array( + np.array([[5.0, 5.0, 5.0]], dtype=np.float32), + u.atoms.positions, + box=u.dimensions, + ) idx = np.where(d < 3.5)[1] @@ -914,11 +1014,13 @@ def test_point_1(self, u): def test_point_2(self, u): ag1 = u.atoms[:10000] - ag2 = ag1.select_atoms('point 5.0 5.0 5.0 3.5') + ag2 = ag1.select_atoms("point 5.0 5.0 5.0 3.5") - d = distance_array(np.array([[5.0, 5.0, 5.0]], dtype=np.float32), - ag1.positions, - box=u.dimensions) + d = distance_array( + np.array([[5.0, 5.0, 5.0]], dtype=np.float32), + ag1.positions, + box=u.dimensions, + ) idx = np.where(d < 3.5)[1] @@ -935,112 +1037,145 @@ def gen_sel_strings(prop, oper): 'prop x<1.5' """ - for x, y in itertools.product([' ', ''], [' ', '']): - yield 'prop {prop}{spc1}{oper}{spc2}1.5'.format( - prop=prop, spc1=x, oper=oper, spc2=y) + for x, y in itertools.product([" ", ""], [" ", ""]): + yield "prop {prop}{spc1}{oper}{spc2}1.5".format( + prop=prop, spc1=x, oper=oper, spc2=y + ) class TestPropSelection(object): - plurals = {'mass': 'masses', - 'charge': 'charges'} + plurals = {"mass": "masses", "charge": "charges"} op_funcs = { - '<': np.less, - '<=': np.less_equal, - '>': np.greater, - '>=': np.greater_equal, - '==': np.equal, - '!=': np.not_equal + "<": np.less, + "<=": np.less_equal, + ">": np.greater, + ">=": np.greater_equal, + "==": np.equal, + "!=": np.not_equal, } opposites = { - '==': '==', '!=': '!=', - '>': '<=', '<=': '>', - '<': '>=', '>=': '<', + "==": "==", + "!=": "!=", + ">": "<=", + "<=": ">", + "<": ">=", + ">=": "<", } @pytest.fixture(params=[slice(None, None), slice(None, 100)]) def ag(self, request): - u = make_Universe(('masses', 'charges')) + u = make_Universe(("masses", "charges")) u.atoms[::2].masses = 1.5 u.atoms[::2].charges = 1.5 return u.atoms[request.param] - @pytest.mark.parametrize('prop, selstr', [ - (prop, sel) - for prop in ['mass', 'charge'] - for sel in gen_sel_strings(prop, '<') - ]) + @pytest.mark.parametrize( + "prop, selstr", + [ + (prop, sel) + for prop in ["mass", "charge"] + for sel in gen_sel_strings(prop, "<") + ], + ) def test_lt(self, prop, selstr, ag): sel = ag.select_atoms(selstr) - assert_equal(set(sel.indices), - set(ag[getattr(ag, self.plurals[prop]) < 1.5].indices)) - - @pytest.mark.parametrize('prop, selstr', [ - (prop, sel) - for prop in ['mass', 'charge'] - for sel in gen_sel_strings(prop, '<=') - ]) + assert_equal( + set(sel.indices), + set(ag[getattr(ag, self.plurals[prop]) < 1.5].indices), + ) + + @pytest.mark.parametrize( + "prop, selstr", + [ + (prop, sel) + for prop in ["mass", "charge"] + for sel in gen_sel_strings(prop, "<=") + ], + ) def test_le(self, prop, selstr, ag): sel = ag.select_atoms(selstr) - assert_equal(set(sel.indices), - set(ag[getattr(ag, - self.plurals[prop]) <= 1.5].indices)) - - @pytest.mark.parametrize('prop, selstr', [ - (prop, sel) - for prop in ['mass', 'charge'] - for sel in gen_sel_strings(prop, '>') - ]) + assert_equal( + set(sel.indices), + set(ag[getattr(ag, self.plurals[prop]) <= 1.5].indices), + ) + + @pytest.mark.parametrize( + "prop, selstr", + [ + (prop, sel) + for prop in ["mass", "charge"] + for sel in gen_sel_strings(prop, ">") + ], + ) def test_gt(self, prop, selstr, ag): sel = ag.select_atoms(selstr) - assert_equal(set(sel.indices), - set(ag[getattr(ag, self.plurals[prop]) > 1.5].indices)) - - @pytest.mark.parametrize('prop, selstr', [ - (prop, sel) - for prop in ['mass', 'charge'] - for sel in gen_sel_strings(prop, '>=') - ]) + assert_equal( + set(sel.indices), + set(ag[getattr(ag, self.plurals[prop]) > 1.5].indices), + ) + + @pytest.mark.parametrize( + "prop, selstr", + [ + (prop, sel) + for prop in ["mass", "charge"] + for sel in gen_sel_strings(prop, ">=") + ], + ) def test_ge(self, prop, selstr, ag): sel = ag.select_atoms(selstr) - assert_equal(set(sel.indices), - set(ag[getattr(ag, - self.plurals[prop]) >= 1.5].indices)) - - @pytest.mark.parametrize('prop, selstr', [ - (prop, sel) - for prop in ['mass', 'charge'] - for sel in gen_sel_strings(prop, '==') - ]) + assert_equal( + set(sel.indices), + set(ag[getattr(ag, self.plurals[prop]) >= 1.5].indices), + ) + + @pytest.mark.parametrize( + "prop, selstr", + [ + (prop, sel) + for prop in ["mass", "charge"] + for sel in gen_sel_strings(prop, "==") + ], + ) def test_eq(self, prop, selstr, ag): sel = ag.select_atoms(selstr) - assert_equal(set(sel.indices), - set(ag[getattr(ag, - self.plurals[prop]) == 1.5].indices)) - - @pytest.mark.parametrize('prop, selstr', [ - (prop, sel) - for prop in ['mass', 'charge'] - for sel in gen_sel_strings(prop, '!=') - ]) + assert_equal( + set(sel.indices), + set(ag[getattr(ag, self.plurals[prop]) == 1.5].indices), + ) + + @pytest.mark.parametrize( + "prop, selstr", + [ + (prop, sel) + for prop in ["mass", "charge"] + for sel in gen_sel_strings(prop, "!=") + ], + ) def test_ne(self, prop, selstr, ag): sel = ag.select_atoms(selstr) - assert_equal(set(sel.indices), - set(ag[getattr(ag, - self.plurals[prop]) != 1.5].indices)) - - @pytest.mark.parametrize('prop, op', [ - (prop, op) - for prop in ['mass', 'charge'] - for op in ('<', '>', '<=', '>=', '==', '!=') - ]) + assert_equal( + set(sel.indices), + set(ag[getattr(ag, self.plurals[prop]) != 1.5].indices), + ) + + @pytest.mark.parametrize( + "prop, op", + [ + (prop, op) + for prop in ["mass", "charge"] + for op in ("<", ">", "<=", ">=", "==", "!=") + ], + ) def test_flip(self, prop, ag, op): func = self.op_funcs[op] # reference group, doing things forwards ref = ag[func(getattr(ag, self.plurals[prop]), 1.5)] - selstr = 'prop 1.5 {op} {prop}'.format( - op=self.opposites[op], prop=prop) + selstr = "prop 1.5 {op} {prop}".format( + op=self.opposites[op], prop=prop + ) sel = ag.select_atoms(selstr) assert_equal(set(ref.indices), set(sel.indices)) @@ -1052,19 +1187,19 @@ def u(self): return mda.Universe(PSF, DCD) def test_bonded_1(self, u): - ag = u.select_atoms('type 2 and bonded name N') + ag = u.select_atoms("type 2 and bonded name N") assert len(ag) == 3 def test_nobonds_warns(self, u): - u = make_Universe(('names',)) + u = make_Universe(("names",)) # empty bond topology attr batt = mda.core.topologyattrs.Bonds([]) u.add_TopologyAttr(batt) with pytest.warns(UserWarning): - u.select_atoms('bonded name AAA') + u.select_atoms("bonded name AAA") class TestSelectionErrors(object): @@ -1072,36 +1207,40 @@ class TestSelectionErrors(object): @pytest.fixture() def universe(): return make_Universe( - ('names', 'masses', 'resids', 'resnames', 'resnums')) - - @pytest.mark.parametrize('selstr', [ - 'name and H', # string selection - 'name )', - 'resid abcd', # resid arg parsing selection - 'resnum 7a7', # rangeselection arg parsing - 'resid 1-', - 'prop chicken == tasty', - 'prop chicken <= 7.4', - 'prop mass ^^ 12.0', - 'same this as resid 1', # same selection - 'same resid resname mass 5.0', # same / expect - 'name H and', # check all tokens used - 'naem H', # unkonwn (misplet) opertaor - 'resid and name C', # rangesel not finding vals - 'resnum ', - 'bynum or protein', - 'index or protein', - 'prop mass < 4.0 hello', # unused token - 'prop mass > 10. and group this', # missing group - # bad ranges - 'mass 1.0 to', - 'mass to 3.0', - 'mass 1.0:', - 'mass :3.0', - 'mass 1-', - 'chirality ', - 'formalcharge 0.2', - ]) + ("names", "masses", "resids", "resnames", "resnums") + ) + + @pytest.mark.parametrize( + "selstr", + [ + "name and H", # string selection + "name )", + "resid abcd", # resid arg parsing selection + "resnum 7a7", # rangeselection arg parsing + "resid 1-", + "prop chicken == tasty", + "prop chicken <= 7.4", + "prop mass ^^ 12.0", + "same this as resid 1", # same selection + "same resid resname mass 5.0", # same / expect + "name H and", # check all tokens used + "naem H", # unkonwn (misplet) opertaor + "resid and name C", # rangesel not finding vals + "resnum ", + "bynum or protein", + "index or protein", + "prop mass < 4.0 hello", # unused token + "prop mass > 10. and group this", # missing group + # bad ranges + "mass 1.0 to", + "mass to 3.0", + "mass 1.0:", + "mass :3.0", + "mass 1-", + "chirality ", + "formalcharge 0.2", + ], + ) def test_selection_fail(self, selstr, universe): with pytest.raises(SelectionError): universe.select_atoms(selstr) @@ -1112,11 +1251,11 @@ def test_invalid_prop_selection(self, universe): def test_segid_and_resid(): - u = make_Universe(('segids', 'resids')) + u = make_Universe(("segids", "resids")) - ag = u.select_atoms('segid SegB and resid 1-100') + ag = u.select_atoms("segid SegB and resid 1-100") - ref = ag.select_atoms('segid SegB').select_atoms('resid 1-100') + ref = ag.select_atoms("segid SegB").select_atoms("resid 1-100") assert_equal(ag.indices, ref.indices) @@ -1126,7 +1265,8 @@ class TestImplicitOr(object): @pytest.fixture() def universe(): return make_Universe( - ('names', 'types', 'resids', 'resnums', 'resnames', 'segids')) + ("names", "types", "resids", "resnums", "resnames", "segids") + ) def _check_sels(self, ref, sel, universe): ref = universe.select_atoms(ref) @@ -1134,31 +1274,45 @@ def _check_sels(self, ref, sel, universe): assert_equal(ref.indices, sel.indices) - @pytest.mark.parametrize('ref, sel', [ - ('name NameABA or name NameACA or name NameADA', - 'name NameABA NameACA NameADA'), - ('type TypeE or type TypeD or type TypeB', 'type TypeE TypeD TypeB'), - ('resname RsC or resname RsY', 'resname RsC RsY'), - ('name NameAB* or name NameACC', 'name NameAB* NameACC'), - ('segid SegA or segid SegC', 'segid SegA SegC'), - ('(name NameABC or name NameABB) and (resname RsD or resname RsF)', - 'name NameABC NameABB and resname RsD RsF'), - ]) + @pytest.mark.parametrize( + "ref, sel", + [ + ( + "name NameABA or name NameACA or name NameADA", + "name NameABA NameACA NameADA", + ), + ( + "type TypeE or type TypeD or type TypeB", + "type TypeE TypeD TypeB", + ), + ("resname RsC or resname RsY", "resname RsC RsY"), + ("name NameAB* or name NameACC", "name NameAB* NameACC"), + ("segid SegA or segid SegC", "segid SegA SegC"), + ( + "(name NameABC or name NameABB) and (resname RsD or resname RsF)", + "name NameABC NameABB and resname RsD RsF", + ), + ], + ) def test_string_selections(self, ref, sel, universe): self._check_sels(ref, sel, universe) - @pytest.mark.parametrize("seltype", ['resid', 'resnum', 'bynum', 'index']) - @pytest.mark.parametrize('ref, sel', [ - ('{typ} 1 or {typ} 2', '{typ} 1 2'), - ('{typ} 1:10 or {typ} 22', '{typ} 1:10 22'), - ('{typ} 1:10 or {typ} 20:30', '{typ} 1:10 20:30'), - ('{typ} 1-5 or {typ} 7', '{typ} 1-5 7'), - ('{typ} 1-5 or {typ} 7:10 or {typ} 12', '{typ} 1-5 7:10 12'), - ('{typ} 1 or {typ} 3 or {typ} 5:10', '{typ} 1 3 5:10'), - ]) + @pytest.mark.parametrize("seltype", ["resid", "resnum", "bynum", "index"]) + @pytest.mark.parametrize( + "ref, sel", + [ + ("{typ} 1 or {typ} 2", "{typ} 1 2"), + ("{typ} 1:10 or {typ} 22", "{typ} 1:10 22"), + ("{typ} 1:10 or {typ} 20:30", "{typ} 1:10 20:30"), + ("{typ} 1-5 or {typ} 7", "{typ} 1-5 7"), + ("{typ} 1-5 or {typ} 7:10 or {typ} 12", "{typ} 1-5 7:10 12"), + ("{typ} 1 or {typ} 3 or {typ} 5:10", "{typ} 1 3 5:10"), + ], + ) def test_range_selections(self, seltype, ref, sel, universe): - self._check_sels(ref.format(typ=seltype), sel.format(typ=seltype), - universe) + self._check_sels( + ref.format(typ=seltype), sel.format(typ=seltype), universe + ) class TestICodeSelection(object): @@ -1167,13 +1321,13 @@ def u(self): return mda.Universe(PDB_icodes) def test_select_icode(self, u): - ag = u.select_atoms('resid 163A') + ag = u.select_atoms("resid 163A") assert len(ag) == 7 assert_equal(ag.ids, np.arange(7) + 1230) def test_select_resid_implicit_icode(self, u): - ag = u.select_atoms('resid 163') + ag = u.select_atoms("resid 163") assert len(ag) == 6 assert_equal(ag.ids, np.arange(6) + 1224) @@ -1181,11 +1335,11 @@ def test_select_resid_implicit_icode(self, u): def test_select_icode_range_1(self, u): # testing range within a single resid integer value u = u - ag = u.select_atoms('resid 163B-163D') + ag = u.select_atoms("resid 163B-163D") # do it manually without selection language... ref = u.residues[u.residues.resids == 163] - ref = ref[(ref.icodes >= 'B') & (ref.icodes <= 'D')] + ref = ref[(ref.icodes >= "B") & (ref.icodes <= "D")] ref = ref.atoms assert_equal(ag.ids, ref.ids) @@ -1196,17 +1350,17 @@ def test_select_icode_range_1(self, u): def test_select_icode_range_2(self, u): u = u - ag = u.select_atoms('resid 163B-165') + ag = u.select_atoms("resid 163B-165") resids = u.residues.resids start = u.residues[resids == 163] - start = start[start.icodes >= 'B'] + start = start[start.icodes >= "B"] mid = u.residues[resids == 164] end = u.residues[resids == 165] - end = end[end.icodes == ''] + end = end[end.icodes == ""] ref = start.atoms + mid.atoms + end.atoms @@ -1216,15 +1370,15 @@ def test_select_icode_range_3(self, u): # same as #2 but with no "middle" icodes u = u - ag = u.select_atoms('resid 163B-164') + ag = u.select_atoms("resid 163B-164") resids = u.residues.resids start = u.residues[resids == 163] - start = start[start.icodes >= 'B'] + start = start[start.icodes >= "B"] end = u.residues[resids == 164] - end = end[end.icodes == ''] + end = end[end.icodes == ""] ref = start.atoms + end.atoms @@ -1233,17 +1387,17 @@ def test_select_icode_range_3(self, u): def test_select_icode_range_4(self, u): u = u - ag = u.select_atoms('resid 160-163G') + ag = u.select_atoms("resid 160-163G") resids = u.residues.resids start = u.residues[resids == 160] - start = start[start.icodes >= ''] + start = start[start.icodes >= ""] mid = u.residues[(resids == 161) | (resids == 162)] end = u.residues[resids == 163] - end = end[end.icodes <= 'G'] + end = end[end.icodes <= "G"] ref = start.atoms + mid.atoms + end.atoms @@ -1253,15 +1407,15 @@ def test_select_icode_range_5(self, u): # same as #4 but with no "middle" icodes in range u = u - ag = u.select_atoms('resid 162-163G') + ag = u.select_atoms("resid 162-163G") resids = u.residues.resids start = u.residues[resids == 162] - start = start[start.icodes >= ''] + start = start[start.icodes >= ""] end = u.residues[resids == 163] - end = end[end.icodes <= 'G'] + end = end[end.icodes <= "G"] ref = start.atoms + end.atoms @@ -1269,14 +1423,14 @@ def test_select_icode_range_5(self, u): def test_missing_icodes_VE(self, u): # trying a selection with icodes in a Universe without raises VA - u = make_Universe(('resids',)) + u = make_Universe(("resids",)) with pytest.raises(ValueError): - u.select_atoms('resid 10A') + u.select_atoms("resid 10A") def test_missing_icodes_range_VE(self, u): - u = make_Universe(('resids',)) + u = make_Universe(("resids",)) with pytest.raises(ValueError): - u.select_atoms('resid 10A-12') + u.select_atoms("resid 10A-12") @pytest.fixture @@ -1296,10 +1450,10 @@ def u_pdb_icodes(): # See Issues #2308 for a discussion ("same resid as", 72), # Selection using resindices - # For PDBs: + # For PDBs: # residues with different insertion codes have different resindices - ("byres", 11) - ] + ("byres", 11), + ], ) def test_similarity_selection_icodes(u_pdb_icodes, selection, n_atoms): @@ -1308,30 +1462,54 @@ def test_similarity_selection_icodes(u_pdb_icodes, selection, n_atoms): assert len(sel.atoms) == n_atoms -@pytest.mark.parametrize('selection', [ - 'all', 'protein', 'backbone', 'nucleic', 'nucleicbackbone', - 'name O', 'name N*', 'resname stuff', 'resname ALA', 'type O', - 'index 0', 'index 1', 'bynum 1-10', - 'segid SYSTEM', 'resid 163', 'resid 1-10', 'resnum 2', - 'around 10 resid 1', 'point 0 0 0 10', 'sphzone 10 resid 1', - 'sphlayer 0 10 index 1', 'cyzone 15 4 -8 index 0', - 'cylayer 5 10 10 -8 index 1', 'prop abs z <= 100', - 'byres index 0', 'same resid as index 0', -]) + +@pytest.mark.parametrize( + "selection", + [ + "all", + "protein", + "backbone", + "nucleic", + "nucleicbackbone", + "name O", + "name N*", + "resname stuff", + "resname ALA", + "type O", + "index 0", + "index 1", + "bynum 1-10", + "segid SYSTEM", + "resid 163", + "resid 1-10", + "resnum 2", + "around 10 resid 1", + "point 0 0 0 10", + "sphzone 10 resid 1", + "sphlayer 0 10 index 1", + "cyzone 15 4 -8 index 0", + "cylayer 5 10 10 -8 index 1", + "prop abs z <= 100", + "byres index 0", + "same resid as index 0", + ], +) def test_selections_on_empty_group(u_pdb_icodes, selection): ag = u_pdb_icodes.atoms[[]].select_atoms(selection) assert len(ag) == 0 + def test_empty_yet_global(u_pdb_icodes): # slight exception to above test, an empty AG can return something if 'global' used - ag = u_pdb_icodes.atoms[[]].select_atoms('global name O') + ag = u_pdb_icodes.atoms[[]].select_atoms("global name O") assert len(ag) == 185 # len(u_pdb_icodes.select_atoms('name O')) + def test_arbitrary_atom_group_raises_error(): u = make_Universe(trajectory=True) with pytest.raises(TypeError): - u.select_atoms('around 2.0 group this', this=u.atoms[0]) + u.select_atoms("around 2.0 group this", this=u.atoms[0]) def test_empty_sel(): @@ -1345,12 +1523,12 @@ def test_empty_sel(): def test_record_type_sel(): u = mda.Universe(PDB_HOLE) - assert len(u.select_atoms('record_type ATOM')) == 264 - assert len(u.select_atoms('not record_type HETATM')) == 264 - assert len(u.select_atoms('record_type HETATM')) == 8 + assert len(u.select_atoms("record_type ATOM")) == 264 + assert len(u.select_atoms("not record_type HETATM")) == 264 + assert len(u.select_atoms("record_type HETATM")) == 8 - assert len(u.select_atoms('name CA and not record_type HETATM')) == 30 - assert len(u.select_atoms('name CA and record_type HETATM')) == 2 + assert len(u.select_atoms("name CA and not record_type HETATM")) == 30 + assert len(u.select_atoms("name CA and record_type HETATM")) == 2 def test_element_sel(): @@ -1368,65 +1546,74 @@ def test_chain_sel(): @pytest.fixture() def u_fake_masses(): u = mda.Universe(TPR) - u.atoms[-10:].masses = - (np.arange(10) + 1.001) + u.atoms[-10:].masses = -(np.arange(10) + 1.001) u.atoms[:5].masses = 0.1 * 3 # 0.30000000000000004 u.atoms[5:10].masses = 0.30000000000000001 return u -@pytest.mark.parametrize("selstr,n_atoms, selkwargs", [ - ("mass 0.8 to 1.2", 23844, {}), - ("mass 8e-1 to 1200e-3", 23844, {}), - ("mass -5--3", 2, {}), # select -5 to -3 - ("mass -3 : -5", 0, {}), # wrong way around - # regex; spacing, delimiters, and negatives - ("mass -5 --3", 2, {}), - ("mass -5- -3", 2, {}), - ("mass -5 - -3", 2, {}), - ("mass -10:3", 34945, {}), - ("mass -10 :3", 34945, {}), - ("mass -10: 3", 34945, {}), - ("mass -10 : 3", 34945, {}), - ("mass -10 -3", 0, {}), # separate selections, not range - # float equality - ("mass 0.3", 5, {"rtol": 0, "atol": 0}), - ("mass 0.3", 5, {"rtol": 1e-22, "atol": 1e-22}), - # 0.30000000000000001 == 0.3 - ("mass 0.3 - 0.30000000000000004", 10, {}), - ("mass 0.30000000000000004", 5, {"rtol": 0, "atol": 0}), - ("mass 0.3 0.30000000000000001", 5, {"rtol": 0, "atol": 0}), - # float near-equality - ("mass 0.3", 10, {}), - ("mass 0.30000000000000004", 10, {}), - ("mass 0.3 0.30000000000000001", 10, {}), - # prop thingy - ("prop mass == 0.3", 10, {}), - ("prop mass == 0.30000000000000004", 10, {}), - ("prop mass == 0.30000000000000004", 5, {"rtol": 0, "atol": 0}), -]) +@pytest.mark.parametrize( + "selstr,n_atoms, selkwargs", + [ + ("mass 0.8 to 1.2", 23844, {}), + ("mass 8e-1 to 1200e-3", 23844, {}), + ("mass -5--3", 2, {}), # select -5 to -3 + ("mass -3 : -5", 0, {}), # wrong way around + # regex; spacing, delimiters, and negatives + ("mass -5 --3", 2, {}), + ("mass -5- -3", 2, {}), + ("mass -5 - -3", 2, {}), + ("mass -10:3", 34945, {}), + ("mass -10 :3", 34945, {}), + ("mass -10: 3", 34945, {}), + ("mass -10 : 3", 34945, {}), + ("mass -10 -3", 0, {}), # separate selections, not range + # float equality + ("mass 0.3", 5, {"rtol": 0, "atol": 0}), + ("mass 0.3", 5, {"rtol": 1e-22, "atol": 1e-22}), + # 0.30000000000000001 == 0.3 + ("mass 0.3 - 0.30000000000000004", 10, {}), + ("mass 0.30000000000000004", 5, {"rtol": 0, "atol": 0}), + ("mass 0.3 0.30000000000000001", 5, {"rtol": 0, "atol": 0}), + # float near-equality + ("mass 0.3", 10, {}), + ("mass 0.30000000000000004", 10, {}), + ("mass 0.3 0.30000000000000001", 10, {}), + # prop thingy + ("prop mass == 0.3", 10, {}), + ("prop mass == 0.30000000000000004", 10, {}), + ("prop mass == 0.30000000000000004", 5, {"rtol": 0, "atol": 0}), + ], +) def test_mass_sel(u_fake_masses, selstr, n_atoms, selkwargs): # test auto-topattr addition of float (FloatRangeSelection) ag = u_fake_masses.select_atoms(selstr, **selkwargs) assert len(ag) == n_atoms + def test_mass_sel_warning(u_fake_masses): - warn_msg = (r"Using float equality .* is not recommended .* " - r"we recommend using a range .*" - r"'mass -0.6 to 1.4'.*" - r"use the `atol` and `rtol` keywords") + warn_msg = ( + r"Using float equality .* is not recommended .* " + r"we recommend using a range .*" + r"'mass -0.6 to 1.4'.*" + r"use the `atol` and `rtol` keywords" + ) with pytest.warns(SelectionWarning, match=warn_msg): u_fake_masses.select_atoms("mass 0.4") -@pytest.mark.parametrize("selstr,n_res", [ - ("resnum -10 to 3", 13), - ("resnum -5--3", 3), # select -5 to -3 - ("resnum -3 : -5", 0), # wrong way around -]) +@pytest.mark.parametrize( + "selstr,n_res", + [ + ("resnum -10 to 3", 13), + ("resnum -5--3", 3), # select -5 to -3 + ("resnum -3 : -5", 0), # wrong way around + ], +) def test_int_sel(selstr, n_res): # test auto-topattr addition of int (IntRangeSelection) u = mda.Universe(TPR) - u.residues[-10:].resnums = - (np.arange(10) + 1) + u.residues[-10:].resnums = -(np.arange(10) + 1) ag = u.select_atoms(selstr).residues assert len(ag) == n_res @@ -1444,12 +1631,15 @@ def test_negative_resid(): assert len(ag) == 4 -@pytest.mark.parametrize("selstr, n_atoms", [ - ("aromaticity", 5), - ("aromaticity true", 5), - ("not aromaticity", 15), - ("aromaticity False", 15), -]) +@pytest.mark.parametrize( + "selstr, n_atoms", + [ + ("aromaticity", 5), + ("aromaticity true", 5), + ("not aromaticity", 15), + ("aromaticity False", 15), + ], +) def test_bool_sel(selstr, n_atoms): if NumpyVersion(np.__version__) >= "2.0.0": pytest.skip("RDKit does not support NumPy 2") @@ -1471,14 +1661,18 @@ def test_bool_sel_error(): def test_error_selection_for_strange_dtype(): with pytest.raises(ValueError, match="No base class defined for dtype"): - MDAnalysis.core.selection.gen_selection_class("star", "stars", - dict, "atom") + MDAnalysis.core.selection.gen_selection_class( + "star", "stars", dict, "atom" + ) -@pytest.mark.parametrize("sel, ix", [ - ("name N", [5, 335, 451]), - ("resname GLU", [5, 6, 7, 8, 335, 451]), -]) +@pytest.mark.parametrize( + "sel, ix", + [ + ("name N", [5, 335, 451]), + ("resname GLU", [5, 6, 7, 8, 335, 451]), + ], +) def test_default_selection_on_ordered_unique_group(u_pdb_icodes, sel, ix): # manually ordered unique atomgroup => sorted by index base_ag = u_pdb_icodes.atoms[[335, 5, 451, 8, 7, 6]] @@ -1486,12 +1680,15 @@ def test_default_selection_on_ordered_unique_group(u_pdb_icodes, sel, ix): assert_equal(ag.ix, ix) -@pytest.mark.parametrize("sel, sort, ix", [ - ("name N", True, [5, 335, 451]), - ("name N", False, [335, 5, 451]), - ("resname GLU", True, [5, 6, 7, 8, 335, 451]), - ("resname GLU", False, [335, 5, 451, 8, 7, 6]), -]) +@pytest.mark.parametrize( + "sel, sort, ix", + [ + ("name N", True, [5, 335, 451]), + ("name N", False, [335, 5, 451]), + ("resname GLU", True, [5, 6, 7, 8, 335, 451]), + ("resname GLU", False, [335, 5, 451, 8, 7, 6]), + ], +) def test_unique_selection_on_ordered_unique_group(u_pdb_icodes, sel, sort, ix): # manually ordered unique atomgroup base_ag = u_pdb_icodes.atoms[[335, 5, 451, 8, 7, 6]] @@ -1499,12 +1696,15 @@ def test_unique_selection_on_ordered_unique_group(u_pdb_icodes, sel, sort, ix): assert_equal(ag.ix, ix) -@pytest.mark.parametrize("sel, sort, ix", [ - ("name N", True, [5, 335, 451]), - ("name N", False, [335, 5, 451]), - ("resname GLU", True, [5, 6, 7, 8, 335, 451]), - ("resname GLU", False, [335, 5, 451, 8, 7, 6]), -]) +@pytest.mark.parametrize( + "sel, sort, ix", + [ + ("name N", True, [5, 335, 451]), + ("name N", False, [335, 5, 451]), + ("resname GLU", True, [5, 6, 7, 8, 335, 451]), + ("resname GLU", False, [335, 5, 451, 8, 7, 6]), + ], +) def test_unique_selection_on_ordered_group(u_pdb_icodes, sel, sort, ix): # manually ordered duplicate atomgroup base_ag = u_pdb_icodes.atoms[[335, 5, 451, 8, 5, 5, 7, 6, 451]] @@ -1512,47 +1712,64 @@ def test_unique_selection_on_ordered_group(u_pdb_icodes, sel, sort, ix): assert_equal(ag.ix, ix) -@pytest.mark.parametrize('smi,chirality', [ - ('C[C@@H](C(=O)O)N', 'S'), - ('C[C@H](C(=O)O)N', 'R'), -]) +@pytest.mark.parametrize( + "smi,chirality", + [ + ("C[C@@H](C(=O)O)N", "S"), + ("C[C@H](C(=O)O)N", "R"), + ], +) def test_chirality(smi, chirality): - Chem = pytest.importorskip('rdkit.Chem', reason='requires rdkit') + Chem = pytest.importorskip("rdkit.Chem", reason="requires rdkit") m = Chem.MolFromSmiles(smi) u = mda.Universe(m) - assert hasattr(u.atoms, 'chiralities') + assert hasattr(u.atoms, "chiralities") - assert u.atoms[0].chirality == '' + assert u.atoms[0].chirality == "" assert u.atoms[1].chirality == chirality - assert_equal(u.atoms[:3].chiralities, np.array(['', chirality, ''], dtype='U1')) + assert_equal( + u.atoms[:3].chiralities, np.array(["", chirality, ""], dtype="U1") + ) -@pytest.mark.parametrize('sel,size', [ - ('R', 1), ('S', 1), ('R S', 2), ('S R', 2), -]) +@pytest.mark.parametrize( + "sel,size", + [ + ("R", 1), + ("S", 1), + ("R S", 2), + ("S R", 2), + ], +) def test_chirality_selection(sel, size): # 2 centers, one R one S - Chem = pytest.importorskip('rdkit.Chem', reason='requires rdkit') + Chem = pytest.importorskip("rdkit.Chem", reason="requires rdkit") - m = Chem.MolFromSmiles('CC[C@H](C)[C@H](C(=O)O)N') + m = Chem.MolFromSmiles("CC[C@H](C)[C@H](C(=O)O)N") u = mda.Universe(m) - ag = u.select_atoms('chirality {}'.format(sel)) + ag = u.select_atoms("chirality {}".format(sel)) assert len(ag) == size -@pytest.mark.parametrize('sel,size,name', [ - ('1', 1, 'NH2'), ('-1', 1, 'OD2'), ('0', 34, 'N'), ('-1 1', 2, 'OD2'), -]) +@pytest.mark.parametrize( + "sel,size,name", + [ + ("1", 1, "NH2"), + ("-1", 1, "OD2"), + ("0", 34, "N"), + ("-1 1", 2, "OD2"), + ], +) def test_formal_charge_selection(sel, size, name): # 2 charge points, one positive one negative u = mda.Universe(PDB_charges) - ag = u.select_atoms(f'formalcharge {sel}') + ag = u.select_atoms(f"formalcharge {sel}") assert len(ag) == size assert ag.atoms[0].name == name diff --git a/testsuite/MDAnalysisTests/core/test_copying.py b/testsuite/MDAnalysisTests/core/test_copying.py index 9f8d0c7000c..919d6fb08c7 100644 --- a/testsuite/MDAnalysisTests/core/test_copying.py +++ b/testsuite/MDAnalysisTests/core/test_copying.py @@ -24,9 +24,7 @@ from numpy.testing import assert_equal import pytest -from MDAnalysisTests.datafiles import ( - PSF, DCD, PDB_small -) +from MDAnalysisTests.datafiles import PSF, DCD, PDB_small import MDAnalysis as mda from MDAnalysis.core import topology @@ -36,9 +34,11 @@ @pytest.fixture() def refTT(): ref = topology.TransTable( - 9, 6, 3, + 9, + 6, + 3, atom_resindex=np.array([0, 0, 1, 1, 2, 2, 3, 4, 5]), - residue_segindex=np.array([0, 1, 2, 0, 1, 1]) + residue_segindex=np.array([0, 1, 2, 0, 1, 1]), ) return ref @@ -51,13 +51,13 @@ def test_size(self, refTT): assert new.n_segments == refTT.n_segments def test_size_independent(self, refTT): - # check changing + # check changing new = refTT.copy() old = refTT.n_atoms refTT.n_atoms = -10 assert new.n_atoms == old - @pytest.mark.parametrize('attr', ['_AR', 'RA', '_RS', 'SR']) + @pytest.mark.parametrize("attr", ["_AR", "RA", "_RS", "SR"]) def test_AR(self, refTT, attr): new = refTT.copy() ref = getattr(refTT, attr) @@ -66,7 +66,7 @@ def test_AR(self, refTT, attr): for a, b in zip(ref, other): assert_equal(a, b) - @pytest.mark.parametrize('attr', ['_AR', 'RA', '_RS', 'SR']) + @pytest.mark.parametrize("attr", ["_AR", "RA", "_RS", "SR"]) def test_AR_independent(self, refTT, attr): new = refTT.copy() ref = getattr(refTT, attr) @@ -89,38 +89,41 @@ def test_move_residue(self, refTT): TA_FILLER = { - object: np.array(['dave', 'steve', 'hugo'], dtype=object), + object: np.array(["dave", "steve", "hugo"], dtype=object), int: np.array([5, 4, 6]), float: np.array([15.4, 5.7, 22.2]), - 'record': np.array(['ATOM', 'ATOM', 'HETATM'], dtype='object'), - 'bond': [(0, 1), (1, 2), (5, 6)], - 'angles': [(0, 1, 2), (1, 2, 3), (4, 5, 6)], - 'dihe': [(0, 1, 2, 3), (1, 2, 3, 4), (5, 6, 7, 8)], + "record": np.array(["ATOM", "ATOM", "HETATM"], dtype="object"), + "bond": [(0, 1), (1, 2), (5, 6)], + "angles": [(0, 1, 2), (1, 2, 3), (4, 5, 6)], + "dihe": [(0, 1, 2, 3), (1, 2, 3, 4), (5, 6, 7, 8)], } -@pytest.fixture(params=[ - (ta.Atomids, int), - (ta.Atomnames, object), - (ta.Atomtypes, object), - (ta.Elements, object), - (ta.Radii, float), - (ta.RecordTypes, 'record'), - (ta.ChainIDs, object), - (ta.Tempfactors, float), - (ta.Masses, float), - (ta.Charges, float), - (ta.Occupancies, float), - (ta.AltLocs, object), - (ta.Resids, int), - (ta.Resnames, object), - (ta.Resnums, int), - (ta.ICodes, object), - (ta.Segids, object), - (ta.Bonds, 'bond'), - (ta.Angles, 'angles'), - (ta.Dihedrals, 'dihe'), - (ta.Impropers, 'dihe'), -]) + +@pytest.fixture( + params=[ + (ta.Atomids, int), + (ta.Atomnames, object), + (ta.Atomtypes, object), + (ta.Elements, object), + (ta.Radii, float), + (ta.RecordTypes, "record"), + (ta.ChainIDs, object), + (ta.Tempfactors, float), + (ta.Masses, float), + (ta.Charges, float), + (ta.Occupancies, float), + (ta.AltLocs, object), + (ta.Resids, int), + (ta.Resnames, object), + (ta.Resnums, int), + (ta.ICodes, object), + (ta.Segids, object), + (ta.Bonds, "bond"), + (ta.Angles, "angles"), + (ta.Dihedrals, "dihe"), + (ta.Impropers, "dihe"), + ] +) def refTA(request): cls, dt = request.param return cls(TA_FILLER[dt]) @@ -136,38 +139,45 @@ def test_copy_attr(refTA): @pytest.fixture() def refTop(): return topology.Topology( - 3, 2, 2, - attrs = [ + 3, + 2, + 2, + attrs=[ ta.Atomnames(TA_FILLER[object]), ta.Masses(TA_FILLER[float]), ta.Resids(TA_FILLER[int]), - ta.Bonds(TA_FILLER['bond']), + ta.Bonds(TA_FILLER["bond"]), ], atom_resindex=np.array([0, 0, 1]), - residue_segindex=np.array([0, 1]) + residue_segindex=np.array([0, 1]), ) + def test_topology_copy_n_attrs(refTop): new = refTop.copy() assert len(new.attrs) == 7 # 4 + 3 indices -@pytest.mark.parametrize('attr', [ - 'names', - 'masses', - 'resids', - 'bonds', - 'tt', -]) + +@pytest.mark.parametrize( + "attr", + [ + "names", + "masses", + "resids", + "bonds", + "tt", + ], +) def test_topology_copy_unique_attrs(refTop, attr): new = refTop.copy() assert getattr(refTop, attr) is not getattr(new, attr) - -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def refUniverse(): return mda.Universe(PSF, DCD) + class TestCopyUniverse(object): def test_universe_copy(self, refUniverse): new = refUniverse.copy() @@ -177,7 +187,7 @@ def test_universe_copy(self, refUniverse): def test_positions(self, refUniverse): new = refUniverse.copy() - + assert_equal(new.atoms.positions, refUniverse.atoms.positions) def test_change_positions(self, refUniverse): @@ -189,7 +199,7 @@ def test_change_positions(self, refUniverse): assert_equal(new.atoms[0].position, previous) assert_equal(refUniverse.atoms[0].position, [1, 2, 3]) - + def test_topology(self, refUniverse): new = refUniverse.copy() @@ -199,10 +209,11 @@ def test_change_topology(self, refUniverse): new = refUniverse.copy() previous = new.atoms[0].name - refUniverse.atoms[0].name = 'newname' + refUniverse.atoms[0].name = "newname" assert new.atoms[0].name == previous - assert refUniverse.atoms[0].name == 'newname' + assert refUniverse.atoms[0].name == "newname" + def test_pdb_copy(): u = mda.Universe(PDB_small) diff --git a/testsuite/MDAnalysisTests/core/test_fragments.py b/testsuite/MDAnalysisTests/core/test_fragments.py index 02c3bb00ef2..5d9d965a376 100644 --- a/testsuite/MDAnalysisTests/core/test_fragments.py +++ b/testsuite/MDAnalysisTests/core/test_fragments.py @@ -110,10 +110,7 @@ class TestFragments(object): Test ring molecules? """ - @pytest.mark.parametrize('u', ( - case1(), - case2() - )) + @pytest.mark.parametrize("u", (case1(), case2())) def test_total_frags(self, u): fragments = u.atoms.fragments fragindices = u.atoms.fragindices @@ -127,23 +124,17 @@ def test_total_frags(self, u): assert len(np.unique(fragindices)) == len(fragments) # check fragindices dtype: assert fragindices.dtype == np.intp - #check n_fragments + # check n_fragments assert u.atoms.n_fragments == len(fragments) - @pytest.mark.parametrize('u', ( - case1(), - case2() - )) + @pytest.mark.parametrize("u", (case1(), case2())) def test_frag_external_ordering(self, u): # check fragments and fragindices are sorted correctly: for i, frag in enumerate(u.atoms.fragments): assert frag[0].index == i * 25 assert np.unique(frag.fragindices)[0] == i - @pytest.mark.parametrize('u', ( - case1(), - case2() - )) + @pytest.mark.parametrize("u", (case1(), case2())) def test_frag_internal_ordering(self, u): # check atoms are sorted within fragments and have the same fragindex: for i, frag in enumerate(u.atoms.fragments): @@ -151,10 +142,7 @@ def test_frag_internal_ordering(self, u): assert len(np.unique(frag.fragindices)) == 1 assert frag.n_fragments == 1 - @pytest.mark.parametrize('u', ( - case1(), - case2() - )) + @pytest.mark.parametrize("u", (case1(), case2())) def test_atom_access(self, u): # check atom can access fragment and fragindex: for at in (u.atoms[0], u.atoms[76], u.atoms[111]): @@ -167,10 +155,7 @@ def test_atom_access(self, u): with pytest.raises(AttributeError): x = at.n_fragments - @pytest.mark.parametrize('u', ( - case1(), - case2() - )) + @pytest.mark.parametrize("u", (case1(), case2())) def test_atomgroup_access(self, u): # check atomgroup can access fragments # first 60 atoms have 3 fragments, given as tuple @@ -198,36 +183,36 @@ def test_atomgroup_fragments_nobonds_NDE(self): u = make_Universe() ag = u.atoms[:10] with pytest.raises(NoDataError): - getattr(ag, 'fragments') + getattr(ag, "fragments") with pytest.raises(NoDataError): - getattr(ag, 'fragindices') + getattr(ag, "fragindices") with pytest.raises(NoDataError): - getattr(ag, 'n_fragments') + getattr(ag, "n_fragments") def test_atom_fragment_nobonds_NDE(self): # should raise NDE u = make_Universe() with pytest.raises(NoDataError): - getattr(u.atoms[10], 'fragment') + getattr(u.atoms[10], "fragment") with pytest.raises(NoDataError): - getattr(u.atoms[10], 'fragindex') + getattr(u.atoms[10], "fragindex") def test_atomgroup_fragment_cache_invalidation_bond_making(self): u = case1() fgs = u.atoms.fragments - assert fgs is u.atoms._cache['fragments'] - assert u.atoms._cache_key in u._cache['_valid']['fragments'] + assert fgs is u.atoms._cache["fragments"] + assert u.atoms._cache_key in u._cache["_valid"]["fragments"] u.add_bonds((fgs[0][-1] + fgs[1][0],)) # should trigger invalidation - assert 'fragments' not in u._cache['_valid'] + assert "fragments" not in u._cache["_valid"] assert len(fgs) > len(u.atoms.fragments) # recomputed def test_atomgroup_fragment_cache_invalidation_bond_breaking(self): u = case1() fgs = u.atoms.fragments - assert fgs is u.atoms._cache['fragments'] - assert u.atoms._cache_key in u._cache['_valid']['fragments'] + assert fgs is u.atoms._cache["fragments"] + assert u.atoms._cache_key in u._cache["_valid"]["fragments"] u.delete_bonds((u.atoms.bonds[3],)) # should trigger invalidation - assert 'fragments' not in u._cache['_valid'] + assert "fragments" not in u._cache["_valid"] assert len(fgs) < len(u.atoms.fragments) # recomputed diff --git a/testsuite/MDAnalysisTests/core/test_group_traj_access.py b/testsuite/MDAnalysisTests/core/test_group_traj_access.py index bc63c83466d..f7061177f4d 100644 --- a/testsuite/MDAnalysisTests/core/test_group_traj_access.py +++ b/testsuite/MDAnalysisTests/core/test_group_traj_access.py @@ -29,9 +29,13 @@ from MDAnalysisTests import make_Universe from MDAnalysisTests.datafiles import ( - COORDINATES_XYZ, COORDINATES_TRR, - GRO, TRR, - GRO_velocity, PDB_xvf, TRR_xvf + COORDINATES_XYZ, + COORDINATES_TRR, + GRO, + TRR, + GRO_velocity, + PDB_xvf, + TRR_xvf, ) import MDAnalysis @@ -39,7 +43,7 @@ def assert_not_view(arr): - assert arr.flags['OWNDATA'] is True + assert arr.flags["OWNDATA"] is True def assert_correct_errormessage(func, var): @@ -52,12 +56,16 @@ def assert_correct_errormessage(func, var): pytest.fail() -@pytest.mark.parametrize('pos,vel,force', ( - (True, False, False), - (True, True, False), - (True, False, True), - (True, True, True), -), indirect=True) +@pytest.mark.parametrize( + "pos,vel,force", + ( + (True, False, False), + (True, True, False), + (True, False, True), + (True, True, True), + ), + indirect=True, +) class TestAtomGroupTrajAccess(object): """ For AtomGroup and Atom access: @@ -78,6 +86,7 @@ class TestAtomGroupTrajAccess(object): - check value in master Timestep object is updated if not present, check we get proper NoDataError on setting """ + @pytest.fixture() def pos(self, request): return request.param @@ -115,9 +124,10 @@ def test_atomgroup_velocities_access(self, u, vel): assert_equal(ag_vel, u.trajectory.ts.velocities[10:20]) else: with pytest.raises(NoDataError): - getattr(ag, 'velocities') - assert_correct_errormessage((getattr, ag, 'velocities'), - 'velocities') + getattr(ag, "velocities") + assert_correct_errormessage( + (getattr, ag, "velocities"), "velocities" + ) def test_atomgroup_forces_access(self, u, force): ag = u.atoms[10:20] @@ -131,8 +141,8 @@ def test_atomgroup_forces_access(self, u, force): assert_equal(ag_for, u.trajectory.ts.forces[10:20]) else: with pytest.raises(NoDataError): - getattr(ag, 'forces') - assert_correct_errormessage((getattr, ag, 'forces'), 'forces') + getattr(ag, "forces") + assert_correct_errormessage((getattr, ag, "forces"), "forces") def test_atom_position_access(self, u): at = u.atoms[55] @@ -156,9 +166,10 @@ def test_atom_velocity_access(self, u, vel): assert_equal(at_vel, u.trajectory.ts.velocities[55]) else: with pytest.raises(NoDataError): - getattr(at, 'velocity') + getattr(at, "velocity") assert_correct_errormessage( - (getattr, at, 'velocity'), 'velocities') + (getattr, at, "velocity"), "velocities" + ) def test_atom_force_access(self, u, force): at = u.atoms[55] @@ -172,58 +183,68 @@ def test_atom_force_access(self, u, force): assert_equal(at_for, u.trajectory.ts.forces[55]) else: with pytest.raises(NoDataError): - getattr(at, 'force') - assert_correct_errormessage((getattr, at, 'force'), 'forces') + getattr(at, "force") + assert_correct_errormessage((getattr, at, "force"), "forces") def test_atomgroup_positions_setting(self, u): ag = u.atoms[[101, 107, 109]] - new = np.array([[72.4, 64.5, 74.7], - [124.6, 15.6, -1.11], - [25.2, -66.6, 0]]) + new = np.array( + [[72.4, 64.5, 74.7], [124.6, 15.6, -1.11], [25.2, -66.6, 0]] + ) ag.positions = new assert_almost_equal(ag.positions, new, decimal=5) - assert_almost_equal(u.trajectory.ts.positions[[101, 107, 109]], - new, decimal=5) + assert_almost_equal( + u.trajectory.ts.positions[[101, 107, 109]], new, decimal=5 + ) def test_atomgroup_velocities_setting(self, u, vel): ag = u.atoms[[101, 107, 109]] - new = np.array([[72.4, 64.5, 74.7], - [124.6, 15.6, -1.11], - [25.2, -66.6, 0]]) + 0.1 + new = ( + np.array( + [[72.4, 64.5, 74.7], [124.6, 15.6, -1.11], [25.2, -66.6, 0]] + ) + + 0.1 + ) if vel: ag.velocities = new assert_almost_equal(ag.velocities, new, decimal=5) assert_almost_equal( - u.trajectory.ts.velocities[[101, 107, 109]], new, decimal=5) + u.trajectory.ts.velocities[[101, 107, 109]], new, decimal=5 + ) else: with pytest.raises(NoDataError): - setattr(ag, 'velocities', new) - assert_correct_errormessage((setattr, ag, 'velocities', new), - 'velocities') + setattr(ag, "velocities", new) + assert_correct_errormessage( + (setattr, ag, "velocities", new), "velocities" + ) def test_atomgroup_forces_setting(self, u, force): ag = u.atoms[[101, 107, 109]] - new = np.array([[72.4, 64.5, 74.7], - [124.6, 15.6, -1.11], - [25.2, -66.6, 0]]) + 0.2 + new = ( + np.array( + [[72.4, 64.5, 74.7], [124.6, 15.6, -1.11], [25.2, -66.6, 0]] + ) + + 0.2 + ) if force: ag.forces = new assert_almost_equal(ag.forces, new, decimal=5) - assert_almost_equal(u.trajectory.ts.forces[[101, 107, 109]], - new, decimal=5) + assert_almost_equal( + u.trajectory.ts.forces[[101, 107, 109]], new, decimal=5 + ) else: with pytest.raises(NoDataError): - setattr(ag, 'forces', new) - assert_correct_errormessage((setattr, ag, 'forces', new), 'forces') + setattr(ag, "forces", new) + assert_correct_errormessage((setattr, ag, "forces", new), "forces") def test_atom_position_setting(self, u): at = u.atoms[94] @@ -244,13 +265,13 @@ def test_atom_velocity_setting(self, u, vel): at.velocity = new assert_almost_equal(at.velocity, new, decimal=5) - assert_almost_equal(u.trajectory.ts.velocities[94], new, - decimal=5) + assert_almost_equal(u.trajectory.ts.velocities[94], new, decimal=5) else: with pytest.raises(NoDataError): - setattr(at, 'velocity', new) - assert_correct_errormessage((setattr, at, 'velocity', new), - 'velocities') + setattr(at, "velocity", new) + assert_correct_errormessage( + (setattr, at, "velocity", new), "velocities" + ) def test_atom_force_setting(self, u, force): at = u.atoms[94] @@ -261,12 +282,11 @@ def test_atom_force_setting(self, u, force): at.force = new assert_almost_equal(at.force, new, decimal=5) - assert_almost_equal(u.trajectory.ts.forces[94], new, - decimal=5) + assert_almost_equal(u.trajectory.ts.forces[94], new, decimal=5) else: with pytest.raises(NoDataError): - setattr(at, 'force', new) - assert_correct_errormessage((setattr, at, 'force', new), 'forces') + setattr(at, "force", new) + assert_correct_errormessage((setattr, at, "force", new), "forces") class TestAtom_ForceVelocity(object): @@ -329,41 +349,66 @@ class TestGROVelocities(object): @pytest.fixture() def reference_velocities(self): return np.array( - [[-101.227, -0.57999998, 0.43400002], - [8.08500004, 3.19099998, -7.79099989], - [-9.04500008, -26.46899986, 13.17999935], - [2.51899981, 3.1400001, -1.73399997], - [-10.64100075, -11.34899998, 0.257], - [19.42700005, -8.21600056, -0.24399999]], dtype=np.float32) + [ + [-101.227, -0.57999998, 0.43400002], + [8.08500004, 3.19099998, -7.79099989], + [-9.04500008, -26.46899986, 13.17999935], + [2.51899981, 3.1400001, -1.73399997], + [-10.64100075, -11.34899998, 0.257], + [19.42700005, -8.21600056, -0.24399999], + ], + dtype=np.float32, + ) def testParse_velocities(self, reference_velocities): # read the velocities from the GRO_velocity file and compare the AtomGroup and individual Atom velocities # parsed with the reference values: u = MDAnalysis.Universe(GRO_velocity) - all_atoms = u.select_atoms('all') + all_atoms = u.select_atoms("all") # check for read-in and unit conversion for .gro file velocities for the entire AtomGroup: - assert_almost_equal(all_atoms.velocities, reference_velocities, - self.prec, - err_msg="problem reading .gro file velocities") + assert_almost_equal( + all_atoms.velocities, + reference_velocities, + self.prec, + err_msg="problem reading .gro file velocities", + ) # likewise for each individual atom (to be robust--in case someone alters the individual atom property code): - assert_almost_equal(all_atoms[0].velocity, reference_velocities[0], - self.prec, - err_msg="problem reading .gro file velocities") - assert_almost_equal(all_atoms[1].velocity, reference_velocities[1], - self.prec, - err_msg="problem reading .gro file velocities") - assert_almost_equal(all_atoms[2].velocity, reference_velocities[2], - self.prec, - err_msg="problem reading .gro file velocities") - assert_almost_equal(all_atoms[3].velocity, reference_velocities[3], - self.prec, - err_msg="problem reading .gro file velocities") - assert_almost_equal(all_atoms[4].velocity, reference_velocities[4], - self.prec, - err_msg="problem reading .gro file velocities") - assert_almost_equal(all_atoms[5].velocity, reference_velocities[5], - self.prec, - err_msg="problem reading .gro file velocities") + assert_almost_equal( + all_atoms[0].velocity, + reference_velocities[0], + self.prec, + err_msg="problem reading .gro file velocities", + ) + assert_almost_equal( + all_atoms[1].velocity, + reference_velocities[1], + self.prec, + err_msg="problem reading .gro file velocities", + ) + assert_almost_equal( + all_atoms[2].velocity, + reference_velocities[2], + self.prec, + err_msg="problem reading .gro file velocities", + ) + assert_almost_equal( + all_atoms[3].velocity, + reference_velocities[3], + self.prec, + err_msg="problem reading .gro file velocities", + ) + assert_almost_equal( + all_atoms[4].velocity, + reference_velocities[4], + self.prec, + err_msg="problem reading .gro file velocities", + ) + assert_almost_equal( + all_atoms[5].velocity, + reference_velocities[5], + self.prec, + err_msg="problem reading .gro file velocities", + ) class TestTRRForces(object): @@ -377,16 +422,22 @@ def universe(self): def reference_mean_protein_force(self): reference_mean_protein_force_native = np.array( [3.4609879271822823, -0.63302345167392804, -1.0587882545813336], - dtype=np.float32) + dtype=np.float32, + ) return reference_mean_protein_force_native / 10 def testForces(self, universe, reference_mean_protein_force): protein = universe.select_atoms("protein") assert_equal(len(protein), 918) mean_F = np.mean( - [protein.forces.mean(axis=0) for ts in universe.trajectory], axis=0) - assert_almost_equal(mean_F, reference_mean_protein_force, self.prec, - err_msg="mean force on protein over whole trajectory does not match") + [protein.forces.mean(axis=0) for ts in universe.trajectory], axis=0 + ) + assert_almost_equal( + mean_F, + reference_mean_protein_force, + self.prec, + err_msg="mean force on protein over whole trajectory does not match", + ) class TestTRRForcesNativeUnits(TestTRRForces): @@ -398,7 +449,8 @@ def universe(self): def reference_mean_protein_force(self): reference_mean_protein_force_native = np.array( [3.4609879271822823, -0.63302345167392804, -1.0587882545813336], - dtype=np.float32) + dtype=np.float32, + ) return reference_mean_protein_force_native @@ -419,20 +471,25 @@ def test_get_velocities(self, ag): def test_velocities(self, universe): ag = universe.atoms[42:45] - ref_v = np.array([ - [-3.61757946, -4.9867239, 2.46281552], - [2.57792854, 3.25411797, -0.75065529], - [13.91627216, 30.17778587, -12.16669178]]) + ref_v = np.array( + [ + [-3.61757946, -4.9867239, 2.46281552], + [2.57792854, 3.25411797, -0.75065529], + [13.91627216, 30.17778587, -12.16669178], + ] + ) v = ag.velocities - assert_almost_equal(v, ref_v, - err_msg="velocities were not read correctly") + assert_almost_equal( + v, ref_v, err_msg="velocities were not read correctly" + ) def test_set_velocities(self, ag): ag = ag v = ag.velocities - 2.7271 ag.velocities = v - assert_almost_equal(ag.velocities, v, - err_msg="messages were not set to new value") + assert_almost_equal( + ag.velocities, v, err_msg="messages were not set to new value" + ) class TestAtomGroupForces(object): @@ -452,12 +509,13 @@ def test_get_forces(self, ag): def test_forces(self, universe): ag = universe.atoms[1:4] - ref_v = np.arange(9).reshape(3, 3) * .01 + .03 + ref_v = np.arange(9).reshape(3, 3) * 0.01 + 0.03 v = ag.forces assert_almost_equal(v, ref_v, err_msg="forces were not read correctly") def test_set_forces(self, ag): v = ag.forces - 2.7271 ag.forces = v - assert_almost_equal(ag.forces, v, - err_msg="messages were not set to new value") + assert_almost_equal( + ag.forces, v, err_msg="messages were not set to new value" + ) diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index 6137d2b4244..7cd890b046f 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -23,11 +23,7 @@ import itertools import re import numpy as np -from numpy.testing import ( - assert_array_equal, - assert_equal, - assert_almost_equal -) +from numpy.testing import assert_array_equal, assert_equal, assert_almost_equal import pytest import operator import warnings @@ -40,21 +36,17 @@ class TestGroupProperties(object): - """ Test attributes common to all groups - """ + """Test attributes common to all groups""" + @pytest.fixture() def u(self): return make_Universe(trajectory=True) @pytest.fixture() def group_dict(self, u): - return { - 'atom': u.atoms, - 'residue': u.residues, - 'segment': u.segments - } + return {"atom": u.atoms, "residue": u.residues, "segment": u.segments} - uni = make_Universe() # can't use fixtures in @pytest.mark.parametrize + uni = make_Universe() # can't use fixtures in @pytest.mark.parametrize def test_dimensions(self, u, group_dict): dimensions = np.arange(6) @@ -63,60 +55,63 @@ def test_dimensions(self, u, group_dict): group.dimensions = dimensions.copy() assert_array_equal(group.dimensions, dimensions) assert_equal(u.dimensions, group.dimensions) - - @pytest.mark.parametrize('group', (uni.atoms[:2], uni.residues[:2], - uni.segments[:2])) + + @pytest.mark.parametrize( + "group", (uni.atoms[:2], uni.residues[:2], uni.segments[:2]) + ) def test_group_isunique(self, group): assert len(group) == 2 # Initially, cache must be empty: with pytest.raises(KeyError): - _ = group._cache['isunique'] + _ = group._cache["isunique"] # Check for correct value and type: assert group.isunique is True # Check if cache is set correctly: - assert group._cache['isunique'] is True + assert group._cache["isunique"] is True # Add duplicate element to group: group += group[0] assert len(group) == 3 # Cache must be reset since the group changed: with pytest.raises(KeyError): - _ = group._cache['isunique'] + _ = group._cache["isunique"] # Check for correct value and type: assert group.isunique is False # Check if cache is set correctly: - assert group._cache['isunique'] is False + assert group._cache["isunique"] is False - #Check empty group: + # Check empty group: group = group[[]] assert len(group) == 0 # Cache must be empty: with pytest.raises(KeyError): - _ = group._cache['isunique'] + _ = group._cache["isunique"] # Check for correct value and type: assert group.isunique is True # Check if cache is set correctly: - assert group._cache['isunique'] is True + assert group._cache["isunique"] is True - @pytest.mark.parametrize('group', (uni.atoms[:2], uni.residues[:2], - uni.segments[:2])) + @pytest.mark.parametrize( + "group", (uni.atoms[:2], uni.residues[:2], uni.segments[:2]) + ) def test_group_unique_nocache(self, group): # check unique group: assert len(group) == 2 # assert caches are empty: - for attr in ('isunique', 'sorted_unique', 'unsorted_unique'): + for attr in ("isunique", "sorted_unique", "unsorted_unique"): assert attr not in group._cache - @pytest.mark.parametrize('group', (uni.atoms[:2], uni.residues[:2], - uni.segments[:2])) + @pytest.mark.parametrize( + "group", (uni.atoms[:2], uni.residues[:2], uni.segments[:2]) + ) def test_create_unique_group_from_unique(self, group): unique_group = group.asunique(sorted=True) # assert identity and caches assert unique_group is group - assert group._cache['sorted_unique'] is unique_group - assert group._cache['unsorted_unique'] is unique_group - assert group._cache['isunique'] is True - assert group._cache['issorted'] # numpy.bool != bool + assert group._cache["sorted_unique"] is unique_group + assert group._cache["unsorted_unique"] is unique_group + assert group._cache["isunique"] is True + assert group._cache["issorted"] # numpy.bool != bool # assert .unique copies assert group.unique is not group @@ -127,7 +122,7 @@ def test_create_unique_group_from_unique(self, group): assert len(group) == 3 # assert caches are cleared since the group changed: - for attr in ('isunique', 'sorted_unique', 'unsorted_unique'): + for attr in ("isunique", "sorted_unique", "unsorted_unique"): assert attr not in group._cache # now not unique @@ -137,8 +132,8 @@ def test_create_unique_group_from_unique(self, group): assert group.asunique() is not unique_group assert group.asunique() == unique_group # check if caches have been set correctly: - assert group._cache['unsorted_unique'] is group.asunique() - assert group._cache['sorted_unique'] is group.asunique() + assert group._cache["unsorted_unique"] is group.asunique() + assert group._cache["sorted_unique"] is group.asunique() assert group.unique is not group.asunique() # check length and type: @@ -146,17 +141,18 @@ def test_create_unique_group_from_unique(self, group): assert type(group.unique) is type(group) # check if caches of group.sorted_unique have been set correctly: - assert group.sorted_unique._cache['isunique'] is True - assert group.sorted_unique._cache['sorted_unique'] is group.sorted_unique + assert group.sorted_unique._cache["isunique"] is True + assert ( + group.sorted_unique._cache["sorted_unique"] is group.sorted_unique + ) # assert that repeated access yields the same object (not a copy): unique_group = group.sorted_unique assert unique_group is group.sorted_unique - @pytest.mark.parametrize('ugroup', [uni.atoms, uni.residues, uni.segments]) - @pytest.mark.parametrize('ix, unique_ix', [ - ([0, 1], [0, 1]), - ([4, 3, 3, 1], [1, 3, 4]) - ]) + @pytest.mark.parametrize("ugroup", [uni.atoms, uni.residues, uni.segments]) + @pytest.mark.parametrize( + "ix, unique_ix", [([0, 1], [0, 1]), ([4, 3, 3, 1], [1, 3, 4])] + ) def test_group_unique_returns_sorted_copy(self, ugroup, ix, unique_ix): # is copy group = ugroup[ix] @@ -164,26 +160,32 @@ def test_group_unique_returns_sorted_copy(self, ugroup, ix, unique_ix): # sorted assert_equal(group.unique.ix, unique_ix) - @pytest.mark.parametrize('ugroup', [uni.atoms, uni.residues, uni.segments]) - @pytest.mark.parametrize('ix, value', [ - ([4, 3, 3, 1], False), - ([1, 3, 4], True), - ([2, 2, 2, 4], True), - ]) + @pytest.mark.parametrize("ugroup", [uni.atoms, uni.residues, uni.segments]) + @pytest.mark.parametrize( + "ix, value", + [ + ([4, 3, 3, 1], False), + ([1, 3, 4], True), + ([2, 2, 2, 4], True), + ], + ) def test_group_issorted(self, ugroup, ix, value): assert ugroup[ix].issorted == value - @pytest.mark.parametrize('ugroup', [uni.atoms, uni.residues, uni.segments]) - @pytest.mark.parametrize('ix, sort, unique_ix, is_same', [ - ([1, 3, 4], True, [1, 3, 4], True), - ([1, 3, 4], False, [1, 3, 4], True), - ([4, 3, 1], True, [1, 3, 4], False), - ([4, 3, 1], False, [4, 3, 1], True), - ([1, 3, 3, 4], True, [1, 3, 4], False), - ([1, 3, 3, 4], False, [1, 3, 4], False), - ([4, 3, 3, 1], True, [1, 3, 4], False), - ([4, 3, 3, 1], False, [4, 3, 1], False), - ]) + @pytest.mark.parametrize("ugroup", [uni.atoms, uni.residues, uni.segments]) + @pytest.mark.parametrize( + "ix, sort, unique_ix, is_same", + [ + ([1, 3, 4], True, [1, 3, 4], True), + ([1, 3, 4], False, [1, 3, 4], True), + ([4, 3, 1], True, [1, 3, 4], False), + ([4, 3, 1], False, [4, 3, 1], True), + ([1, 3, 3, 4], True, [1, 3, 4], False), + ([1, 3, 3, 4], False, [1, 3, 4], False), + ([4, 3, 3, 1], True, [1, 3, 4], False), + ([4, 3, 3, 1], False, [4, 3, 1], False), + ], + ) def test_group_asunique(self, ugroup, ix, sort, unique_ix, is_same): group = ugroup[ix] unique_group = group.asunique(sorted=sort) @@ -191,71 +193,91 @@ def test_group_asunique(self, ugroup, ix, sort, unique_ix, is_same): if is_same: assert unique_group is group - @pytest.mark.parametrize('ugroup', [uni.atoms, uni.residues, uni.segments]) + @pytest.mark.parametrize("ugroup", [uni.atoms, uni.residues, uni.segments]) def test_group_return_sorted_unsorted_unique(self, ugroup): unsorted_unique = ugroup[[1, 3, 4]].asunique(sorted=False) - assert 'unsorted_unique' in unsorted_unique._cache - assert 'sorted_unique' not in unsorted_unique._cache - assert 'issorted' not in unsorted_unique._cache - assert 'isunique' in unsorted_unique._cache + assert "unsorted_unique" in unsorted_unique._cache + assert "sorted_unique" not in unsorted_unique._cache + assert "issorted" not in unsorted_unique._cache + assert "isunique" in unsorted_unique._cache sorted_unique = unsorted_unique.asunique(sorted=True) assert sorted_unique is unsorted_unique - assert unsorted_unique._cache['issorted'] - assert unsorted_unique._cache['sorted_unique'] is unsorted_unique + assert unsorted_unique._cache["issorted"] + assert unsorted_unique._cache["sorted_unique"] is unsorted_unique - @pytest.mark.parametrize('ugroup', [uni.atoms, uni.residues, uni.segments]) + @pytest.mark.parametrize("ugroup", [uni.atoms, uni.residues, uni.segments]) def test_group_return_unsorted_sorted_unique(self, ugroup): unique = ugroup[[1, 3, 3, 4]] sorted_unique = unique.asunique(sorted=True) - assert unique._cache['sorted_unique'] is sorted_unique - assert 'unsorted_unique' not in unique._cache + assert unique._cache["sorted_unique"] is sorted_unique + assert "unsorted_unique" not in unique._cache unsorted_unique = unique.asunique(sorted=False) assert unsorted_unique is sorted_unique - assert unique._cache['unsorted_unique'] is sorted_unique + assert unique._cache["unsorted_unique"] is sorted_unique class TestEmptyAtomGroup(object): - """ Test empty atom groups - """ + """Test empty atom groups""" + u = mda.Universe(PSF, DCD) - @pytest.mark.parametrize('ag', [u.residues[:1]]) + @pytest.mark.parametrize("ag", [u.residues[:1]]) def test_passive_decorator(self, ag): - assert_almost_equal(ag.center_of_mass(), np.array([10.52567673, 9.49548312, -8.15335145])) + assert_almost_equal( + ag.center_of_mass(), + np.array([10.52567673, 9.49548312, -8.15335145]), + ) assert_almost_equal(ag.total_mass(), 133.209) - assert_almost_equal(ag.moment_of_inertia(), np.array([[ 657.514361 , 104.9446833, 110.4782 ], - [ 104.9446833, 307.4360346, -199.1794289], - [ 110.4782 , -199.1794289, 570.2924896]])) + assert_almost_equal( + ag.moment_of_inertia(), + np.array( + [ + [657.514361, 104.9446833, 110.4782], + [104.9446833, 307.4360346, -199.1794289], + [110.4782, -199.1794289, 570.2924896], + ] + ), + ) assert_almost_equal(ag.radius_of_gyration(), 2.400527938286) assert_almost_equal(ag.shape_parameter(), 0.61460819) assert_almost_equal(ag.asphericity(), 0.4892751412) - assert_almost_equal(ag.principal_axes(), np.array([[ 0.7574113, -0.113481 , 0.643001 ], - [ 0.5896252, 0.5419056, -0.5988993], - [-0.2804821, 0.8327427, 0.4773566]])) - assert_almost_equal(ag.center_of_charge(), np.array([11.0800112, 8.8885659, -8.9886632])) + assert_almost_equal( + ag.principal_axes(), + np.array( + [ + [0.7574113, -0.113481, 0.643001], + [0.5896252, 0.5419056, -0.5988993], + [-0.2804821, 0.8327427, 0.4773566], + ] + ), + ) + assert_almost_equal( + ag.center_of_charge(), + np.array([11.0800112, 8.8885659, -8.9886632]), + ) assert_almost_equal(ag.total_charge(), 1) - @pytest.mark.parametrize('ag', [mda.AtomGroup([],u)]) + @pytest.mark.parametrize("ag", [mda.AtomGroup([], u)]) def test_error_empty_group(self, ag): - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.center_of_mass() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.total_mass() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.moment_of_inertia() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.radius_of_gyration() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.shape_parameter() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.asphericity() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.principal_axes() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.center_of_charge() - with pytest.raises(ValueError, match ="AtomGroup is empty"): + with pytest.raises(ValueError, match="AtomGroup is empty"): ag.total_charge() @@ -266,18 +288,19 @@ class TestGroupSlicing(object): ---- TopologyGroup is technically called group, add this in too! """ + u = make_Universe() # test universe is 5:1 mapping 3 times group_dict = { - 'atom': u.atoms, - 'residue': u.residues, - 'segment': u.segments + "atom": u.atoms, + "residue": u.residues, + "segment": u.segments, } singulars = { - 'atom': groups.Atom, - 'residue': groups.Residue, - 'segment': groups.Segment + "atom": groups.Atom, + "residue": groups.Residue, + "segment": groups.Segment, } slices = ( slice(0, 10), @@ -288,11 +311,9 @@ class TestGroupSlicing(object): slice(5, 1, -1), slice(10, 0, -2), ) - length = {'atom': 125, - 'residue': 25, - 'segment': 5} + length = {"atom": 125, "residue": 25, "segment": 5} - levels = ('atom', 'residue', 'segment') + levels = ("atom", "residue", "segment") @pytest.fixture(params=levels) def level(self, request): @@ -323,7 +344,7 @@ def test_len(self, group, level): ref = self.length[level] assert len(group) == ref - @pytest.mark.parametrize('func', [list, np.array]) + @pytest.mark.parametrize("func", [list, np.array]) def test_boolean_slicing(self, group, func): # func is the container type that will be used to slice group = group[:5] @@ -341,15 +362,21 @@ def test_indexerror(self, group, level): with pytest.raises(IndexError): group.__getitem__(idx) - @pytest.mark.parametrize('sl,func', itertools.product(( - slice(0, 10), - slice(0, 2), - slice(1, 3), - slice(0, 2, 2), - slice(0, -1), - slice(5, 1, -1), - slice(10, 0, -2), - ), [list, lambda x: np.array(x, dtype=np.int64)])) + @pytest.mark.parametrize( + "sl,func", + itertools.product( + ( + slice(0, 10), + slice(0, 2), + slice(1, 3), + slice(0, 2, 2), + slice(0, -1), + slice(5, 1, -1), + slice(10, 0, -2), + ), + [list, lambda x: np.array(x, dtype=np.int64)], + ), + ) def test_slice(self, group, nparray, sl, func): """Check that slicing a np array is identical""" g2 = group[sl] @@ -363,14 +390,14 @@ def test_slice(self, group, nparray, sl, func): else: assert g not in g2 - @pytest.mark.parametrize('idx', [0, 1, -1, -2]) + @pytest.mark.parametrize("idx", [0, 1, -1, -2]) def test_integer_getitem(self, group, nparray, idx, singular): a = group[idx] ref = nparray[idx] assert a.ix == ref assert isinstance(a, singular) - + def test_none_getitem(self, group): with pytest.raises(TypeError): group[None] @@ -378,9 +405,11 @@ def test_none_getitem(self, group): def _yield_groups(group_dict, singles, levels, groupclasses, repeat): for level in levels: - for groups in itertools.product([group_dict[level], singles[level]], - repeat=repeat): - yield list(groups) + [groupclasses[level]] + for groups in itertools.product( + [group_dict[level], singles[level]], repeat=repeat + ): + yield list(groups) + [groupclasses[level]] + class TestGroupAddition(object): """Tests for combining Group objects @@ -397,30 +426,31 @@ class TestGroupAddition(object): Sum() should work on an iterable of many same level Components/Groups Groups contain items "x in y" """ + u = make_Universe() - levels = ['atom', 'residue', 'segment'] + levels = ["atom", "residue", "segment"] group_dict = { - 'atom': u.atoms[:5], - 'residue': u.residues[:5], - 'segment': u.segments[:5], + "atom": u.atoms[:5], + "residue": u.residues[:5], + "segment": u.segments[:5], } singles = { - 'atom': u.atoms[0], - 'residue': u.residues[0], - 'segment': u.segments[0], + "atom": u.atoms[0], + "residue": u.residues[0], + "segment": u.segments[0], } groupclasses = { - 'atom': groups.AtomGroup, - 'residue': groups.ResidueGroup, - 'segment': groups.SegmentGroup, + "atom": groups.AtomGroup, + "residue": groups.ResidueGroup, + "segment": groups.SegmentGroup, } # TODO: actually use this singleclasses = { - 'atom': groups.Atom, - 'residue': groups.Residue, - 'segment': groups.Segment + "atom": groups.Atom, + "residue": groups.Residue, + "segment": groups.Segment, } @pytest.fixture(params=levels) @@ -454,8 +484,8 @@ def itr(x): return x @pytest.mark.parametrize( - 'a, b, refclass', - _yield_groups(group_dict, singles, levels, groupclasses, repeat=2) + "a, b, refclass", + _yield_groups(group_dict, singles, levels, groupclasses, repeat=2), ) def test_addition(self, a, b, refclass): """Combine a and b, check length, returned type and ordering""" @@ -468,23 +498,25 @@ def test_addition(self, a, b, refclass): assert x == y @pytest.mark.parametrize( - 'a, b, c, refclass', - _yield_groups(group_dict, singles, levels, groupclasses, repeat=3) + "a, b, c, refclass", + _yield_groups(group_dict, singles, levels, groupclasses, repeat=3), ) def test_sum(self, a, b, c, refclass): # weird hack in radd allows this summed = sum([a, b, c]) assert isinstance(summed, refclass) - assert_equal(len(summed), - len(self.itr(a)) + len(self.itr(b)) + len(self.itr(c))) - for x, y in zip(summed, - itertools.chain(self.itr(a), self.itr(b), self.itr(c))): + assert_equal( + len(summed), len(self.itr(a)) + len(self.itr(b)) + len(self.itr(c)) + ) + for x, y in zip( + summed, itertools.chain(self.itr(a), self.itr(b), self.itr(c)) + ): assert x == y @pytest.mark.parametrize( - 'a, b, c, refclass', - _yield_groups(group_dict, singles, levels, groupclasses, repeat=3) + "a, b, c, refclass", + _yield_groups(group_dict, singles, levels, groupclasses, repeat=3), ) def test_bad_sum(self, a, b, c, refclass): # sum with bad first argument @@ -498,13 +530,12 @@ def test_contains_false(self, group): assert not group[3] in group[:2] @pytest.mark.parametrize( - 'one_level, other_level', + "one_level, other_level", [ (l1, l2) - for l1, l2 - in itertools.product(levels, repeat=2) + for l1, l2 in itertools.product(levels, repeat=2) if l1 != l2 - ] + ], ) def test_contains_wronglevel(self, one_level, other_level): group = self.group_dict[one_level] @@ -512,15 +543,14 @@ def test_contains_wronglevel(self, one_level, other_level): assert not group[2] in group2 @pytest.mark.parametrize( - 'a, b', + "a, b", [ (typeA[alevel], typeB[blevel]) - for (typeA, typeB), (alevel, blevel) - in itertools.product( + for (typeA, typeB), (alevel, blevel) in itertools.product( itertools.product([singles, group_dict], repeat=2), - itertools.permutations(levels, 2) + itertools.permutations(levels, 2), ) - ] + ], ) def test_crosslevel(self, a, b): with pytest.raises(TypeError): @@ -560,8 +590,8 @@ def test_atomgroup_to_residuegroup(self, u): assert isinstance(res, groups.ResidueGroup) assert res == u.residues assert res is not u.residues - assert res._cache['isunique'] is True - assert res._cache['sorted_unique'] is res + assert res._cache["isunique"] is True + assert res._cache["sorted_unique"] is res def test_atomgroup_to_segmentgroup(self, u): seg = u.atoms.segments @@ -569,8 +599,8 @@ def test_atomgroup_to_segmentgroup(self, u): assert isinstance(seg, groups.SegmentGroup) assert seg == u.segments assert seg is not u.segments - assert seg._cache['isunique'] is True - assert seg._cache['sorted_unique'] is seg + assert seg._cache["isunique"] is True + assert seg._cache["sorted_unique"] is seg def test_residuegroup_to_atomgroup(self, u): res = u.residues @@ -580,22 +610,22 @@ def test_residuegroup_to_atomgroup(self, u): assert atm == u.atoms assert atm is not u.atoms # clear res' uniqueness caches: - if 'sorted_unique' in res._cache.keys(): - del res._cache['sorted_unique'] - if 'isunique' in res._cache.keys(): - del res._cache['isunique'] + if "sorted_unique" in res._cache.keys(): + del res._cache["sorted_unique"] + if "isunique" in res._cache.keys(): + del res._cache["isunique"] atm = res.atoms # assert uniqueness caches of atm are empty: with pytest.raises(KeyError): - _ = atm._cache['isunique'] + _ = atm._cache["isunique"] with pytest.raises(KeyError): - _ = atm._cache['sorted_unique'] + _ = atm._cache["sorted_unique"] # populate uniqueness cache of res: assert res.isunique atm = res.atoms # assert uniqueness caches of atm are set: - assert atm._cache['isunique'] is True - assert atm._cache['unsorted_unique'] is atm + assert atm._cache["isunique"] is True + assert atm._cache["unsorted_unique"] is atm def test_residuegroup_to_residuegroup(self, u): res = u.residues.residues @@ -609,8 +639,8 @@ def test_residuegroup_to_segmentgroup(self, u): assert isinstance(seg, groups.SegmentGroup) assert seg == u.segments assert seg is not u.segments - assert seg._cache['isunique'] is True - assert seg._cache['sorted_unique'] is seg + assert seg._cache["isunique"] is True + assert seg._cache["sorted_unique"] is seg def test_segmentgroup_to_atomgroup(self, u): seg = u.segments @@ -620,22 +650,22 @@ def test_segmentgroup_to_atomgroup(self, u): assert atm == u.atoms assert atm is not u.atoms # clear seg's uniqueness caches: - if 'sorted_unique' in seg._cache.keys(): - del seg._cache['sorted_unique'] - if 'isunique' in seg._cache.keys(): - del seg._cache['isunique'] + if "sorted_unique" in seg._cache.keys(): + del seg._cache["sorted_unique"] + if "isunique" in seg._cache.keys(): + del seg._cache["isunique"] atm = seg.atoms # assert uniqueness caches of atm are empty: with pytest.raises(KeyError): - _ = atm._cache['isunique'] + _ = atm._cache["isunique"] with pytest.raises(KeyError): - _ = atm._cache['sorted_unique'] + _ = atm._cache["sorted_unique"] # populate uniqueness cache of seg: assert seg.isunique atm = seg.atoms # assert uniqueness caches of atm are set: - assert atm._cache['isunique'] is True - assert atm._cache['unsorted_unique'] is atm + assert atm._cache["isunique"] is True + assert atm._cache["unsorted_unique"] is atm def test_segmentgroup_to_residuegroup(self, u): seg = u.segments @@ -645,22 +675,22 @@ def test_segmentgroup_to_residuegroup(self, u): assert res == u.residues assert res is not u.residues # clear seg's uniqueness caches: - if 'sorted_unique' in seg._cache.keys(): - del seg._cache['sorted_unique'] - if 'isunique' in seg._cache.keys(): - del seg._cache['isunique'] + if "sorted_unique" in seg._cache.keys(): + del seg._cache["sorted_unique"] + if "isunique" in seg._cache.keys(): + del seg._cache["isunique"] res = seg.residues # assert uniqueness caches of res are empty: with pytest.raises(KeyError): - _ = res._cache['isunique'] + _ = res._cache["isunique"] with pytest.raises(KeyError): - _ = res._cache['sorted_unique'] + _ = res._cache["sorted_unique"] # populate uniqueness cache of seg: assert seg.isunique res = seg.residues # assert uniqueness caches of res are set: - assert res._cache['isunique'] is True - assert res._cache['unsorted_unique'] is res + assert res._cache["isunique"] is True + assert res._cache["unsorted_unique"] is res def test_segmentgroup_to_segmentgroup(self, u): seg = u.segments.segments @@ -680,10 +710,10 @@ def test_residue_to_atomgroup(self, u): ag = u.residues[0].atoms assert isinstance(ag, groups.AtomGroup) assert len(ag) == 5 - assert ag._cache['isunique'] is True - assert ag._cache['sorted_unique'] is ag - del ag._cache['sorted_unique'] - del ag._cache['isunique'] + assert ag._cache["isunique"] is True + assert ag._cache["sorted_unique"] is ag + del ag._cache["sorted_unique"] + del ag._cache["isunique"] assert ag.isunique def test_residue_to_segment(self, u): @@ -694,42 +724,42 @@ def test_segment_to_atomgroup(self, u): ag = u.segments[0].atoms assert isinstance(ag, groups.AtomGroup) assert len(ag) == 25 - assert ag._cache['isunique'] is True - assert ag._cache['sorted_unique'] is ag - del ag._cache['sorted_unique'] - del ag._cache['isunique'] + assert ag._cache["isunique"] is True + assert ag._cache["sorted_unique"] is ag + del ag._cache["sorted_unique"] + del ag._cache["isunique"] assert ag.isunique def test_segment_to_residuegroup(self, u): rg = u.segments[0].residues assert isinstance(rg, groups.ResidueGroup) assert len(rg) == 5 - assert rg._cache['isunique'] is True - assert rg._cache['sorted_unique'] is rg - del rg._cache['sorted_unique'] - del rg._cache['isunique'] + assert rg._cache["isunique"] is True + assert rg._cache["sorted_unique"] is rg + del rg._cache["sorted_unique"] + del rg._cache["isunique"] assert rg.isunique def test_atomgroup_to_residuegroup_unique(self, u): ag = u.atoms[:5] + u.atoms[10:15] + u.atoms[:5] rg = ag.residues assert len(rg) == 2 - assert rg._cache['isunique'] is True - assert rg._cache['sorted_unique'] is rg + assert rg._cache["isunique"] is True + assert rg._cache["sorted_unique"] is rg def test_atomgroup_to_segmentgroup_unique(self, u): ag = u.atoms[0] + u.atoms[-1] + u.atoms[0] sg = ag.segments assert len(sg) == 2 - assert sg._cache['isunique'] is True - assert sg._cache['sorted_unique'] is sg + assert sg._cache["isunique"] is True + assert sg._cache["sorted_unique"] is sg def test_residuegroup_to_segmentgroup_unique(self, u): rg = u.residues[0] + u.residues[6] + u.residues[1] sg = rg.segments assert len(sg) == 2 - assert sg._cache['isunique'] is True - assert sg._cache['sorted_unique'] is sg + assert sg._cache["isunique"] is True + assert sg._cache["sorted_unique"] is sg def test_residuegroup_to_atomgroup_listcomp(self, u): rg = u.residues[0] + u.residues[0] + u.residues[4] @@ -737,16 +767,16 @@ def test_residuegroup_to_atomgroup_listcomp(self, u): assert len(ag) == 15 # assert uniqueness caches of ag are empty: with pytest.raises(KeyError): - _ = ag._cache['isunique'] + _ = ag._cache["isunique"] with pytest.raises(KeyError): - _ = ag._cache['sorted_unique'] + _ = ag._cache["sorted_unique"] # populate uniqueness cache of rg: assert not rg.isunique ag = rg.atoms # ag uniqueness caches are now from residue - assert not ag._cache['isunique'] + assert not ag._cache["isunique"] with pytest.raises(KeyError): - _ = ag._cache['sorted_unique'] + _ = ag._cache["sorted_unique"] def test_segmentgroup_to_residuegroup_listcomp(self, u): sg = u.segments[0] + u.segments[0] + u.segments[1] @@ -754,16 +784,16 @@ def test_segmentgroup_to_residuegroup_listcomp(self, u): assert len(rg) == 15 # assert uniqueness caches of rg are empty: with pytest.raises(KeyError): - _ = rg._cache['isunique'] + _ = rg._cache["isunique"] with pytest.raises(KeyError): - _ = rg._cache['sorted_unique'] + _ = rg._cache["sorted_unique"] # populate uniqueness cache of sg: assert not sg.isunique rg = sg.residues # assert uniqueness caches of rg are now populated - assert not rg._cache['isunique'] + assert not rg._cache["isunique"] with pytest.raises(KeyError): - _ = rg._cache['sorted_unique'] + _ = rg._cache["sorted_unique"] def test_segmentgroup_to_atomgroup_listcomp(self, u): sg = u.segments[0] + u.segments[0] + u.segments[1] @@ -771,20 +801,21 @@ def test_segmentgroup_to_atomgroup_listcomp(self, u): assert len(ag) == 75 # assert uniqueness caches of ag are empty: with pytest.raises(KeyError): - _ = ag._cache['isunique'] + _ = ag._cache["isunique"] with pytest.raises(KeyError): - _ = ag._cache['sorted_unique'] + _ = ag._cache["sorted_unique"] # populate uniqueness cache of sg: assert not sg.isunique ag = sg.atoms # ag uniqueness caches are now from segment - assert not ag._cache['isunique'] + assert not ag._cache["isunique"] with pytest.raises(KeyError): - _ = ag._cache['sorted_unique'] + _ = ag._cache["sorted_unique"] class TestComponentComparisons(object): """Use of operators (< > == != <= >=) with Atom, Residue, and Segment""" + u = make_Universe() levels = [u.atoms, u.residues, u.segments] @@ -798,7 +829,7 @@ def a(self, abc): return abc[0] @pytest.fixture - def b (self, abc): + def b(self, abc): return abc[1] @pytest.fixture @@ -840,8 +871,8 @@ def test_sorting(self, a, b, c): assert sorted([b, a, c]) == [a, b, c] @pytest.mark.parametrize( - 'x, y', - itertools.permutations((u.atoms[0], u.residues[0], u.segments[0]), 2) + "x, y", + itertools.permutations((u.atoms[0], u.residues[0], u.segments[0]), 2), ) def test_crosslevel_cmp(self, x, y): with pytest.raises(TypeError): @@ -854,8 +885,8 @@ def test_crosslevel_cmp(self, x, y): operator.ge(x, y) @pytest.mark.parametrize( - 'x, y', - itertools.permutations((u.atoms[0], u.residues[0], u.segments[0]), 2) + "x, y", + itertools.permutations((u.atoms[0], u.residues[0], u.segments[0]), 2), ) def test_crosslevel_eq(self, x, y): with pytest.raises(TypeError): @@ -887,10 +918,10 @@ class TestGroupBy(object): # tests for the method 'groupby' @pytest.fixture() def u(self): - return make_Universe(('segids', 'charges', 'resids')) + return make_Universe(("segids", "charges", "resids")) def test_groupby_float(self, u): - gb = u.atoms.groupby('charges') + gb = u.atoms.groupby("charges") for ref in [-1.5, -0.5, 0.0, 0.5, 1.5]: assert ref in gb @@ -898,19 +929,19 @@ def test_groupby_float(self, u): assert all(g.charges == ref) assert len(g) == 25 - @pytest.mark.parametrize('string', ['segids', b'segids', u'segids']) + @pytest.mark.parametrize("string", ["segids", b"segids", "segids"]) def test_groupby_string(self, u, string): gb = u.atoms.groupby(string) assert len(gb) == 5 - for ref in ['SegA', 'SegB', 'SegC', 'SegD', 'SegE']: + for ref in ["SegA", "SegB", "SegC", "SegD", "SegE"]: assert ref in gb g = gb[ref] assert all(g.segids == ref) assert len(g) == 25 def test_groupby_int(self, u): - gb = u.atoms.groupby('resids') + gb = u.atoms.groupby("resids") for g in gb.values(): assert len(g) == 5 @@ -918,20 +949,20 @@ def test_groupby_int(self, u): # tests for multiple attributes as arguments def test_groupby_float_string(self, u): - gb = u.atoms.groupby(['charges', 'segids']) + gb = u.atoms.groupby(["charges", "segids"]) for ref in [-1.5, -0.5, 0.0, 0.5, 1.5]: - for subref in ['SegA','SegB','SegC','SegD','SegE']: + for subref in ["SegA", "SegB", "SegC", "SegD", "SegE"]: assert (ref, subref) in gb.keys() a = gb[(ref, subref)] assert len(a) == 5 assert all(a.charges == ref) - assert all(a.segids == subref) + assert all(a.segids == subref) def test_groupby_int_float(self, u): - gb = u.atoms.groupby(['resids', 'charges']) + gb = u.atoms.groupby(["resids", "charges"]) - uplim=int(len(gb)/5+1) + uplim = int(len(gb) / 5 + 1) for ref in range(1, uplim): for subref in [-1.5, -0.5, 0.0, 0.5, 1.5]: assert (ref, subref) in gb.keys() @@ -941,11 +972,11 @@ def test_groupby_int_float(self, u): assert all(a.charges == subref) def test_groupby_string_int(self, u): - gb = u.atoms.groupby(['segids', 'resids']) + gb = u.atoms.groupby(["segids", "resids"]) assert len(gb) == 25 res = 1 - for ref in ['SegA','SegB','SegC','SegD','SegE']: + for ref in ["SegA", "SegB", "SegC", "SegD", "SegE"]: for subref in range(0, 5): assert (ref, res) in gb.keys() a = gb[(ref, res)] @@ -961,51 +992,59 @@ def u(self): def test_atom_repr(self, u): at = u.atoms[0] - assert repr(at) == '' + assert ( + repr(at) + == "" + ) def test_residue_repr(self, u): res = u.residues[0] - assert repr(res) == '' + assert repr(res) == "" def test_segment_repr(self, u): seg = u.segments[0] - assert repr(seg) == '' + assert repr(seg) == "" def test_atomgroup_repr(self, u): ag = u.atoms[:10] - assert repr(ag) == '' + assert repr(ag) == "" def test_atomgroup_str_short(self, u): ag = u.atoms[:2] - assert str(ag) == ', ]>' + assert ( + str(ag) + == ", ]>" + ) def test_atomgroup_str_long(self, u): ag = u.atoms[:11] - assert str(ag).startswith(']>') + assert str(ag).startswith( + "]>") def test_residuegroup_repr(self, u): rg = u.residues[:10] - assert repr(rg) == '' + assert repr(rg) == "" def test_residuegroup_str_short(self, u): rg = u.residues[:2] - assert str(rg) == ', ]>' + assert str(rg) == ", ]>" def test_residuegroup_str_long(self, u): rg = u.residues[:11] - assert str(rg).startswith(',') - assert '...' in str(rg) - assert str(rg).endswith(', ]>') + assert str(rg).startswith(",") + assert "..." in str(rg) + assert str(rg).endswith(", ]>") def test_segmentgroup_repr(self, u): sg = u.segments[:10] - assert repr(sg) == '' + assert repr(sg) == "" def test_segmentgroup_str(self, u): sg = u.segments[:10] - assert str(sg) == ']>' + assert str(sg) == "]>" def _yield_mix(groups, components): @@ -1014,17 +1053,19 @@ def _yield_mix(groups, components): yield (groups[left], components[right]) yield (components[left], groups[right]) + def _yield_sliced_groups(u, slice_left, slice_right): - for level in ('atoms', 'residues', 'segments'): + for level in ("atoms", "residues", "segments"): yield (getattr(u, level)[slice_left], getattr(u, level)[slice_right]) + class TestGroupBaseOperators(object): u = make_Universe() components = (u.atoms[0], u.residues[0], u.segments[0]) component_groups = (u.atoms, u.residues, u.segments) - @pytest.fixture(params=('atoms', 'residues', 'segments')) + @pytest.fixture(params=("atoms", "residues", "segments")) def level(self, request): return request.param @@ -1066,10 +1107,12 @@ def groups_duplicated_and_scrambled(self, level): e = getattr(u, level)[[6, 5, 7, 7, 6]] return a, b, c, d, e - @pytest.fixture(params=('simple', 'scrambled')) + @pytest.fixture(params=("simple", "scrambled")) def groups(self, request, groups_simple, groups_duplicated_and_scrambled): - return {'simple': groups_simple, - 'scrambled': groups_duplicated_and_scrambled}[request.param] + return { + "simple": groups_simple, + "scrambled": groups_duplicated_and_scrambled, + }[request.param] def test_len(self, groups_simple): a, b, c, d, e = groups_simple @@ -1079,7 +1122,9 @@ def test_len(self, groups_simple): assert_equal(len(d), 0) assert_equal(len(e), 3) - def test_len_duplicated_and_scrambled(self, groups_duplicated_and_scrambled): + def test_len_duplicated_and_scrambled( + self, groups_duplicated_and_scrambled + ): a, b, c, d, e = groups_duplicated_and_scrambled assert_equal(len(a), 7) assert_equal(len(b), 8) @@ -1087,23 +1132,24 @@ def test_len_duplicated_and_scrambled(self, groups_duplicated_and_scrambled): assert_equal(len(d), 0) assert_equal(len(e), 5) - def test_equal(self, groups): a, b, c, d, e = groups assert a == a assert a != b assert not a == b - assert not a[0:1] == a[0], \ - 'Element should not equal single element group.' + assert ( + not a[0:1] == a[0] + ), "Element should not equal single element group." - @pytest.mark.parametrize('group', (u.atoms[:2], u.residues[:2], - u.segments[:2])) + @pytest.mark.parametrize( + "group", (u.atoms[:2], u.residues[:2], u.segments[:2]) + ) def test_copy(self, group): # make sure uniqueness caches of group are empty: with pytest.raises(KeyError): - _ = group._cache['isunique'] + _ = group._cache["isunique"] with pytest.raises(KeyError): - _ = group._cache['sorted_unique'] + _ = group._cache["sorted_unique"] # make a copy: cgroup = group.copy() # check if cgroup is an identical copy of group: @@ -1112,17 +1158,17 @@ def test_copy(self, group): assert cgroup == group # check if the copied group's uniqueness caches are empty: with pytest.raises(KeyError): - _ = cgroup._cache['isunique'] + _ = cgroup._cache["isunique"] with pytest.raises(KeyError): - _ = cgroup._cache['sorted_unique'] + _ = cgroup._cache["sorted_unique"] # populate group's uniqueness caches: assert group.isunique # make a copy: cgroup = group.copy() # check if the copied group's uniqueness caches are set correctly: - assert cgroup._cache['isunique'] is True + assert cgroup._cache["isunique"] is True # assert sorted_unique still doesn't exist - assert 'sorted_unique' not in cgroup._cache + assert "sorted_unique" not in cgroup._cache # add duplicate element to group: group += group[0] # populate group's uniqueness caches: @@ -1130,9 +1176,9 @@ def test_copy(self, group): # make a copy: cgroup = group.copy() # check if the copied group's uniqueness caches are set correctly: - assert cgroup._cache['isunique'] is False + assert cgroup._cache["isunique"] is False with pytest.raises(KeyError): - _ = cgroup._cache['sorted_unique'] + _ = cgroup._cache["sorted_unique"] # assert that duplicates are preserved: assert cgroup == group @@ -1167,16 +1213,16 @@ def test_is_strict_superset(self, groups): def test_concatenate(self, groups): a, b, c, d, e = groups cat_ab = a.concatenate(b) - assert cat_ab[:len(a)] == a - assert cat_ab[len(a):] == b + assert cat_ab[: len(a)] == a + assert cat_ab[len(a) :] == b cat_ba = b.concatenate(a) - assert cat_ba[:len(b)] == b - assert cat_ba[len(b):] == a + assert cat_ba[: len(b)] == b + assert cat_ba[len(b) :] == a cat_aa = a.concatenate(a) - assert cat_aa[:len(a)] == a - assert cat_aa[len(a):] == a + assert cat_aa[: len(a)] == a + assert cat_aa[len(a) :] == a cat_ad = a.concatenate(d) assert cat_ad == a @@ -1236,8 +1282,9 @@ def test_difference(self, groups): def test_symmetric_difference(self, groups): a, b, c, d, e = groups symdiff_ab = a.symmetric_difference(b) - assert_array_equal(symdiff_ab.ix, np.array(list(range(1, 3)) + - list(range(5, 8)))) + assert_array_equal( + symdiff_ab.ix, np.array(list(range(1, 3)) + list(range(5, 8))) + ) assert a.symmetric_difference(b) == b.symmetric_difference(a) assert_array_equal(a.symmetric_difference(e).ix, np.arange(1, 8)) @@ -1249,16 +1296,19 @@ def test_isdisjoint(self, groups): assert d.isdisjoint(a) assert not a.isdisjoint(b) - @pytest.mark.parametrize('left, right', itertools.chain( - # Do inter-levels pairs of groups fail as expected? - itertools.permutations(component_groups, 2), - # Do inter-levels pairs of components - itertools.permutations(components, 2), - # Do inter-levels pairs of components/groups fail as expected? - _yield_mix(component_groups, components), - # Does the function fail with inputs that are not components or groups - ((u.atoms, 'invalid'), ), - )) + @pytest.mark.parametrize( + "left, right", + itertools.chain( + # Do inter-levels pairs of groups fail as expected? + itertools.permutations(component_groups, 2), + # Do inter-levels pairs of components + itertools.permutations(components, 2), + # Do inter-levels pairs of components/groups fail as expected? + _yield_mix(component_groups, components), + # Does the function fail with inputs that are not components or groups + ((u.atoms, "invalid"),), + ), + ) def test_failing_pairs(self, left, right): def dummy(self, other): return True @@ -1266,15 +1316,18 @@ def dummy(self, other): with pytest.raises(TypeError): mda.core.groups._only_same_level(dummy)(left, right) - @pytest.mark.parametrize('left, right', itertools.chain( - # Groups - _yield_sliced_groups(u, slice(0, 2), slice(1, 3)), - # Components - _yield_sliced_groups(u, 0, 1), - # Mixed - _yield_sliced_groups(u, slice(0, 2), 1), - _yield_sliced_groups(u, 1, slice(0, 2)), - )) + @pytest.mark.parametrize( + "left, right", + itertools.chain( + # Groups + _yield_sliced_groups(u, slice(0, 2), slice(1, 3)), + # Components + _yield_sliced_groups(u, 0, 1), + # Mixed + _yield_sliced_groups(u, slice(0, 2), 1), + _yield_sliced_groups(u, 1, slice(0, 2)), + ), + ) def test_succeeding_pairs(self, left, right): def dummy(self, other): return True @@ -1291,11 +1344,16 @@ def dummy(self, other): with pytest.raises(ValueError): _only_same_level(dummy)(u.atoms, u2.atoms) - @pytest.mark.parametrize('op, method', ((operator.add, 'concatenate'), - (operator.sub, 'difference'), - (operator.and_, 'intersection'), - (operator.or_, 'union'), - (operator.xor, 'symmetric_difference'))) + @pytest.mark.parametrize( + "op, method", + ( + (operator.add, "concatenate"), + (operator.sub, "difference"), + (operator.and_, "intersection"), + (operator.or_, "union"), + (operator.xor, "symmetric_difference"), + ), + ) def test_shortcut_overriding(self, op, method, level): def check_operator(op, method, level): left = getattr(u, level)[1:3] @@ -1316,13 +1374,14 @@ class TestGroupHash(object): See issue #1397 """ - levels = ('atoms', 'residues', 'segments') + + levels = ("atoms", "residues", "segments") @pytest.fixture(params=levels) def level(self, request): return request.param - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u(self): return make_Universe(size=(3, 3, 3)) @@ -1340,8 +1399,9 @@ def test_hash_difference(self, u, level): b = getattr(u, level)[1:] assert hash(a) != hash(b) - @pytest.mark.parametrize('level_a, level_b', - itertools.permutations(levels, 2)) + @pytest.mark.parametrize( + "level_a, level_b", itertools.permutations(levels, 2) + ) def test_hash_difference_cross(self, u, level_a, level_b): a = getattr(u, level_a)[0:-1] b = getattr(u, level_b)[0:-1] @@ -1357,31 +1417,44 @@ def test_hash_diff_cross_universe(self, level, u): class TestAtomGroup(object): def test_PDB_atom_repr(self): - u = make_Universe(extras=('altLocs', 'names', 'types', 'resnames', 'resids', 'segids')) - assert_equal("", u.atoms[0].__repr__()) + u = make_Universe( + extras=( + "altLocs", + "names", + "types", + "resnames", + "resids", + "segids", + ) + ) + assert_equal( + "", + u.atoms[0].__repr__(), + ) @pytest.fixture() def attr_universe(): - return make_Universe(('names', 'resids', 'segids')) + return make_Universe(("names", "resids", "segids")) + class TestAttributeSetting(object): - @pytest.mark.parametrize('groupname', ['atoms', 'residues', 'segments']) + @pytest.mark.parametrize("groupname", ["atoms", "residues", "segments"]) def test_setting_group_fail(self, attr_universe, groupname): group = getattr(attr_universe, groupname) with pytest.raises(AttributeError): - group.this = 'that' + group.this = "that" - @pytest.mark.parametrize('groupname', ['atoms', 'residues', 'segments']) + @pytest.mark.parametrize("groupname", ["atoms", "residues", "segments"]) def test_setting_component_fails(self, attr_universe, groupname): component = getattr(attr_universe, groupname)[0] with pytest.raises(AttributeError): - component.this = 'that' + component.this = "that" - @pytest.mark.parametrize('attr', ['name', 'resid', 'segid']) - @pytest.mark.parametrize('groupname', ['atoms', 'residues', 'segments']) + @pytest.mark.parametrize("attr", ["name", "resid", "segid"]) + @pytest.mark.parametrize("groupname", ["atoms", "residues", "segments"]) def test_group_set_singular(self, attr_universe, attr, groupname): # this should fail as you can't set the 'name' of a 'ResidueGroup' group = getattr(attr_universe, groupname) @@ -1389,8 +1462,8 @@ def test_group_set_singular(self, attr_universe, attr, groupname): setattr(group, attr, 24) def test_atom_set_name(self, attr_universe): - attr_universe.atoms[0].name = 'this' - assert attr_universe.atoms[0].name == 'this' + attr_universe.atoms[0].name = "this" + assert attr_universe.atoms[0].name == "this" def test_atom_set_resid(self, attr_universe): with pytest.raises(NotImplementedError): @@ -1398,11 +1471,11 @@ def test_atom_set_resid(self, attr_universe): def test_atom_set_segid(self, attr_universe): with pytest.raises(NotImplementedError): - attr_universe.atoms[0].segid = 'this' + attr_universe.atoms[0].segid = "this" def test_residue_set_name(self, attr_universe): with pytest.raises(AttributeError): - attr_universe.residues[0].name = 'this' + attr_universe.residues[0].name = "this" def test_residue_set_resid(self, attr_universe): attr_universe.residues[0].resid = 24 @@ -1410,36 +1483,37 @@ def test_residue_set_resid(self, attr_universe): def test_residue_set_segid(self, attr_universe): with pytest.raises(NotImplementedError): - attr_universe.residues[0].segid = 'this' + attr_universe.residues[0].segid = "this" def test_segment_set_name(self, attr_universe): with pytest.raises(AttributeError): - attr_universe.segments[0].name = 'this' + attr_universe.segments[0].name = "this" def test_segment_set_resid(self, attr_universe): with pytest.raises(AttributeError): attr_universe.segments[0].resid = 24 def test_segment_set_segid(self, attr_universe): - attr_universe.segments[0].segid = 'this' - assert attr_universe.segments[0].segid == 'this' + attr_universe.segments[0].segid = "this" + assert attr_universe.segments[0].segid == "this" - @pytest.mark.parametrize('attr', ['names', 'resids', 'segids']) - @pytest.mark.parametrize('groupname', ['atoms', 'residues', 'segments']) + @pytest.mark.parametrize("attr", ["names", "resids", "segids"]) + @pytest.mark.parametrize("groupname", ["atoms", "residues", "segments"]) def test_component_set_plural(self, attr, groupname): # this should fail as you can't set the 'Names' of an 'Atom' - u = make_Universe(('names', 'resids', 'segids')) + u = make_Universe(("names", "resids", "segids")) group = getattr(u, groupname) comp = group[0] with pytest.raises(AttributeError): setattr(comp, attr, 24) + class TestAttributeGetting(object): @staticmethod @pytest.fixture() def universe(): - return make_Universe(extras=('masses', 'altLocs')) + return make_Universe(extras=("masses", "altLocs")) @staticmethod @pytest.fixture() @@ -1447,45 +1521,49 @@ def atoms(): u = make_Universe(extras=("masses",), size=(3, 1, 1)) return u.atoms - @pytest.mark.parametrize('attr', ['masses', 'altLocs']) + @pytest.mark.parametrize("attr", ["masses", "altLocs"]) def test_get_present_topattr_group(self, universe, attr): values = getattr(universe.atoms, attr) assert values is not None - @pytest.mark.parametrize('attr', ['mass', 'altLoc']) + @pytest.mark.parametrize("attr", ["mass", "altLoc"]) def test_get_present_topattr_component(self, universe, attr): value = getattr(universe.atoms[0], attr) assert value is not None - @pytest.mark.parametrize('attr,singular', [ - ('masses', 'mass'), - ('altLocs', 'altLoc')]) + @pytest.mark.parametrize( + "attr,singular", [("masses", "mass"), ("altLocs", "altLoc")] + ) def test_get_plural_topattr_from_component(self, universe, attr, singular): with pytest.raises(AttributeError) as exc: getattr(universe.atoms[0], attr) - assert ('Do you mean ' + singular) in str(exc.value) + assert ("Do you mean " + singular) in str(exc.value) - @pytest.mark.parametrize('attr,singular', [ - ('masses', 'mass'), - ('altLocs', 'altLoc')]) + @pytest.mark.parametrize( + "attr,singular", [("masses", "mass"), ("altLocs", "altLoc")] + ) def test_get_sing_topattr_from_group(self, universe, attr, singular): with pytest.raises(AttributeError) as exc: getattr(universe.atoms, singular) - assert ('Do you mean ' + attr) in str(exc.value) + assert ("Do you mean " + attr) in str(exc.value) - @pytest.mark.parametrize('attr,singular', [ - ('elements', 'element'), - ('tempfactors', 'tempfactor'), - ('bonds', 'bonds')]) + @pytest.mark.parametrize( + "attr,singular", + [ + ("elements", "element"), + ("tempfactors", "tempfactor"), + ("bonds", "bonds"), + ], + ) def test_get_absent_topattr_group(self, universe, attr, singular): with pytest.raises(NoDataError) as exc: getattr(universe.atoms, attr) - assert 'does not contain ' + singular in str(exc.value) + assert "does not contain " + singular in str(exc.value) def test_get_non_topattr(self, universe): with pytest.raises(AttributeError) as exc: universe.atoms.jabberwocky - assert 'has no attribute' in str(exc.value) + assert "has no attribute" in str(exc.value) def test_unwrap_without_bonds(self, universe): expected_message = ( @@ -1496,47 +1574,47 @@ def test_unwrap_without_bonds(self, universe): ) expected_message_pattern = re.escape(expected_message) with pytest.raises(NoDataError, match=expected_message_pattern): - universe.atoms.unwrap() + universe.atoms.unwrap() def test_get_absent_attr_method(self, universe): with pytest.raises(NoDataError) as exc: universe.atoms.total_charge() - err = ('AtomGroup.total_charge() not available; ' - 'this requires charges') + err = ( + "AtomGroup.total_charge() not available; " "this requires charges" + ) assert str(exc.value) == err def test_get_absent_attrprop(self, universe): with pytest.raises(NoDataError) as exc: universe.atoms.fragindices - err = ('AtomGroup.fragindices not available; ' - 'this requires bonds') + err = "AtomGroup.fragindices not available; " "this requires bonds" assert str(exc.value) == err def test_attrprop_wrong_group(self, universe): with pytest.raises(AttributeError) as exc: universe.atoms[0].fragindices - err = ('fragindices is a property of AtomGroup, not Atom') + err = "fragindices is a property of AtomGroup, not Atom" assert str(exc.value) == err def test_attrmethod_wrong_group(self, universe): with pytest.raises(AttributeError) as exc: universe.atoms[0].center_of_mass() - err = ('center_of_mass() is a method of AtomGroup, not Atom') + err = "center_of_mass() is a method of AtomGroup, not Atom" assert str(exc.value) == err - @pytest.mark.parametrize('attr', ['altlocs', 'alt_Locs']) + @pytest.mark.parametrize("attr", ["altlocs", "alt_Locs"]) def test_wrong_name(self, universe, attr): with pytest.raises(AttributeError) as exc: getattr(universe.atoms, attr) - err = ('AtomGroup has no attribute {}. ' - 'Did you mean altLocs?').format(attr) + err = ( + "AtomGroup has no attribute {}. " "Did you mean altLocs?" + ).format(attr) assert str(exc.value) == err + class TestInitGroup(object): @staticmethod - @pytest.fixture( - params=['atoms', 'residues', 'segments'] - ) + @pytest.fixture(params=["atoms", "residues", "segments"]) def components(request): # return list of Component and container class for all three levels u = make_Universe() @@ -1544,9 +1622,9 @@ def components(request): group = getattr(u, request.param) cls = { - 'atoms': mda.AtomGroup, - 'residues': mda.ResidueGroup, - 'segments': mda.SegmentGroup, + "atoms": mda.AtomGroup, + "residues": mda.ResidueGroup, + "segments": mda.SegmentGroup, }[request.param] yield (u, [group[0], group[2], group[4]], cls) @@ -1586,10 +1664,11 @@ class TestDecorator(object): def dummy_funtion(cls, compound="group", wrap=True, unwrap=True): return 0 - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('pbc', (True, False)) - @pytest.mark.parametrize('unwrap', (True, False)) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("pbc", (True, False)) + @pytest.mark.parametrize("unwrap", (True, False)) def test_wrap_and_unwrap_deprecation(self, compound, pbc, unwrap): if pbc and unwrap: @@ -1604,65 +1683,83 @@ def test_wrap_and_unwrap_deprecation(self, compound, pbc, unwrap): # We call a deprecated argument that does not appear in the # function's signature. This is done on purpose to test the # deprecation. We need to tell the linter. - # pylint: disable-next=unexpected-keyword-arg - assert self.dummy_funtion(compound=compound, pbc=pbc, unwrap=unwrap) == 0 + assert ( + # pylint: disable-next=unexpected-keyword-arg + self.dummy_funtion( + compound=compound, pbc=pbc, unwrap=unwrap + ) + == 0 + ) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('wrap', (True, False)) - @pytest.mark.parametrize('unwrap', (True, False)) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("wrap", (True, False)) + @pytest.mark.parametrize("unwrap", (True, False)) def test_wrap_and_unwrap(self, compound, wrap, unwrap): if wrap and unwrap: with pytest.raises(ValueError): self.dummy_funtion(compound=compound, wrap=wrap, unwrap=unwrap) else: - assert self.dummy_funtion(compound=compound, wrap=wrap, unwrap=unwrap) == 0 + assert ( + self.dummy_funtion(compound=compound, wrap=wrap, unwrap=unwrap) + == 0 + ) @pytest.fixture() def tpr(): with warnings.catch_warnings(): - warnings.filterwarnings("ignore", - message="No coordinate reader found") + warnings.filterwarnings("ignore", message="No coordinate reader found") return mda.Universe(TPR) + class TestGetConnectionsAtoms(object): """Test Atom and AtomGroup.get_connections""" - @pytest.mark.parametrize("typename", - ["bonds", "angles", "dihedrals", "impropers"]) + @pytest.mark.parametrize( + "typename", ["bonds", "angles", "dihedrals", "impropers"] + ) def test_connection_from_atom_not_outside(self, tpr, typename): cxns = tpr.atoms[1].get_connections(typename, outside=False) assert len(cxns) == 0 - @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 1), - ("angles", 3), - ("dihedrals", 4), - ]) + @pytest.mark.parametrize( + "typename, n_atoms", + [ + ("bonds", 1), + ("angles", 3), + ("dihedrals", 4), + ], + ) def test_connection_from_atom_outside(self, tpr, typename, n_atoms): cxns = tpr.atoms[10].get_connections(typename, outside=True) assert len(cxns) == n_atoms - @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 9), - ("angles", 15), - ("dihedrals", 12), - ]) - def test_connection_from_atoms_not_outside(self, tpr, typename, - n_atoms): + @pytest.mark.parametrize( + "typename, n_atoms", + [ + ("bonds", 9), + ("angles", 15), + ("dihedrals", 12), + ], + ) + def test_connection_from_atoms_not_outside(self, tpr, typename, n_atoms): ag = tpr.atoms[:10] cxns = ag.get_connections(typename, outside=False) assert len(cxns) == n_atoms indices = np.ravel(cxns.to_indices()) assert np.all(np.isin(indices, ag.indices)) - @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 13), - ("angles", 27), - ("dihedrals", 38), - ]) + @pytest.mark.parametrize( + "typename, n_atoms", + [ + ("bonds", 13), + ("angles", 27), + ("dihedrals", 38), + ], + ) def test_connection_from_atoms_outside(self, tpr, typename, n_atoms): ag = tpr.atoms[:10] cxns = ag.get_connections(typename, outside=True) @@ -1687,44 +1784,57 @@ def test_get_empty_group(self, tpr, outside): class TestGetConnectionsResidues(object): """Test Residue and ResidueGroup.get_connections""" - @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 9), - ("angles", 14), - ("dihedrals", 9), - ("impropers", 0), - ]) + @pytest.mark.parametrize( + "typename, n_atoms", + [ + ("bonds", 9), + ("angles", 14), + ("dihedrals", 9), + ("impropers", 0), + ], + ) def test_connection_from_res_not_outside(self, tpr, typename, n_atoms): cxns = tpr.residues[10].get_connections(typename, outside=False) assert len(cxns) == n_atoms - @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 11), - ("angles", 22), - ("dihedrals", 27), - ("impropers", 0), - ]) + @pytest.mark.parametrize( + "typename, n_atoms", + [ + ("bonds", 11), + ("angles", 22), + ("dihedrals", 27), + ("impropers", 0), + ], + ) def test_connection_from_res_outside(self, tpr, typename, n_atoms): cxns = tpr.residues[10].get_connections(typename, outside=True) assert len(cxns) == n_atoms - @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 157), - ("angles", 290), - ("dihedrals", 351), - ]) - def test_connection_from_residues_not_outside(self, tpr, typename, - n_atoms): + @pytest.mark.parametrize( + "typename, n_atoms", + [ + ("bonds", 157), + ("angles", 290), + ("dihedrals", 351), + ], + ) + def test_connection_from_residues_not_outside( + self, tpr, typename, n_atoms + ): ag = tpr.residues[:10] cxns = ag.get_connections(typename, outside=False) assert len(cxns) == n_atoms indices = np.ravel(cxns.to_indices()) assert np.all(np.isin(indices, ag.atoms.indices)) - @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 158), - ("angles", 294), - ("dihedrals", 360), - ]) + @pytest.mark.parametrize( + "typename, n_atoms", + [ + ("bonds", 158), + ("angles", 294), + ("dihedrals", 360), + ], + ) def test_connection_from_residues_outside(self, tpr, typename, n_atoms): ag = tpr.residues[:10] cxns = ag.get_connections(typename, outside=True) @@ -1746,11 +1856,14 @@ def test_get_empty_group(self, tpr, outside): assert len(cxns) == 0 -@pytest.mark.parametrize("typename, n_inside", [ - ("intra_bonds", 9), - ("intra_angles", 15), - ("intra_dihedrals", 12), -]) +@pytest.mark.parametrize( + "typename, n_inside", + [ + ("intra_bonds", 9), + ("intra_angles", 15), + ("intra_dihedrals", 12), + ], +) def test_topologygroup_gets_connections_inside(tpr, typename, n_inside): ag = tpr.atoms[:10] cxns = getattr(ag, typename) @@ -1759,11 +1872,14 @@ def test_topologygroup_gets_connections_inside(tpr, typename, n_inside): assert np.all(np.isin(indices, ag.indices)) -@pytest.mark.parametrize("typename, n_outside", [ - ("bonds", 13), - ("angles", 27), - ("dihedrals", 38), -]) +@pytest.mark.parametrize( + "typename, n_outside", + [ + ("bonds", 13), + ("angles", 27), + ("dihedrals", 38), + ], +) def test_topologygroup_gets_connections_outside(tpr, typename, n_outside): ag = tpr.atoms[:10] cxns = getattr(ag, typename) diff --git a/testsuite/MDAnalysisTests/core/test_index_dtype.py b/testsuite/MDAnalysisTests/core/test_index_dtype.py index b9cb0f43a09..1500430f69f 100644 --- a/testsuite/MDAnalysisTests/core/test_index_dtype.py +++ b/testsuite/MDAnalysisTests/core/test_index_dtype.py @@ -75,8 +75,7 @@ def test_atomgroup_segment_upshift(u): def test_residuegroup_atom_downshift(u): # downshift arrays are a list (one for each residue) - assert all((arr.dtype == np.intp) - for arr in u.residues.indices) + assert all((arr.dtype == np.intp) for arr in u.residues.indices) def test_residuegroup_resindices(u): @@ -88,13 +87,11 @@ def test_residuegroup_segment_upshift(u): def test_segmentgroup_atom_downshift(u): - assert all((arr.dtype == np.intp) - for arr in u.segments.indices) + assert all((arr.dtype == np.intp) for arr in u.segments.indices) def test_segmentgroup_residue_downshift(u): - assert all((arr.dtype == np.intp) - for arr in u.segments.resindices) + assert all((arr.dtype == np.intp) for arr in u.segments.resindices) def test_segmentgroup_segindices(u): diff --git a/testsuite/MDAnalysisTests/core/test_requires.py b/testsuite/MDAnalysisTests/core/test_requires.py index d76a1ea8051..1dea033b7eb 100644 --- a/testsuite/MDAnalysisTests/core/test_requires.py +++ b/testsuite/MDAnalysisTests/core/test_requires.py @@ -1,6 +1,7 @@ """Tests for core.groups.requires decorator """ + import numpy as np import pytest @@ -12,31 +13,29 @@ class TestRequires(object): def test_requires_failure_singular(self): - @requires('masses') + @requires("masses") def mass_multiplier(ag1, ag2, scalar): return (ag1.masses + ag2.masses) * scalar - u = make_Universe(('charges',)) + u = make_Universe(("charges",)) with pytest.raises(NoDataError): mass_multiplier(u.atoms[:10], u.atoms[20:30], 4.0) - def test_requires_failure_multiple(self): - @requires('masses', 'charges') + @requires("masses", "charges") def mass_multiplier(ag1, ag2, scalar): return (ag1.masses + ag2.charges) * scalar - - u = make_Universe(('masses', 'types')) + u = make_Universe(("masses", "types")) with pytest.raises(NoDataError): mass_multiplier(u.atoms[:10], u.atoms[20:30], 4.0) def test_requires_success(self): - @requires('masses') + @requires("masses") def mass_multiplier(ag1, ag2, scalar): return (ag1.masses + ag2.masses) * scalar - u = make_Universe(('masses',)) + u = make_Universe(("masses",)) result = mass_multiplier(u.atoms[:10], u.atoms[20:30], 4.0) @@ -45,7 +44,7 @@ def mass_multiplier(ag1, ag2, scalar): def test_failure_errormessage(self): # failures should list all required attributes not # just the first one - @requires('cats', 'dogs', 'frogs') + @requires("cats", "dogs", "frogs") def animal_print(ag): return len(ag.cats), len(ag.dogs), len(ag.frogs) @@ -55,9 +54,9 @@ def animal_print(ag): except NoDataError as e: message = e.args[0] # Test function name gets returned (for debug) - assert 'animal_print' in message - assert 'cats' in message - assert 'dogs' in message - assert 'frogs' in message + assert "animal_print" in message + assert "cats" in message + assert "dogs" in message + assert "frogs" in message else: pytest.fail(msg="Should raise NoDataError") diff --git a/testsuite/MDAnalysisTests/core/test_residue.py b/testsuite/MDAnalysisTests/core/test_residue.py index b2bf9429105..a016673c242 100644 --- a/testsuite/MDAnalysisTests/core/test_residue.py +++ b/testsuite/MDAnalysisTests/core/test_residue.py @@ -52,8 +52,7 @@ def test_index(res): def test_atom_order(res): - assert_equal(res.atoms.indices, - sorted(res.atoms.indices)) + assert_equal(res.atoms.indices, sorted(res.atoms.indices)) def test_residue_pickle(res): diff --git a/testsuite/MDAnalysisTests/core/test_residuegroup.py b/testsuite/MDAnalysisTests/core/test_residuegroup.py index ad5521d20a1..06d0e22e9a0 100644 --- a/testsuite/MDAnalysisTests/core/test_residuegroup.py +++ b/testsuite/MDAnalysisTests/core/test_residuegroup.py @@ -32,13 +32,13 @@ @pytest.mark.skipif(HAS_BIOPYTHON, reason="biopython is installed") def test_sequence_import_error(): - p = mda.Universe(PSF, DCD).select_atoms('protein') + p = mda.Universe(PSF, DCD).select_atoms("protein") errmsg = "The `sequence_alignment` method requires an installation" with pytest.raises(ImportError, match=errmsg): _ = p.residues.sequence(format="string") -@pytest.mark.skipif(not HAS_BIOPYTHON, reason='requires biopython') +@pytest.mark.skipif(not HAS_BIOPYTHON, reason="requires biopython") class TestSequence: # all tests are done with the AdK system (PSF and DCD) sequence: # http://www.uniprot.org/uniprot/P69441.fasta @@ -56,21 +56,28 @@ def u(self): def test_string(self, u): p = u.select_atoms("protein") - assert_equal(p.residues.sequence(format="string"), - self.ref_adk_sequence) + assert_equal( + p.residues.sequence(format="string"), self.ref_adk_sequence + ) def test_SeqRecord(self, u): p = u.select_atoms("protein") - s = p.residues.sequence(format="SeqRecord", - id="P69441", name="KAD_ECOLI Adenylate kinase", - description="EcAdK from pdb 4AKE") + s = p.residues.sequence( + format="SeqRecord", + id="P69441", + name="KAD_ECOLI Adenylate kinase", + description="EcAdK from pdb 4AKE", + ) assert_equal(s.id, "P69441") assert_equal(str(s.seq), self.ref_adk_sequence) def test_SeqRecord_default(self, u): p = u.select_atoms("protein") - s = p.residues.sequence(id="P69441", name="KAD_ECOLI Adenylate kinase", - description="EcAdK from pdb 4AKE") + s = p.residues.sequence( + id="P69441", + name="KAD_ECOLI Adenylate kinase", + description="EcAdK from pdb 4AKE", + ) assert_equal(s.id, "P69441") assert_equal(str(s.seq), self.ref_adk_sequence) @@ -94,7 +101,7 @@ def wrong_res(): def test_format_TE(self, u): with pytest.raises(TypeError): - u.residues.sequence(format='chicken') + u.residues.sequence(format="chicken") class TestResidueGroup(object): @@ -112,8 +119,9 @@ def test_newResidueGroup(self, universe): (Issue 135)""" rg = universe.atoms.residues newrg = rg[10:20:2] - assert isinstance(newrg, mda.core.groups.ResidueGroup), \ - "Failed to make a new ResidueGroup: type mismatch" + assert isinstance( + newrg, mda.core.groups.ResidueGroup + ), "Failed to make a new ResidueGroup: type mismatch" def test_n_atoms(self, rg): assert_equal(rg.n_atoms, 3341) @@ -132,8 +140,7 @@ def test_segids_dim(self, rg): def test_len(self, rg): """testing that len(residuegroup) == residuegroup.n_residues""" - assert_equal(len(rg), rg.n_residues, - "len and n_residues disagree") + assert_equal(len(rg), rg.n_residues, "len and n_residues disagree") def test_set_resids(self, universe): rg = universe.select_atoms("bynum 12:42").residues @@ -141,12 +148,18 @@ def test_set_resids(self, universe): rg.resids = resid # check individual atoms for at in rg.atoms: - assert_equal(at.resid, resid, - err_msg="failed to set_resid atoms 12:42 to same resid") + assert_equal( + at.resid, + resid, + err_msg="failed to set_resid atoms 12:42 to same resid", + ) # check residues - assert_equal(rg.resids, resid * np.ones(rg.n_residues), - err_msg="failed to set_resid of residues belonging to " - "atoms 12:42 to same resid") + assert_equal( + rg.resids, + resid * np.ones(rg.n_residues), + err_msg="failed to set_resid of residues belonging to " + "atoms 12:42 to same resid", + ) def test_set_resids(self, universe): """test_set_resid: set ResidueGroup resids on a per-residue basis""" @@ -156,23 +169,30 @@ def test_set_resids(self, universe): # check individual atoms for r, resid in zip(rg, resids): for at in r.atoms: - assert_equal(at.resid, resid, - err_msg="failed to set_resid residues 10:18 to same " - "resid in residue {0}\n" - "(resids = {1}\nresidues = {2})".format(r, - resids, - rg)) - assert_equal(rg.resids, resids, - err_msg="failed to set_resid of residues belonging to " - "residues 10:18 to new resids") + assert_equal( + at.resid, + resid, + err_msg="failed to set_resid residues 10:18 to same " + "resid in residue {0}\n" + "(resids = {1}\nresidues = {2})".format(r, resids, rg), + ) + assert_equal( + rg.resids, + resids, + err_msg="failed to set_resid of residues belonging to " + "residues 10:18 to new resids", + ) def test_set_resids_updates_self(self, universe): rg = universe.select_atoms("resid 10:18").residues resids = np.array(rg.resids) + 1000 rg.resids = resids - assert_equal(rg.resids, resids, - err_msg="old selection was not changed in place " - "after set_resid") + assert_equal( + rg.resids, + resids, + err_msg="old selection was not changed in place " + "after set_resid", + ) def test_set_resnum_single(self, universe): rg = universe.residues[:3] @@ -196,13 +216,13 @@ def test_set_resnum_ValueError(self, universe): rg = universe.residues[:3] new = [22, 23, 24, 25] with pytest.raises(ValueError): - setattr(rg, 'resnums', new) + setattr(rg, "resnums", new) # INVALID: no `set_resnames` method; use `resnames` property directly @pytest.mark.skip def test_set_resname_single(self, universe): rg = universe.residues[:3] - new = 'newname' + new = "newname" rg.set_resnames(new) assert_equal(all(rg.resnames == new), True) @@ -213,7 +233,7 @@ def test_set_resname_single(self, universe): @pytest.mark.skip def test_set_resname_many(self, universe): rg = universe.residues[:3] - new = ['a', 'b', 'c'] + new = ["a", "b", "c"] rg.set_resnames(new) assert_equal(all(rg.resnames == new), True) @@ -224,7 +244,7 @@ def test_set_resname_many(self, universe): @pytest.mark.skip def test_set_resname_ValueError(self, universe): rg = universe.residues[:3] - new = ['a', 'b', 'c', 'd'] + new = ["a", "b", "c", "d"] with pytest.raises(ValueError): rg.set_resnames(new) @@ -241,21 +261,31 @@ def test_merge_residues(self, universe): nres_new = universe.atoms.n_residues r_merged = universe.select_atoms("resid 12:14").residues natoms_new = universe.select_atoms("resid 12").n_atoms - assert_equal(len(r_merged), 1, err_msg="set_resid failed to merge " - "residues: merged = {0}".format( - r_merged)) - assert_equal(nres_new, nres_old - 2, - err_msg="set_resid failed to merge residues: " - "merged = {0}".format(r_merged)) - assert_equal(natoms_new, natoms_old, err_msg="set_resid lost atoms " - "on merge".format( - r_merged)) - - assert_equal(universe.residues.n_residues, - universe.atoms.n_residues, - err_msg="Universe.residues and Universe.atoms.n_residues " - "do not agree after residue " - "merge.") + assert_equal( + len(r_merged), + 1, + err_msg="set_resid failed to merge " + "residues: merged = {0}".format(r_merged), + ) + assert_equal( + nres_new, + nres_old - 2, + err_msg="set_resid failed to merge residues: " + "merged = {0}".format(r_merged), + ) + assert_equal( + natoms_new, + natoms_old, + err_msg="set_resid lost atoms " "on merge".format(r_merged), + ) + + assert_equal( + universe.residues.n_residues, + universe.atoms.n_residues, + err_msg="Universe.residues and Universe.atoms.n_residues " + "do not agree after residue " + "merge.", + ) # INVALID: no `set_masses` method; use `masses` property directly @pytest.mark.skip @@ -264,20 +294,25 @@ def test_set_masses(self, universe): mass = 2.0 rg.set_masses(mass) # check individual atoms - assert_equal([a.mass for a in rg.atoms], - mass * np.ones(rg.n_atoms), - err_msg="failed to set_mass H* atoms in resid 12:42 to {0}".format( - mass)) + assert_equal( + [a.mass for a in rg.atoms], + mass * np.ones(rg.n_atoms), + err_msg="failed to set_mass H* atoms in resid 12:42 to {0}".format( + mass + ), + ) # VALID def test_atom_order(self, universe): - assert_equal(universe.residues.atoms.indices, - sorted(universe.residues.atoms.indices)) + assert_equal( + universe.residues.atoms.indices, + sorted(universe.residues.atoms.indices), + ) def test_get_next_residue(self, rg): unsorted_rep_res = rg[[0, 1, 8, 3, 4, 0, 3, 1, -1]] next_res = unsorted_rep_res._get_next_residues_by_resid() - resids = list(unsorted_rep_res.resids+1) + resids = list(unsorted_rep_res.resids + 1) resids[-1] = None next_resids = [r.resid if r is not None else None for r in next_res] assert_equal(len(next_res), len(unsorted_rep_res)) @@ -286,7 +321,7 @@ def test_get_next_residue(self, rg): def test_get_prev_residue(self, rg): unsorted_rep_res = rg[[0, 1, 8, 3, 4, 0, 3, 1, -1]] prev_res = unsorted_rep_res._get_prev_residues_by_resid() - resids = list(unsorted_rep_res.resids-1) + resids = list(unsorted_rep_res.resids - 1) resids[0] = resids[5] = None prev_resids = [r.resid if r is not None else None for r in prev_res] assert_equal(len(prev_res), len(unsorted_rep_res)) diff --git a/testsuite/MDAnalysisTests/core/test_segment.py b/testsuite/MDAnalysisTests/core/test_segment.py index 60b167dd882..4b9fe5639b5 100644 --- a/testsuite/MDAnalysisTests/core/test_segment.py +++ b/testsuite/MDAnalysisTests/core/test_segment.py @@ -35,7 +35,7 @@ class TestSegment(object): @pytest.fixture() def universe(self): - return make_Universe(('segids',)) + return make_Universe(("segids",)) @pytest.fixture() def sB(self, universe): @@ -61,8 +61,10 @@ def test_advanced_slicing(self, sB): assert isinstance(res, mda.core.groups.ResidueGroup) def test_atom_order(self, universe): - assert_equal(universe.segments[0].atoms.indices, - sorted(universe.segments[0].atoms.indices)) + assert_equal( + universe.segments[0].atoms.indices, + sorted(universe.segments[0].atoms.indices), + ) @pytest.mark.parametrize("ix", (1, -1)) def test_residue_pickle(self, universe, ix): diff --git a/testsuite/MDAnalysisTests/core/test_segmentgroup.py b/testsuite/MDAnalysisTests/core/test_segmentgroup.py index 11841bb7797..f6f2ed82d79 100644 --- a/testsuite/MDAnalysisTests/core/test_segmentgroup.py +++ b/testsuite/MDAnalysisTests/core/test_segmentgroup.py @@ -39,6 +39,7 @@ def universe(): def g(universe): return universe.atoms.segments + def test_newSegmentGroup(universe): """test that slicing a SegmentGroup returns a new SegmentGroup (Issue 135)""" g = universe.atoms.segments @@ -74,40 +75,53 @@ def test_segids_dim(g): def test_set_segids(universe): - s = universe.select_atoms('all').segments - s.segids = 'ADK' - assert_equal(universe.segments.segids, ['ADK'], - err_msg="failed to set_segid on segments") + s = universe.select_atoms("all").segments + s.segids = "ADK" + assert_equal( + universe.segments.segids, + ["ADK"], + err_msg="failed to set_segid on segments", + ) def test_set_segid_updates_(universe): g = universe.select_atoms("resid 10:18").segments - g.segids = 'ADK' - assert_equal(g.segids, ['ADK'], - err_msg="old selection was not changed in place after set_segid") + g.segids = "ADK" + assert_equal( + g.segids, + ["ADK"], + err_msg="old selection was not changed in place after set_segid", + ) def test_set_segids_many(): - u = mda.Universe.empty(n_atoms=6, n_residues=2, n_segments=2, - atom_resindex=[0, 0, 0, 1, 1, 1], residue_segindex=[0,1]) - u.add_TopologyAttr('segids', ['A', 'B']) + u = mda.Universe.empty( + n_atoms=6, + n_residues=2, + n_segments=2, + atom_resindex=[0, 0, 0, 1, 1, 1], + residue_segindex=[0, 1], + ) + u.add_TopologyAttr("segids", ["A", "B"]) # universe with 2 segments, A and B - u.segments.segids = ['X', 'Y'] + u.segments.segids = ["X", "Y"] - assert u.segments[0].segid == 'X' - assert u.segments[1].segid == 'Y' + assert u.segments[0].segid == "X" + assert u.segments[1].segid == "Y" - assert len(u.select_atoms('segid A')) == 0 - assert len(u.select_atoms('segid B')) == 0 - assert len(u.select_atoms('segid X')) == 3 - assert len(u.select_atoms('segid Y')) == 3 + assert len(u.select_atoms("segid A")) == 0 + assert len(u.select_atoms("segid B")) == 0 + assert len(u.select_atoms("segid X")) == 3 + assert len(u.select_atoms("segid Y")) == 3 def test_atom_order(universe): - assert_equal(universe.segments.atoms.indices, - sorted(universe.segments.atoms.indices)) + assert_equal( + universe.segments.atoms.indices, + sorted(universe.segments.atoms.indices), + ) def test_segmentgroup_pickle(): diff --git a/testsuite/MDAnalysisTests/core/test_topology.py b/testsuite/MDAnalysisTests/core/test_topology.py index 4ed660b25be..d1154f5c4e0 100644 --- a/testsuite/MDAnalysisTests/core/test_topology.py +++ b/testsuite/MDAnalysisTests/core/test_topology.py @@ -3,6 +3,7 @@ Should convert between indices (*ix) Should work with both a single or an array of indices """ + import itertools from numpy.testing import ( assert_equal, @@ -38,30 +39,22 @@ def tt(self): def test_a2r(self, tt): for aix, rix in zip( - [np.array([0, 1, 2]), - np.array([9, 6, 2]), - np.array([3, 3, 3])], - [np.array([0, 0, 2]), - np.array([2, 3, 2]), - np.array([2, 2, 2])] + [np.array([0, 1, 2]), np.array([9, 6, 2]), np.array([3, 3, 3])], + [np.array([0, 0, 2]), np.array([2, 3, 2]), np.array([2, 2, 2])], ): assert_equal(tt.atoms2residues(aix), rix) def test_r2a_1d(self, tt): for rix, aix in zip( - [[0, 1], [1, 1], [3, 1]], - [[0, 1, 4, 5, 8], [4, 5, 8, 4, 5, 8], [6, 7, 4, 5, 8]] + [[0, 1], [1, 1], [3, 1]], + [[0, 1, 4, 5, 8], [4, 5, 8, 4, 5, 8], [6, 7, 4, 5, 8]], ): assert_equal(tt.residues2atoms_1d(rix), aix) def test_r2a_2d(self, tt): for rix, aix in zip( - [[0, 1], - [1, 1], - [3, 1]], - [[[0, 1], [4, 5, 8]], - [[4, 5, 8], [4, 5, 8]], - [[6, 7], [4, 5, 8]]] + [[0, 1], [1, 1], [3, 1]], + [[[0, 1], [4, 5, 8]], [[4, 5, 8], [4, 5, 8]], [[6, 7], [4, 5, 8]]], ): answer = tt.residues2atoms_2d(rix) for a1, a2 in zip(answer, aix): @@ -69,34 +62,22 @@ def test_r2a_2d(self, tt): def test_r2s(self, tt): for rix, sidx in zip( - [np.array([0, 1]), - np.array([2, 1, 0]), - np.array([1, 1, 1])], - [np.array([0, 1]), - np.array([1, 1, 0]), - np.array([1, 1, 1])] + [np.array([0, 1]), np.array([2, 1, 0]), np.array([1, 1, 1])], + [np.array([0, 1]), np.array([1, 1, 0]), np.array([1, 1, 1])], ): assert_equal(tt.residues2segments(rix), sidx) def test_s2r_1d(self, tt): for sidx, rix in zip( - [[0, 1], - [1, 0], - [1, 1]], - [[0, 3, 1, 2], - [1, 2, 0, 3], - [1, 2, 1, 2]] + [[0, 1], [1, 0], [1, 1]], + [[0, 3, 1, 2], [1, 2, 0, 3], [1, 2, 1, 2]], ): assert_equal(tt.segments2residues_1d(sidx), rix) def test_s2r_2d(self, tt): for sidx, rix in zip( - [[0, 1], - [1, 0], - [1, 1]], - [[[0, 3], [1, 2]], - [[1, 2], [0, 3]], - [[1, 2], [1, 2]]] + [[0, 1], [1, 0], [1, 1]], + [[[0, 3], [1, 2]], [[1, 2], [0, 3]], [[1, 2], [1, 2]]], ): answer = tt.segments2residues_2d(sidx) for a1, a2 in zip(answer, rix): @@ -104,23 +85,23 @@ def test_s2r_2d(self, tt): def test_s2a_1d(self, tt): for sidx, aix in zip( - [[0, 1], - [1, 0], - [1, 1]], - [[0, 1, 6, 7, 4, 5, 8, 2, 3, 9], - [4, 5, 8, 2, 3, 9, 0, 1, 6, 7], - [4, 5, 8, 2, 3, 9, 4, 5, 8, 2, 3, 9]], + [[0, 1], [1, 0], [1, 1]], + [ + [0, 1, 6, 7, 4, 5, 8, 2, 3, 9], + [4, 5, 8, 2, 3, 9, 0, 1, 6, 7], + [4, 5, 8, 2, 3, 9, 4, 5, 8, 2, 3, 9], + ], ): assert_equal(tt.segments2atoms_1d(sidx), aix) def test_s2a_2d(self, tt): for sidx, aix in zip( - [[0, 1], - [1, 0], - [1, 1]], - [[[0, 1, 6, 7], [4, 5, 8, 2, 3, 9]], - [[4, 5, 8, 2, 3, 9], [0, 1, 6, 7]], - [[4, 5, 8, 2, 3, 9], [4, 5, 8, 2, 3, 9]]], + [[0, 1], [1, 0], [1, 1]], + [ + [[0, 1, 6, 7], [4, 5, 8, 2, 3, 9]], + [[4, 5, 8, 2, 3, 9], [0, 1, 6, 7]], + [[4, 5, 8, 2, 3, 9], [4, 5, 8, 2, 3, 9]], + ], ): answer = tt.segments2atoms_2d(sidx) for a1, a2 in zip(answer, aix): @@ -153,12 +134,19 @@ def test_move_residue_simple(self, tt): def test_lazy_building_RA(self, tt): assert_equal(tt._RA, None) RA = tt.RA - assert_rows_match(tt.RA, - np.array([np.array([0, 1]), - np.array([4, 5, 8]), - np.array([2, 3, 9]), - np.array([6, 7]), - None], dtype=object)) + assert_rows_match( + tt.RA, + np.array( + [ + np.array([0, 1]), + np.array([4, 5, 8]), + np.array([2, 3, 9]), + np.array([6, 7]), + None, + ], + dtype=object, + ), + ) tt.move_atom(1, 3) assert_equal(tt._RA, None) @@ -166,10 +154,10 @@ def test_lazy_building_RA(self, tt): def test_lazy_building_SR(self, tt): assert_equal(tt._SR, None) SR = tt.SR - assert_rows_match(tt.SR, - np.array([np.array([0, 3]), - np.array([1, 2]), - None], dtype=object)) + assert_rows_match( + tt.SR, + np.array([np.array([0, 3]), np.array([1, 2]), None], dtype=object), + ) tt.move_residue(1, 0) assert_equal(tt._SR, None) @@ -187,18 +175,18 @@ def test_serialization(self, tt): class TestLevelMoves(object): """Tests for moving atoms/residues between residues/segments - + Atoms can move between residues by setting .residue with a Residue Residues can move between segments by setting .segment with a Segment Moves are performed by setting either [res/seg]indices or [res/seg]ids - + """ @pytest.fixture() def u(self): - return make_Universe(('resids', 'resnames', 'segids')) + return make_Universe(("resids", "resnames", "segids")) @staticmethod def assert_atoms_match_residue(atom, residue): @@ -305,48 +293,48 @@ def test_move_atomgroup_residue_list(self, u): # Wrong size argument for these operations def test_move_atom_residuegroup_TE(self, u): with pytest.raises(TypeError): - setattr(u.atoms[0], 'residue', u.atoms[1:3]) + setattr(u.atoms[0], "residue", u.atoms[1:3]) def test_move_atom_residue_list_TE(self, u): dest = [u.residues[1], u.residues[3]] with pytest.raises(TypeError): - setattr(u.atoms[0], 'residue', dest) + setattr(u.atoms[0], "residue", dest) def test_move_atomgroup_residuegroup_VE(self, u): ag = u.atoms[:2] dest = u.residues[5:10] with pytest.raises(ValueError): - setattr(ag, 'residues', dest) + setattr(ag, "residues", dest) def test_move_atomgroup_residue_list_VE(self, u): ag = u.atoms[:2] dest = [u.residues[0], u.residues[10], u.residues[15]] with pytest.raises(ValueError): - setattr(ag, 'residues', dest) + setattr(ag, "residues", dest) # Setting to non-Residue/ResidueGroup raises TE def test_move_atom_TE(self, u): with pytest.raises(TypeError): - setattr(u.atoms[0], 'residue', 14) + setattr(u.atoms[0], "residue", 14) def test_move_atomgroup_TE(self, u): with pytest.raises(TypeError): - setattr(u.atoms[:5], 'residues', 15) + setattr(u.atoms[:5], "residues", 15) def test_move_atomgroup_list_TE(self, u): with pytest.raises(TypeError): - setattr(u.atoms[:5], 'residues', [14, 12]) + setattr(u.atoms[:5], "residues", [14, 12]) # Test illegal moves - Atom.segment can't be changed def test_move_atom_segment_NIE(self, u): with pytest.raises(NotImplementedError): - setattr(u.atoms[0], 'segment', u.segments[1]) + setattr(u.atoms[0], "segment", u.segments[1]) def test_move_atomgroup_segment_NIE(self, u): with pytest.raises(NotImplementedError): - setattr(u.atoms[:3], 'segments', u.segments[1]) + setattr(u.atoms[:3], "segments", u.segments[1]) @staticmethod def assert_residue_matches_segment(res, seg): @@ -446,38 +434,38 @@ def test_move_residuegroup_segment_list(self, u): def test_move_residue_segmentgroup_TE(self, u): with pytest.raises(TypeError): - setattr(u.residues[0], 'segment', u.segments[:4]) + setattr(u.residues[0], "segment", u.segments[:4]) def test_move_residue_list_TE(self, u): dest = [u.segments[3], u.segments[4]] with pytest.raises(TypeError): - setattr(u.residues[0], 'segment', dest) + setattr(u.residues[0], "segment", dest) def test_move_residuegroup_segmentgroup_VE(self, u): rg = u.residues[:3] sg = u.segments[1:] with pytest.raises(ValueError): - setattr(rg, 'segments', sg) + setattr(rg, "segments", sg) def test_move_residuegroup_list_VE(self, u): rg = u.residues[:2] sg = [u.segments[1], u.segments[2], u.segments[3]] with pytest.raises(ValueError): - setattr(rg, 'segments', sg) + setattr(rg, "segments", sg) def test_move_residue_TE(self, u): with pytest.raises(TypeError): - setattr(u.residues[0], 'segment', 1) + setattr(u.residues[0], "segment", 1) def test_move_residuegroup_TE(self, u): with pytest.raises(TypeError): - setattr(u.residues[:3], 'segments', 4) + setattr(u.residues[:3], "segments", 4) def test_move_residuegroup_list_TE(self, u): with pytest.raises(TypeError): - setattr(u.residues[:3], 'segments', [1, 2, 3]) + setattr(u.residues[:3], "segments", [1, 2, 3]) class TestDownshiftArrays(object): @@ -503,8 +491,7 @@ def ragged_size(self): @pytest.fixture() def ragged_result(self): - return np.array([[0, 4, 7], [1, 5, 8], [2, 3, 6, 9]], - dtype=object) + return np.array([[0, 4, 7], [1, 5, 8], [2, 3, 6, 9]], dtype=object) # The array as a whole must be dtype object # While the subarrays must be integers @@ -539,53 +526,87 @@ def test_contents_ragged(self, ragged, ragged_size, ragged_result): assert_rows_match(out, ragged_result) def test_missing_intra_values(self): - out = make_downshift_arrays( - np.array([0, 0, 2, 2, 3, 3]), 4) - assert_rows_match(out, - np.array([np.array([0, 1]), - np.array([], dtype=int), - np.array([2, 3]), - np.array([4, 5]), - None], dtype=object)) + out = make_downshift_arrays(np.array([0, 0, 2, 2, 3, 3]), 4) + assert_rows_match( + out, + np.array( + [ + np.array([0, 1]), + np.array([], dtype=int), + np.array([2, 3]), + np.array([4, 5]), + None, + ], + dtype=object, + ), + ) def test_missing_intra_values_2(self): - out = make_downshift_arrays( - np.array([0, 0, 3, 3, 4, 4]), 5) - assert_rows_match(out, - np.array([np.array([0, 1]), - np.array([], dtype=int), - np.array([], dtype=int), - np.array([2, 3]), - np.array([4, 5]), - None], dtype=object)) + out = make_downshift_arrays(np.array([0, 0, 3, 3, 4, 4]), 5) + assert_rows_match( + out, + np.array( + [ + np.array([0, 1]), + np.array([], dtype=int), + np.array([], dtype=int), + np.array([2, 3]), + np.array([4, 5]), + None, + ], + dtype=object, + ), + ) def test_missing_end_values(self): out = make_downshift_arrays(np.array([0, 0, 1, 1, 2, 2]), 4) - assert_rows_match(out, - np.array([np.array([0, 1]), - np.array([2, 3]), - np.array([4, 5]), - np.array([], dtype=int), - None], dtype=object)) + assert_rows_match( + out, + np.array( + [ + np.array([0, 1]), + np.array([2, 3]), + np.array([4, 5]), + np.array([], dtype=int), + None, + ], + dtype=object, + ), + ) def test_missing_end_values_2(self): out = make_downshift_arrays(np.array([0, 0, 1, 1, 2, 2]), 6) - assert_rows_match(out, - np.array([np.array([0, 1]), - np.array([2, 3]), - np.array([4, 5]), - np.array([], dtype=int), - np.array([], dtype=int), - None], dtype=object)) + assert_rows_match( + out, + np.array( + [ + np.array([0, 1]), + np.array([2, 3]), + np.array([4, 5]), + np.array([], dtype=int), + np.array([], dtype=int), + None, + ], + dtype=object, + ), + ) def test_missing_start_values_2(self): out = make_downshift_arrays(np.array([1, 1, 2, 2, 3, 3]), 4) - assert_rows_match(out, - np.array([np.array([], dtype=int), - np.array([0, 1]), - np.array([2, 3]), - np.array([4, 5]), - None], dtype=object)) + assert_rows_match( + out, + np.array( + [ + np.array([], dtype=int), + np.array([0, 1]), + np.array([2, 3]), + np.array([4, 5]), + None, + ], + dtype=object, + ), + ) + class TestAddingResidues(object): """Tests for adding residues and segments to a Universe @@ -635,74 +656,75 @@ def test_add_Residue_ambiguous_segment_NDE(self): u.add_Residue() def test_add_Residue_missing_attr_NDE(self): - u = make_Universe(('resids',)) + u = make_Universe(("resids",)) with pytest.raises(NoDataError): u.add_Residue(segment=u.segments[0]) def test_add_Residue_NDE_message(self): # check error message asks for missing attr - u = make_Universe(('resnames', 'resids')) + u = make_Universe(("resnames", "resids")) try: u.add_Residue(segment=u.segments[0], resid=42) except NoDataError as e: - assert 'resname' in str(e) + assert "resname" in str(e) else: raise AssertionError def test_add_Residue_NDE_message_2(self): # multiple missing attrs, check all get mentioned in error - u = make_Universe(('resnames', 'resids')) + u = make_Universe(("resnames", "resids")) try: u.add_Residue(segment=u.segments[0]) except NoDataError as e: - assert 'resname' in str(e) - assert 'resid' in str(e) + assert "resname" in str(e) + assert "resid" in str(e) else: raise AssertionError def test_add_Residue_with_attrs(self): - u = make_Universe(('resnames', 'resids')) + u = make_Universe(("resnames", "resids")) - r_new = u.add_Residue(segment=u.segments[0], resid=4321, resname='New') + r_new = u.add_Residue(segment=u.segments[0], resid=4321, resname="New") assert r_new.resid == 4321 - assert r_new.resname == 'New' + assert r_new.resname == "New" def test_missing_attr_NDE_Segment(self): - u = make_Universe(('segids',)) + u = make_Universe(("segids",)) with pytest.raises(NoDataError): u.add_Segment() def test_add_Segment_NDE_message(self): - u = make_Universe(('segids',)) + u = make_Universe(("segids",)) try: u.add_Segment() except NoDataError as e: - assert 'segid' in str(e) + assert "segid" in str(e) else: raise AssertionError def test_add_Segment_with_attr(self): - u = make_Universe(('segids',)) + u = make_Universe(("segids",)) - new_seg = u.add_Segment(segid='New') + new_seg = u.add_Segment(segid="New") - assert new_seg.segid == 'New' + assert new_seg.segid == "New" class TestTopologyGuessed(object): @pytest.fixture() def names(self): - return ta.Atomnames(np.array(['A', 'B', 'C'], dtype=object)) + return ta.Atomnames(np.array(["A", "B", "C"], dtype=object)) @pytest.fixture() def types(self): - return ta.Atomtypes(np.array(['X', 'Y', 'Z'], dtype=object), - guessed=True) + return ta.Atomtypes( + np.array(["X", "Y", "Z"], dtype=object), guessed=True + ) @pytest.fixture() def resids(self): @@ -710,8 +732,7 @@ def resids(self): @pytest.fixture() def resnames(self): - return ta.Resnames(np.array(['ABC'], dtype=object), - guessed=True) + return ta.Resnames(np.array(["ABC"], dtype=object), guessed=True) @pytest.fixture() def bonds(self): @@ -719,8 +740,9 @@ def bonds(self): @pytest.fixture() def top(self, names, types, resids, resnames, bonds): - return Topology(n_atoms=3, n_res=1, - attrs=[names, types, resids, resnames, bonds]) + return Topology( + n_atoms=3, n_res=1, attrs=[names, types, resids, resnames, bonds] + ) def test_guessed(self, names, types, resids, resnames, bonds, top): guessed = top.guessed_attributes @@ -739,12 +761,13 @@ def test_read(self, names, types, resids, resnames, bonds, top): assert not types in read assert not resnames in read + class TestTopologyCreation(object): def test_make_topology_no_attrs(self): # should still make attrs list when attrs=None top = Topology() - assert hasattr(top, 'attrs') + assert hasattr(top, "attrs") assert isinstance(top.attrs, list) def test_resindex_VE(self): @@ -758,5 +781,4 @@ def test_segindex_VE(self): AR = np.arange(5) RS = np.arange(10) with pytest.raises(ValueError): - Topology(n_atoms=5, n_res=5, atom_resindex=AR, - residue_segindex=RS) + Topology(n_atoms=5, n_res=5, atom_resindex=AR, residue_segindex=RS) diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 3ece107a93d..5155933c2e4 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -45,6 +45,7 @@ class DummyGroup(object): initiate with indices, these are then available as ._ix """ + def __init__(self, vals): self._ix = vals @@ -64,17 +65,21 @@ class TopologyAttrMixin(object): 2 segments """ - # Reference data + # Reference data @pytest.fixture() def top(self): Ridx = np.array([0, 0, 2, 2, 1, 1, 3, 3, 1, 2]) Sidx = np.array([0, 1, 1, 0]) - return Topology(10, 4, 2, - attrs=[self.attrclass(self.values.copy())], - atom_resindex=Ridx, - residue_segindex=Sidx) + return Topology( + 10, + 4, + 2, + attrs=[self.attrclass(self.values.copy())], + atom_resindex=Ridx, + residue_segindex=Sidx, + ) @pytest.fixture() def attr(self, top): @@ -89,33 +94,33 @@ def test_len(self, attr): class TestAtomAttr(TopologyAttrMixin): - """Test atom-level TopologyAttrs. + """Test atom-level TopologyAttrs.""" - """ values = np.array([7, 3, 69, 9993, 84, 194, 263, 501, 109, 5873]) single_value = 567 attrclass = tpattrs.AtomAttr def test_set_atom_VE(self): - u = make_Universe(('names',)) + u = make_Universe(("names",)) at = u.atoms[0] with pytest.raises(ValueError): - setattr(at, 'name', ['oopsy', 'daisies']) + setattr(at, "name", ["oopsy", "daisies"]) def test_get_atoms(self, attr): result = attr.get_atoms(DummyGroup([2, 1])) assert len(result) == 2 - assert_equal(result, - self.values[[2, 1]]) + assert_equal(result, self.values[[2, 1]]) def test_set_atoms_singular(self, attr): # set len 2 Group to len 1 value dg = DummyGroup([3, 7]) attr.set_atoms(dg, self.single_value) - assert_equal(attr.get_atoms(dg), - np.array([self.single_value, self.single_value])) + assert_equal( + attr.get_atoms(dg), + np.array([self.single_value, self.single_value]), + ) def test_set_atoms_plural(self, attr): # set len 2 Group to len 2 values @@ -137,8 +142,7 @@ def test_get_residues(self, attr): result = attr.get_residues(DummyGroup([2, 1])) assert len(result) == 2 - assert_equal(result, - [self.values[[2, 3, 9]], self.values[[4, 5, 8]]]) + assert_equal(result, [self.values[[2, 3, 9]], self.values[[4, 5, 8]]]) def test_get_segments(self, attr): """Unless overriden by child class, this should yield values for all @@ -148,8 +152,7 @@ def test_get_segments(self, attr): result = attr.get_segments(DummyGroup([1])) assert len(result) == 1 - assert_equal(result, - [self.values[[4, 5, 8, 2, 3, 9]]]) + assert_equal(result, [self.values[[4, 5, 8, 2, 3, 9]]]) class TestAtomids(TestAtomAttr): @@ -175,9 +178,10 @@ def test_cant_set_segment_indices(self, u): class TestAtomnames(TestAtomAttr): - values = np.array(['O', 'C', 'CA', 'N', 'CB', 'CG', 'CD', 'NA', 'CL', 'OW'], - dtype=object) - single_value = 'Ca2' + values = np.array( + ["O", "C", "CA", "N", "CB", "CG", "CD", "NA", "CL", "OW"], dtype=object + ) + single_value = "Ca2" attrclass = tpattrs.Atomnames @pytest.fixture() @@ -198,39 +202,65 @@ def test_next_emptyresidue(self, u): 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])) + 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])) + 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): - assert_equal(attr.get_residues(DummyGroup([2, 1])), - np.array([self.values[[2, 3, 9]].sum(), - self.values[[4, 5, 8]].sum()])) + assert_equal( + attr.get_residues(DummyGroup([2, 1])), + np.array( + [self.values[[2, 3, 9]].sum(), self.values[[4, 5, 8]].sum()] + ), + ) def test_get_segments(self, attr): - assert_equal(attr.get_segments(DummyGroup([1])), - np.array([self.values[[4, 5, 8, 2, 3, 9]].sum()])) + assert_equal( + attr.get_segments(DummyGroup([1])), + np.array([self.values[[4, 5, 8, 2, 3, 9]].sum()]), + ) def test_get_segment(self, attr): - assert_equal(attr.get_segments(DummyGroup(1)), - np.sum(self.values[[4, 5, 8, 2, 3, 9]])) + assert_equal( + attr.get_segments(DummyGroup(1)), + np.sum(self.values[[4, 5, 8, 2, 3, 9]]), + ) 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])) + values = [1.0, 2.0, np.nan, 3.0] + 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]) @@ -238,9 +268,8 @@ class TestCharges(AggregationMixin): class TestResidueAttr(TopologyAttrMixin): - """Test residue-level TopologyAttrs. + """Test residue-level TopologyAttrs.""" - """ single_value = 2 values = np.array([15.2, 395.6, 0.1, 9.8]) attrclass = tpattrs.ResidueAttr @@ -252,29 +281,34 @@ def test_set_residue_VE(self, universe): setattr(res, self.attrclass.singular, self.values[:2]) def test_get_atoms(self, attr): - assert_equal(attr.get_atoms(DummyGroup([7, 3, 9])), - self.values[[3, 2, 2]]) + assert_equal( + attr.get_atoms(DummyGroup([7, 3, 9])), self.values[[3, 2, 2]] + ) def test_get_atom(self, universe): attr = getattr(universe.atoms[0], self.attrclass.singular) assert_equal(attr, self.values[0]) def test_get_residues(self, attr): - assert_equal(attr.get_residues(DummyGroup([1, 2, 1, 3])), - self.values[[1, 2, 1, 3]]) + assert_equal( + attr.get_residues(DummyGroup([1, 2, 1, 3])), + self.values[[1, 2, 1, 3]], + ) def test_set_residues_singular(self, attr): dg = DummyGroup([3, 0, 1]) attr.set_residues(dg, self.single_value) - assert_equal(attr.get_residues(dg), - np.array([self.single_value]*3, dtype=self.values.dtype)) + assert_equal( + attr.get_residues(dg), + np.array([self.single_value] * 3, dtype=self.values.dtype), + ) def test_set_residues_plural(self, attr): - attr.set_residues(DummyGroup([3, 0, 1]), - np.array([23, 504, 2])) - assert_almost_equal(attr.get_residues(DummyGroup([3, 0, 1])), - np.array([23, 504, 2])) + attr.set_residues(DummyGroup([3, 0, 1]), np.array([23, 504, 2])) + assert_almost_equal( + attr.get_residues(DummyGroup([3, 0, 1])), np.array([23, 504, 2]) + ) def test_set_residues_VE(self, attr): dg = DummyGroup([3, 0, 1]) @@ -287,14 +321,16 @@ def test_get_segments(self, attr): atoms in segments. """ - assert_equal(attr.get_segments(DummyGroup([0, 1, 1])), - [self.values[[0, 3]], self.values[[1, 2]], self.values[[1, 2]]]) + assert_equal( + attr.get_segments(DummyGroup([0, 1, 1])), + [self.values[[0, 3]], self.values[[1, 2]], self.values[[1, 2]]], + ) class TestResnames(TestResidueAttr): attrclass = tpattrs.Resnames - single_value = 'xyz' - values = np.array(['a', 'b', '', 'd'], dtype=object) + single_value = "xyz" + values = np.array(["a", "b", "", "d"], dtype=object) class TestICodes(TestResnames): @@ -307,9 +343,7 @@ class TestResids(TestResidueAttr): @pytest.mark.xfail def test_set_atoms(self, attr): - """Setting the resids of atoms changes their residue membership. - - """ + """Setting the resids of atoms changes their residue membership.""" # moving resids doesn't currently work! assert 1 == 2 @@ -322,40 +356,43 @@ def test_set_atoms(self, attr): attr.set_atoms(DummyGroup([3, 7]), np.array([11, 21])) def test_set_residues(self, attr): - attr.set_residues(DummyGroup([3, 0, 1]), - np.array([23, 504, 27])) - assert_almost_equal(attr.get_residues(DummyGroup([3, 0, 1])), - np.array([23, 504, 27])) + attr.set_residues(DummyGroup([3, 0, 1]), np.array([23, 504, 27])) + assert_almost_equal( + attr.get_residues(DummyGroup([3, 0, 1])), np.array([23, 504, 27]) + ) class TestSegmentAttr(TopologyAttrMixin): - """Test segment-level TopologyAttrs. + """Test segment-level TopologyAttrs.""" - """ values = np.array([-0.19, 500]) attrclass = tpattrs.SegmentAttr def test_set_segment_VE(self): - u = make_Universe(('segids',)) + u = make_Universe(("segids",)) seg = u.segments[0] with pytest.raises(ValueError): - setattr(seg, 'segid', [1, 2, 3]) + setattr(seg, "segid", [1, 2, 3]) def test_get_atoms(self, attr): - assert_equal(attr.get_atoms(DummyGroup([2, 4, 1])), - self.values[[1, 1, 0]]) + assert_equal( + attr.get_atoms(DummyGroup([2, 4, 1])), self.values[[1, 1, 0]] + ) def test_get_residues(self, attr): - assert_equal(attr.get_residues(DummyGroup([1, 2, 1, 3])), - self.values[[1, 1, 1, 0]]) + assert_equal( + attr.get_residues(DummyGroup([1, 2, 1, 3])), + self.values[[1, 1, 1, 0]], + ) def test_get_segments(self, attr): """Unless overriden by child class, this should yield values for all atoms in segments. """ - assert_equal(attr.get_segments(DummyGroup([1, 0, 0])), - self.values[[1, 0, 0]]) + assert_equal( + attr.get_segments(DummyGroup([1, 0, 0])), self.values[[1, 0, 0]] + ) def test_set_segments_singular(self, attr): dg = DummyGroup([0, 1]) @@ -382,10 +419,14 @@ def ag(self): def test_principal_axes(self, ag): assert_almost_equal( ag.principal_axes(), - np.array([ - [1.53389276e-03, 4.41386224e-02, 9.99024239e-01], - [1.20986911e-02, 9.98951474e-01, -4.41539838e-02], - [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04]])) + np.array( + [ + [1.53389276e-03, 4.41386224e-02, 9.99024239e-01], + [1.20986911e-02, 9.98951474e-01, -4.41539838e-02], + [-9.99925632e-01, 1.21546132e-02, 9.98264877e-04], + ] + ), + ) @pytest.fixture() def universe_pa(self): @@ -393,7 +434,9 @@ def universe_pa(self): def test_principal_axes_handedness(self, universe_pa): e_vec = universe_pa.atoms.principal_axes() - assert_almost_equal(np.dot(np.cross(e_vec[0], e_vec[1]), e_vec[2]), 1.0) + assert_almost_equal( + np.dot(np.cross(e_vec[0], e_vec[1]), e_vec[2]), 1.0 + ) def test_align_principal_axes_with_self(self, ag): pa = ag.principal_axes() @@ -422,7 +465,7 @@ class TestCrossLevelAttributeSetting(object): Atom.resid = 4 should fail because resid belongs to Residue not Atom """ - u = make_Universe(('names', 'resids', 'segids')) + u = make_Universe(("names", "resids", "segids")) # component and group in each level atomlevel = (u.atoms[0], u.atoms[:10]) @@ -430,13 +473,13 @@ class TestCrossLevelAttributeSetting(object): segmentlevel = (u.segments[0], u.segments[:2]) levels = {0: atomlevel, 1: residuelevel, 2: segmentlevel} - atomattr = 'names' - residueattr = 'resids' - segmentattr = 'segids' + atomattr = "names" + residueattr = "resids" + segmentattr = "segids" attrs = {0: atomattr, 1: residueattr, 2: segmentattr} - @pytest.mark.parametrize('level_idx, level', levels.items()) - @pytest.mark.parametrize('attr_idx, attr', attrs.items()) + @pytest.mark.parametrize("level_idx, level", levels.items()) + @pytest.mark.parametrize("attr_idx, attr", attrs.items()) def test_set_crosslevel(self, level_idx, level, attr_idx, attr): if level_idx == attr_idx: # if we're on the same level, then this should work @@ -468,33 +511,33 @@ class TestRecordTypes(object): def test_record_types_default(self): u = make_Universe() - u.add_TopologyAttr('record_type') + u.add_TopologyAttr("record_type") - assert u.atoms[0].record_type == 'ATOM' - assert_equal(u.atoms[:10].record_types, 'ATOM') + assert u.atoms[0].record_type == "ATOM" + assert_equal(u.atoms[:10].record_types, "ATOM") @pytest.fixture() def rectype_uni(self): # standard 125/25/5 universe u = make_Universe() - u.add_TopologyAttr('record_type') + u.add_TopologyAttr("record_type") # first 25 atoms are ATOM (first 5 residues, first segment) # 25 to 50th are HETATM (res 5:10, second segment) # all after are ATOM - u.atoms[:25].record_types = 'ATOM' - u.atoms[25:50].record_types = 'HETATM' - u.atoms[50:].record_types = 'ATOM' + u.atoms[:25].record_types = "ATOM" + u.atoms[25:50].record_types = "HETATM" + u.atoms[50:].record_types = "ATOM" return u def test_encoding(self, rectype_uni): ag = rectype_uni.atoms[:10] - ag[0].record_type = 'ATOM' - ag[1:4].record_types = 'HETATM' + ag[0].record_type = "ATOM" + ag[1:4].record_types = "HETATM" - assert ag[0].record_type == 'ATOM' - assert ag[1].record_type == 'HETATM' + assert ag[0].record_type == "ATOM" + assert ag[1].record_type == "HETATM" def test_residue_record_types(self, rectype_uni): rt = rectype_uni.residues.record_types @@ -505,8 +548,8 @@ def test_residue_record_types(self, rectype_uni): # check return type explicitly # some versions of numpy allow bool to str comparison assert not rt[0].dtype == bool - assert (rt[0] == 'ATOM').all() - assert (rt[5] == 'HETATM').all() + assert (rt[0] == "ATOM").all() + assert (rt[5] == "HETATM").all() def test_segment_record_types(self, rectype_uni): rt = rectype_uni.segments.record_types @@ -515,12 +558,12 @@ def test_segment_record_types(self, rectype_uni): assert len(rt) == 5 assert not rt[0].dtype == bool - assert (rt[0] == 'ATOM').all() - assert (rt[1] == 'HETATM').all() + assert (rt[0] == "ATOM").all() + assert (rt[1] == "HETATM").all() def test_static_typing(): - ta = tpattrs.Charges(['1.0', '2.0', '3.0']) + ta = tpattrs.Charges(["1.0", "2.0", "3.0"]) assert isinstance(ta.values, np.ndarray) assert ta.values.dtype == float @@ -529,18 +572,21 @@ def test_static_typing(): def test_static_typing_from_empty(): u = mda.Universe.empty(3) - u.add_TopologyAttr('masses', values=['1.0', '2.0', '3.0']) + u.add_TopologyAttr("masses", values=["1.0", "2.0", "3.0"]) assert isinstance(u._topology.masses.values, np.ndarray) assert isinstance(u.atoms[0].mass, float) -@pytest.mark.parametrize('level, transplant_name', ( - ('atoms', 'center_of_mass'), - ('atoms', 'center_of_charge'), - ('atoms', 'total_charge'), - ('residues', 'total_charge'), -)) +@pytest.mark.parametrize( + "level, transplant_name", + ( + ("atoms", "center_of_mass"), + ("atoms", "center_of_charge"), + ("atoms", "total_charge"), + ("residues", "total_charge"), + ), +) def test_stub_transplant_methods(level, transplant_name): u = mda.Universe.empty(n_atoms=2) group = getattr(u, level) @@ -548,10 +594,13 @@ def test_stub_transplant_methods(level, transplant_name): getattr(group, transplant_name)() -@pytest.mark.parametrize('level, transplant_name', ( - ('universe', 'models'), - ('atoms', 'n_fragments'), -)) +@pytest.mark.parametrize( + "level, transplant_name", + ( + ("universe", "models"), + ("atoms", "n_fragments"), + ), +) def test_stub_transplant_property(level, transplant_name): u = mda.Universe.empty(n_atoms=2) group = getattr(u, level) @@ -563,6 +612,7 @@ def test_warn_selection_for_strange_dtype(): err = "A selection keyword could not be automatically generated" with pytest.warns(UserWarning, match=err): + class Star(tpattrs.TopologyAttr): singular = "star" # turns out test_imports doesn't like emoji attrname = "stars" # :( @@ -612,15 +662,16 @@ class TestStringInterning: # try and trip up the string interning we use for string attributes @pytest.fixture def universe(self): - u = mda.Universe.empty(n_atoms=10, n_residues=2, - atom_resindex=[0]*5 + [1] * 5) - u.add_TopologyAttr('names', values=['A'] * 10) - u.add_TopologyAttr('resnames', values=['ResA', 'ResB']) - u.add_TopologyAttr('segids', values=['SegA']) + u = mda.Universe.empty( + n_atoms=10, n_residues=2, atom_resindex=[0] * 5 + [1] * 5 + ) + u.add_TopologyAttr("names", values=["A"] * 10) + u.add_TopologyAttr("resnames", values=["ResA", "ResB"]) + u.add_TopologyAttr("segids", values=["SegA"]) return u - @pytest.mark.parametrize('newname', ['ResA', 'ResB']) + @pytest.mark.parametrize("newname", ["ResA", "ResB"]) def test_add_residue(self, universe, newname): newres = universe.add_Residue(resname=newname) @@ -631,7 +682,7 @@ def test_add_residue(self, universe, newname): assert ag.resname == newname - @pytest.mark.parametrize('newname', ['SegA', 'SegB']) + @pytest.mark.parametrize("newname", ["SegA", "SegB"]) def test_add_segment(self, universe, newname): newseg = universe.add_Segment(segid=newname) @@ -643,32 +694,34 @@ def test_add_segment(self, universe, newname): assert rg.atoms[0].segid == newname def test_issue3437(self, universe): - newseg = universe.add_Segment(segid='B') + newseg = universe.add_Segment(segid="B") ag = universe.residues[0].atoms ag.residues.segments = newseg - assert 'B' in universe.segments.segids + assert "B" in universe.segments.segids - ag2 = universe.select_atoms('segid B') + ag2 = universe.select_atoms("segid B") assert len(ag2) == 5 assert (ag2.ix == ag.ix).all() -class Testcenter_of_charge(): +class Testcenter_of_charge: - compounds = ['group', 'segments', 'residues', 'molecules', 'fragments'] + compounds = ["group", "segments", "residues", "molecules", "fragments"] @pytest.fixture def u(self): """A universe containing two dimers with a finite dipole moment.""" - universe = mda.Universe.empty(n_atoms=4, - n_residues=2, - n_segments=2, - atom_resindex=[0, 0, 1, 1], - residue_segindex=[0, 1]) + universe = mda.Universe.empty( + n_atoms=4, + n_residues=2, + n_segments=2, + atom_resindex=[0, 0, 1, 1], + residue_segindex=[0, 1], + ) universe.add_TopologyAttr("masses", [1, 0, 0, 1]) universe.add_TopologyAttr("charges", [1, -1, -1, 1]) @@ -678,16 +731,16 @@ def u(self): positions = np.array([[0, 0, 0], [0, 1, 0], [2, 1, 0], [2, 2, 0]]) - universe.trajectory = get_reader_for(positions)(positions, - order='fac', - n_atoms=4) + universe.trajectory = get_reader_for(positions)( + positions, order="fac", n_atoms=4 + ) for ts in universe.trajectory: ts.dimensions = np.array([1, 2, 3, 90, 90, 90]) return universe - @pytest.mark.parametrize('compound', compounds) + @pytest.mark.parametrize("compound", compounds) def test_coc(self, u, compound): coc = u.atoms.center_of_charge(compound=compound) if compound == "group": @@ -696,12 +749,12 @@ def test_coc(self, u, compound): coc_ref = [[0, 0.5, 0], [2, 1.5, 0]] assert_equal(coc, coc_ref) - @pytest.mark.parametrize('compound', compounds) + @pytest.mark.parametrize("compound", compounds) def test_coc_wrap(self, u, compound): coc = u.atoms[:2].center_of_charge(compound=compound, wrap=True) assert_equal(coc.flatten(), [0, 0.5, 0]) - @pytest.mark.parametrize('compound', compounds) + @pytest.mark.parametrize("compound", compounds) def test_coc_unwrap(self, u, compound): u.atoms.wrap coc = u.atoms[:2].center_of_charge(compound=compound, unwrap=True) diff --git a/testsuite/MDAnalysisTests/core/test_topologyobjects.py b/testsuite/MDAnalysisTests/core/test_topologyobjects.py index c4bc05c6a1c..196b91364df 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyobjects.py +++ b/testsuite/MDAnalysisTests/core/test_topologyobjects.py @@ -31,19 +31,25 @@ from MDAnalysis.lib.distances import calc_bonds, calc_angles, calc_dihedrals from MDAnalysisTests.datafiles import LAMMPSdata_many_bonds from MDAnalysis.core.topologyobjects import ( - TopologyGroup, TopologyObject, TopologyDict, + TopologyGroup, + TopologyObject, + TopologyDict, # TODO: the following items are not used - Bond, Angle, Dihedral, ImproperDihedral, + Bond, + Angle, + Dihedral, + ImproperDihedral, ) from MDAnalysisTests.datafiles import PSF, DCD, TRZ_psf, TRZ -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def PSFDCD(): return mda.Universe(PSF, DCD) + class TestTopologyObjects(object): """Test the base TopologyObject funtionality @@ -54,6 +60,7 @@ class TestTopologyObjects(object): iter len """ + precision = 3 # see Issue #271 and #1556 @staticmethod @@ -82,9 +89,9 @@ def b(PSFDCD): return PSFDCD.atoms[12].bonds[0] def test_repr(self, TO1): - assert_equal(repr(TO1), '') + assert_equal(repr(TO1), "") - def test_eq(self, a1 ,TO1, TO2, PSFDCD): + def test_eq(self, a1, TO1, TO2, PSFDCD): TO1_b = TopologyObject(a1.indices, PSFDCD) assert_equal(TO1 == TO1_b, True) @@ -145,7 +152,7 @@ def test_bondlength(self, b): assert_almost_equal(b.length(), 1.7661301556941993, self.precision) def test_bondrepr(self, b): - assert_equal(repr(b), '') + assert_equal(repr(b), "") # Angle class checks def test_angle(self, PSFDCD): @@ -157,16 +164,13 @@ def test_angle(self, PSFDCD): def test_angle_repr(self, PSFDCD): angle = PSFDCD.atoms[[30, 10, 20]].angle - assert_equal(repr(angle), '') + assert_equal(repr(angle), "") def test_angle_180(self): # we edit the coordinates, so make our own universe u = mda.Universe(PSF, DCD) angle = u.atoms[210].angles[0] - coords = np.array([[1, 1, 1], - [2, 1, 1], - [3, 1, 1]], - dtype=np.float32) + coords = np.array([[1, 1, 1], [2, 1, 1], [3, 1, 1]], dtype=np.float32) angle.atoms.positions = coords @@ -182,8 +186,10 @@ def test_dihedral(self, PSFDCD): def test_dihedral_repr(self, PSFDCD): dihedral = PSFDCD.atoms[[4, 7, 8, 1]].dihedral - assert_equal(repr(dihedral), - '') + assert_equal( + repr(dihedral), + "", + ) # Improper_Dihedral class check def test_improper(self, PSFDCD): @@ -197,12 +203,13 @@ def test_improper_repr(self, PSFDCD): assert_equal( repr(imp), - '') + "", + ) def test_ureybradley_repr(self, PSFDCD): ub = PSFDCD.atoms[[30, 10]].ureybradley - assert_equal(repr(ub), '') + assert_equal(repr(ub), "") def test_ureybradley_repr_VE(self, PSFDCD): with pytest.raises(ValueError): @@ -214,19 +221,23 @@ def test_ureybradley_partner(self, PSFDCD): assert ub.partner(PSFDCD.atoms[10]) == PSFDCD.atoms[30] def test_ureybradley_distance(self, b): - assert_almost_equal(b.atoms.ureybradley.distance(), b.length(), self.precision) + assert_almost_equal( + b.atoms.ureybradley.distance(), b.length(), self.precision + ) def test_cmap_repr(self, PSFDCD): cmap = PSFDCD.atoms[[4, 7, 8, 1, 2]].cmap assert_equal( repr(cmap), - '') - + "", + ) + def test_cmap_repr_VE(self, PSFDCD): with pytest.raises(ValueError): cmap = PSFDCD.atoms[[30, 10, 2]].cmap + class TestTopologyGroup(object): """Tests TopologyDict and TopologyGroup classes with psf input""" @@ -271,7 +282,7 @@ def test_td_iter(self, b_td): def test_td_keyerror(self, b_td): with pytest.raises(KeyError): - b_td[('something', 'stupid')] + b_td[("something", "stupid")] def test_td_universe(self, b_td, PSFDCD): assert b_td.universe is PSFDCD @@ -282,15 +293,15 @@ def test_bonds_types(self, PSFDCD, res1): assert len(res1.atoms.bonds.types()) == 12 def test_bonds_contains(self, b_td): - assert ('57', '2') in b_td + assert ("57", "2") in b_td def test_bond_uniqueness(self, PSFDCD): bondtypes = PSFDCD.atoms.bonds.types() # check that a key doesn't appear in reversed format in keylist # have to exclude case of b[::-1] == b as this is false positive - assert not any([b[::-1] in bondtypes - for b in bondtypes if b[::-1] != b]) - + assert not any( + [b[::-1] in bondtypes for b in bondtypes if b[::-1] != b] + ) def test_bond_reversal(self, PSFDCD, b_td): bondtypes = PSFDCD.atoms.bonds.types() @@ -315,12 +326,11 @@ def test_angles_types(self, PSFDCD): assert len(PSFDCD.atoms.angles.types()) == 130 def test_angles_contains(self, a_td): - assert ('23', '73', '1') in a_td + assert ("23", "73", "1") in a_td def test_angles_uniqueness(self, a_td): bondtypes = a_td.keys() - assert not any(b[::-1] in bondtypes - for b in bondtypes if b[::-1] != b) + assert not any(b[::-1] in bondtypes for b in bondtypes if b[::-1] != b) def test_angles_reversal(self, a_td): bondtypes = list(a_td.keys()) @@ -336,7 +346,7 @@ def test_dihedrals_types(self, PSFDCD): assert len(PSFDCD.atoms.dihedrals.types()) == 220 def test_dihedrals_contains(self, t_td): - assert ('30', '29', '20', '70') in t_td + assert ("30", "29", "20", "70") in t_td def test_dihedrals_uniqueness(self, t_td): bondtypes = t_td.keys() @@ -353,26 +363,26 @@ def test_dihedrals_reversal(self, t_td): def test_bad_creation(self): """Test making a TopologyDict out of nonsense""" - inputlist = ['a', 'b', 'c'] + inputlist = ["a", "b", "c"] with pytest.raises(TypeError): TopologyDict(inputlist) def test_bad_creation_TG(self): """Test making a TopologyGroup out of nonsense""" - inputlist = ['a', 'b', 'c'] + inputlist = ["a", "b", "c"] with pytest.raises(TypeError): TopologyGroup(inputlist) def test_tg_creation_bad_btype(self, PSFDCD): vals = np.array([[0, 10], [5, 15]]) with pytest.raises(ValueError): - TopologyGroup(vals, PSFDCD, btype='apple') + TopologyGroup(vals, PSFDCD, btype="apple") def test_bond_tg_creation_notype(self, PSFDCD): vals = np.array([[0, 10], [5, 15]]) tg = TopologyGroup(vals, PSFDCD) - assert tg.btype == 'bond' + assert tg.btype == "bond" assert_equal(tg[0].indices, (0, 10)) assert_equal(tg[1].indices, (5, 15)) @@ -380,7 +390,7 @@ def test_angle_tg_creation_notype(self, PSFDCD): vals = np.array([[0, 5, 10], [5, 10, 15]]) tg = TopologyGroup(vals, PSFDCD) - assert tg.btype == 'angle' + assert tg.btype == "angle" assert_equal(tg[0].indices, (0, 5, 10)) assert_equal(tg[1].indices, (5, 10, 15)) @@ -389,7 +399,7 @@ def test_dihedral_tg_creation_notype(self, PSFDCD): tg = TopologyGroup(vals, PSFDCD) - assert tg.btype == 'dihedral' + assert tg.btype == "dihedral" assert_equal(tg[0].indices, (0, 2, 4, 6)) assert_equal(tg[1].indices, (5, 7, 9, 11)) @@ -410,27 +420,28 @@ def test_TG_equality(self, PSFDCD): * check they're equal * change one very slightly and see if they notice """ - tg = PSFDCD.atoms.bonds.selectBonds(('23', '3')) - tg2 = PSFDCD.atoms.bonds.selectBonds(('23', '3')) + tg = PSFDCD.atoms.bonds.selectBonds(("23", "3")) + tg2 = PSFDCD.atoms.bonds.selectBonds(("23", "3")) assert tg == tg2 - tg3 = PSFDCD.atoms.bonds.selectBonds(('81', '10')) + tg3 = PSFDCD.atoms.bonds.selectBonds(("81", "10")) assert not (tg == tg3) assert tg != tg3 def test_create_TopologyGroup(self, res1, PSFDCD): - res1_tg = res1.atoms.bonds.select_bonds(('23', '3')) # make a tg + res1_tg = res1.atoms.bonds.select_bonds(("23", "3")) # make a tg assert len(res1_tg) == 4 # check size of tg testbond = PSFDCD.atoms[7].bonds[0] assert testbond in res1_tg # check a known bond is present - res1_tg2 = res1.atoms.bonds.select_bonds(('23', '3')) + res1_tg2 = res1.atoms.bonds.select_bonds(("23", "3")) assert res1_tg == res1_tg2 - @pytest.mark.parametrize('attr', - ['bonds', 'angles', 'dihedrals', 'impropers']) + @pytest.mark.parametrize( + "attr", ["bonds", "angles", "dihedrals", "impropers"] + ) def test_TG_loose_intersection(self, PSFDCD, attr): """Pull bonds from a TG which are at least partially in an AG""" ag = PSFDCD.atoms[10:60] @@ -464,28 +475,30 @@ def manual(topg, atomg): # bonds assert check_strict_intersection(PSFDCD.atoms.bonds, testinput) - assert (manual(PSFDCD.atoms.bonds, testinput) == - set(PSFDCD.atoms.bonds.atomgroup_intersection( - testinput, strict=True))) + assert manual(PSFDCD.atoms.bonds, testinput) == set( + PSFDCD.atoms.bonds.atomgroup_intersection(testinput, strict=True) + ) # angles assert check_strict_intersection(PSFDCD.atoms.angles, testinput) - assert (manual(PSFDCD.atoms.angles, testinput) == - set(PSFDCD.atoms.angles.atomgroup_intersection( - testinput, strict=True))) + assert manual(PSFDCD.atoms.angles, testinput) == set( + PSFDCD.atoms.angles.atomgroup_intersection(testinput, strict=True) + ) # dihedrals assert check_strict_intersection(PSFDCD.atoms.dihedrals, testinput) - assert (manual(PSFDCD.atoms.dihedrals, testinput) == - set(PSFDCD.atoms.dihedrals.atomgroup_intersection( - testinput, strict=True))) + assert manual(PSFDCD.atoms.dihedrals, testinput) == set( + PSFDCD.atoms.dihedrals.atomgroup_intersection( + testinput, strict=True + ) + ) def test_add_TopologyGroups(self, res1, res2, PSFDCD): - res1_tg = res1.atoms.bonds.selectBonds(('23', '3')) - res2_tg = res2.atoms.bonds.selectBonds(('23', '3')) + res1_tg = res1.atoms.bonds.selectBonds(("23", "3")) + res2_tg = res2.atoms.bonds.selectBonds(("23", "3")) combined_tg = res1_tg + res2_tg # add tgs together assert len(combined_tg) == 10 - big_tg = PSFDCD.atoms.bonds.selectBonds(('23', '3')) + big_tg = PSFDCD.atoms.bonds.selectBonds(("23", "3")) big_tg += combined_tg # try and add some already included bonds assert len(big_tg) == 494 # check len doesn't change @@ -501,7 +514,7 @@ def test_add_TO_to_empty_TG(self, PSFDCD): to = PSFDCD.bonds[5] tg3 = tg1 + to - assert_equal(tg3.indices, to.indices[None, :]) + assert_equal(tg3.indices, to.indices[None, :]) def test_add_TG_to_empty_TG(self, PSFDCD): tg1 = PSFDCD.bonds[:0] # empty @@ -548,8 +561,7 @@ def test_TG_getitem_fancy(self, PSFDCD): tg = PSFDCD.atoms.bonds[:10] tg2 = tg[[1, 4, 5]] - manual = TopologyGroup(tg.indices[[1, 4, 5]], - tg.universe, tg.btype) + manual = TopologyGroup(tg.indices[[1, 4, 5]], tg.universe, tg.btype) assert list(tg2) == list(manual) @@ -576,7 +588,7 @@ def test_atom1(self, PSFDCD): a1 = tg.atom1 assert len(tg) == len(a1) - for (atom, bond) in zip(a1, tg): + for atom, bond in zip(a1, tg): assert atom == bond[0] def test_atom2(self, PSFDCD): @@ -584,7 +596,7 @@ def test_atom2(self, PSFDCD): a2 = tg.atom2 assert len(tg) == len(a2) - for (atom, bond) in zip(a2, tg): + for atom, bond in zip(a2, tg): assert atom == bond[1] def test_atom3_IE(self, PSFDCD): @@ -596,7 +608,7 @@ def test_atom3(self, PSFDCD): tg = PSFDCD.angles[:5] a3 = tg.atom3 assert len(tg) == len(a3) - for (atom, bond) in zip(a3, tg): + for atom, bond in zip(a3, tg): assert atom == bond[2] def test_atom4_IE(self, PSFDCD): @@ -609,7 +621,7 @@ def test_atom4(self, PSFDCD): a4 = tg.atom4 assert len(tg) == len(a4) - for (atom, bond) in zip(a4, tg): + for atom, bond in zip(a4, tg): assert atom == bond[3] @@ -619,6 +631,7 @@ class TestTopologyGroup_Cython(object): - work (return proper values) - catch errors """ + @staticmethod @pytest.fixture def bgroup(PSFDCD): @@ -646,20 +659,30 @@ def test_wrong_type_bonds(self, agroup, dgroup, igroup): tg.bonds() def test_right_type_bonds(self, bgroup, PSFDCD): - assert_equal(bgroup.bonds(), - calc_bonds(bgroup.atom1.positions, - bgroup.atom2.positions)) - assert_equal(bgroup.bonds(pbc=True), - calc_bonds(bgroup.atom1.positions, - bgroup.atom2.positions, - box=PSFDCD.dimensions)) - assert_equal(bgroup.values(), - calc_bonds(bgroup.atom1.positions, - bgroup.atom2.positions)) - assert_equal(bgroup.values(pbc=True), - calc_bonds(bgroup.atom1.positions, - bgroup.atom2.positions, - box=PSFDCD.dimensions)) + assert_equal( + bgroup.bonds(), + calc_bonds(bgroup.atom1.positions, bgroup.atom2.positions), + ) + assert_equal( + bgroup.bonds(pbc=True), + calc_bonds( + bgroup.atom1.positions, + bgroup.atom2.positions, + box=PSFDCD.dimensions, + ), + ) + assert_equal( + bgroup.values(), + calc_bonds(bgroup.atom1.positions, bgroup.atom2.positions), + ) + assert_equal( + bgroup.values(pbc=True), + calc_bonds( + bgroup.atom1.positions, + bgroup.atom2.positions, + box=PSFDCD.dimensions, + ), + ) # angles def test_wrong_type_angles(self, bgroup, dgroup, igroup): @@ -668,24 +691,40 @@ def test_wrong_type_angles(self, bgroup, dgroup, igroup): tg.angles() def test_right_type_angles(self, agroup, PSFDCD): - assert_equal(agroup.angles(), - calc_angles(agroup.atom1.positions, - agroup.atom2.positions, - agroup.atom3.positions)) - assert_equal(agroup.angles(pbc=True), - calc_angles(agroup.atom1.positions, - agroup.atom2.positions, - agroup.atom3.positions, - box=PSFDCD.dimensions)) - assert_equal(agroup.values(), - calc_angles(agroup.atom1.positions, - agroup.atom2.positions, - agroup.atom3.positions)) - assert_equal(agroup.values(pbc=True), - calc_angles(agroup.atom1.positions, - agroup.atom2.positions, - agroup.atom3.positions, - box=PSFDCD.dimensions)) + assert_equal( + agroup.angles(), + calc_angles( + agroup.atom1.positions, + agroup.atom2.positions, + agroup.atom3.positions, + ), + ) + assert_equal( + agroup.angles(pbc=True), + calc_angles( + agroup.atom1.positions, + agroup.atom2.positions, + agroup.atom3.positions, + box=PSFDCD.dimensions, + ), + ) + assert_equal( + agroup.values(), + calc_angles( + agroup.atom1.positions, + agroup.atom2.positions, + agroup.atom3.positions, + ), + ) + assert_equal( + agroup.values(pbc=True), + calc_angles( + agroup.atom1.positions, + agroup.atom2.positions, + agroup.atom3.positions, + box=PSFDCD.dimensions, + ), + ) # dihedrals & impropers def test_wrong_type_dihedrals(self, bgroup, agroup): @@ -694,52 +733,84 @@ def test_wrong_type_dihedrals(self, bgroup, agroup): tg.dihedrals() def test_right_type_dihedrals(self, dgroup, PSFDCD): - assert_equal(dgroup.dihedrals(), - calc_dihedrals(dgroup.atom1.positions, - dgroup.atom2.positions, - dgroup.atom3.positions, - dgroup.atom4.positions)) - assert_equal(dgroup.dihedrals(pbc=True), - calc_dihedrals(dgroup.atom1.positions, - dgroup.atom2.positions, - dgroup.atom3.positions, - dgroup.atom4.positions, - box=PSFDCD.dimensions)) - assert_equal(dgroup.values(), - calc_dihedrals(dgroup.atom1.positions, - dgroup.atom2.positions, - dgroup.atom3.positions, - dgroup.atom4.positions)) - assert_equal(dgroup.values(pbc=True), - calc_dihedrals(dgroup.atom1.positions, - dgroup.atom2.positions, - dgroup.atom3.positions, - dgroup.atom4.positions, - box=PSFDCD.dimensions)) + assert_equal( + dgroup.dihedrals(), + calc_dihedrals( + dgroup.atom1.positions, + dgroup.atom2.positions, + dgroup.atom3.positions, + dgroup.atom4.positions, + ), + ) + assert_equal( + dgroup.dihedrals(pbc=True), + calc_dihedrals( + dgroup.atom1.positions, + dgroup.atom2.positions, + dgroup.atom3.positions, + dgroup.atom4.positions, + box=PSFDCD.dimensions, + ), + ) + assert_equal( + dgroup.values(), + calc_dihedrals( + dgroup.atom1.positions, + dgroup.atom2.positions, + dgroup.atom3.positions, + dgroup.atom4.positions, + ), + ) + assert_equal( + dgroup.values(pbc=True), + calc_dihedrals( + dgroup.atom1.positions, + dgroup.atom2.positions, + dgroup.atom3.positions, + dgroup.atom4.positions, + box=PSFDCD.dimensions, + ), + ) def test_right_type_impropers(self, igroup, PSFDCD): - assert_equal(igroup.dihedrals(), - calc_dihedrals(igroup.atom1.positions, - igroup.atom2.positions, - igroup.atom3.positions, - igroup.atom4.positions)) - assert_equal(igroup.dihedrals(pbc=True), - calc_dihedrals(igroup.atom1.positions, - igroup.atom2.positions, - igroup.atom3.positions, - igroup.atom4.positions, - box=PSFDCD.dimensions)) - assert_equal(igroup.values(), - calc_dihedrals(igroup.atom1.positions, - igroup.atom2.positions, - igroup.atom3.positions, - igroup.atom4.positions)) - assert_equal(igroup.values(pbc=True), - calc_dihedrals(igroup.atom1.positions, - igroup.atom2.positions, - igroup.atom3.positions, - igroup.atom4.positions, - box=PSFDCD.dimensions)) + assert_equal( + igroup.dihedrals(), + calc_dihedrals( + igroup.atom1.positions, + igroup.atom2.positions, + igroup.atom3.positions, + igroup.atom4.positions, + ), + ) + assert_equal( + igroup.dihedrals(pbc=True), + calc_dihedrals( + igroup.atom1.positions, + igroup.atom2.positions, + igroup.atom3.positions, + igroup.atom4.positions, + box=PSFDCD.dimensions, + ), + ) + assert_equal( + igroup.values(), + calc_dihedrals( + igroup.atom1.positions, + igroup.atom2.positions, + igroup.atom3.positions, + igroup.atom4.positions, + ), + ) + assert_equal( + igroup.values(pbc=True), + calc_dihedrals( + igroup.atom1.positions, + igroup.atom2.positions, + igroup.atom3.positions, + igroup.atom4.positions, + box=PSFDCD.dimensions, + ), + ) def test_bond_length_pbc(): @@ -752,16 +823,18 @@ def test_bond_length_pbc(): assert_almost_equal(ref, u.bonds[0].length(pbc=True), decimal=6) + def test_cross_universe_eq(): u1 = mda.Universe(PSF) u2 = mda.Universe(PSF) assert not (u1.bonds[0] == u2.bonds[0]) + def test_zero_size_TG_indices_bonds(): u = mda.Universe.empty(10) - u.add_TopologyAttr('bonds', values=[(1, 2), (2, 3)]) + u.add_TopologyAttr("bonds", values=[(1, 2), (2, 3)]) ag = u.atoms[[0]] @@ -770,10 +843,11 @@ def test_zero_size_TG_indices_bonds(): assert idx.shape == (0, 2) assert idx.dtype == np.int32 + def test_zero_size_TG_indices_angles(): u = mda.Universe.empty(10) - u.add_TopologyAttr('angles', values=[(1, 2, 3), (2, 3, 4)]) + u.add_TopologyAttr("angles", values=[(1, 2, 3), (2, 3, 4)]) ag = u.atoms[[0]] diff --git a/testsuite/MDAnalysisTests/core/test_universe.py b/testsuite/MDAnalysisTests/core/test_universe.py index d17b9c707a3..99eaec38a2c 100644 --- a/testsuite/MDAnalysisTests/core/test_universe.py +++ b/testsuite/MDAnalysisTests/core/test_universe.py @@ -40,15 +40,21 @@ from MDAnalysisTests import make_Universe from MDAnalysisTests.datafiles import ( - PSF, DCD, + PSF, + DCD, PSF_BAD, PDB_small, PDB_chainidrepeat, - GRO, TRR, - two_water_gro, two_water_gro_nonames, - TRZ, TRZ_psf, - PDB, MMTF, CONECT, - PDB_conect + GRO, + TRR, + two_water_gro, + two_water_gro_nonames, + TRZ, + TRZ_psf, + PDB, + MMTF, + CONECT, + PDB_conect, ) import MDAnalysis as mda @@ -65,6 +71,7 @@ class IOErrorParser(TopologyReaderBase): def parse(self, **kwargs): raise IOError("Useful information") + # This string is not in the `TestUniverseCreation` class or its method because of problems # with whitespace. Extra indentations make the string unreadable. CHOL_GRO = """\ @@ -81,6 +88,7 @@ def parse(self, **kwargs): 10 10 10 """ + class TestUniverseCreation(object): # tests concerning Universe creation and errors encountered def test_load(self): @@ -89,13 +97,25 @@ def test_load(self): assert_equal(len(u.atoms), 3341, "Loading universe failed somehow") def test_load_topology_stringio(self): - u = mda.Universe(StringIO(CHOL_GRO), format='GRO') - assert_equal(len(u.atoms), 8, "Loading universe from StringIO failed somehow") - assert_equal(u.trajectory.ts.positions[0], np.array([65.580002, 29.360001, 40.050003], dtype=np.float32)) + u = mda.Universe(StringIO(CHOL_GRO), format="GRO") + assert_equal( + len(u.atoms), 8, "Loading universe from StringIO failed somehow" + ) + assert_equal( + u.trajectory.ts.positions[0], + np.array([65.580002, 29.360001, 40.050003], dtype=np.float32), + ) def test_load_trajectory_stringio(self): - u = mda.Universe(StringIO(CHOL_GRO), StringIO(CHOL_GRO), format='GRO', topology_format='GRO') - assert_equal(len(u.atoms), 8, "Loading universe from StringIO failed somehow") + u = mda.Universe( + StringIO(CHOL_GRO), + StringIO(CHOL_GRO), + format="GRO", + topology_format="GRO", + ) + assert_equal( + len(u.atoms), 8, "Loading universe from StringIO failed somehow" + ) def test_make_universe_stringio_no_format(self): # Loading from StringIO without format arg should raise TypeError @@ -106,92 +126,104 @@ def test_Universe_no_trajectory_AE(self): # querying trajectory without a trajectory loaded (only topology) u = make_Universe() with pytest.raises(AttributeError): - getattr(u, 'trajectory') + getattr(u, "trajectory") def test_Universe_topology_unrecognizedformat_VE(self): with pytest.raises(ValueError): - mda.Universe('some.file.without.parser_or_coordinate_extension') + mda.Universe("some.file.without.parser_or_coordinate_extension") def test_Universe_topology_unrecognizedformat_VE_msg(self): try: - mda.Universe('some.file.without.parser_or_coordinate_extension') + mda.Universe("some.file.without.parser_or_coordinate_extension") except ValueError as e: - assert 'isn\'t a valid topology format' in e.args[0] + assert "isn't a valid topology format" in e.args[0] else: raise AssertionError def test_Universe_topology_IE(self): with pytest.raises(IOError): - mda.Universe('thisfile', topology_format = IOErrorParser) + mda.Universe("thisfile", topology_format=IOErrorParser) def test_Universe_topology_IE_msg(self): # should get the original error, as well as Universe error try: - mda.Universe('thisfile', topology_format=IOErrorParser) + mda.Universe("thisfile", topology_format=IOErrorParser) except IOError as e: - assert 'Failed to load from the topology file' in e.args[0] - assert 'Useful information' in e.args[0] + assert "Failed to load from the topology file" in e.args[0] + assert "Useful information" in e.args[0] else: raise AssertionError def test_Universe_filename_IE_msg(self): # check for non existent file try: - mda.Universe('thisfile.xml') + mda.Universe("thisfile.xml") except IOError as e: - assert_equal('No such file or directory', e.strerror) + assert_equal("No such file or directory", e.strerror) else: raise AssertionError def test_Universe_invalidfile_IE_msg(self, tmpdir): # check for invalid file (something with the wrong content) with tmpdir.as_cwd(): - with open('invalid.file.tpr', 'w') as temp_file: - temp_file.write('plop') + with open("invalid.file.tpr", "w") as temp_file: + temp_file.write("plop") try: - mda.Universe('invalid.file.tpr') + mda.Universe("invalid.file.tpr") except IOError as e: - assert 'file or cannot be recognized' in e.args[0] + assert "file or cannot be recognized" in e.args[0] else: raise AssertionError - @pytest.mark.skipif(get_userid() == 0, - reason="cannot permisssionerror as root") + @pytest.mark.skipif( + get_userid() == 0, reason="cannot permisssionerror as root" + ) def test_Universe_invalidpermissionfile_IE_msg(self, tmpdir): # check for file with invalid permissions (eg. no read access) with tmpdir.as_cwd(): - temp_file = 'permission.denied.tpr' - with open(temp_file, 'w'): + temp_file = "permission.denied.tpr" + with open(temp_file, "w"): pass - if os.name == 'nt': - subprocess.call("icacls {filename} /deny Users:RX".format(filename=temp_file), - shell=True) + if os.name == "nt": + subprocess.call( + "icacls {filename} /deny Users:RX".format( + filename=temp_file + ), + shell=True, + ) else: os.chmod(temp_file, 0o200) # Issue #3221 match by PermissionError and error number instead with pytest.raises(PermissionError, match=f"Errno {errno.EACCES}"): - mda.Universe('permission.denied.tpr') + mda.Universe("permission.denied.tpr") def test_load_new_VE(self): u = mda.Universe.empty(0) with pytest.raises(TypeError): - u.load_new('thisfile', format = 'soup') + u.load_new("thisfile", format="soup") def test_load_new_memory_reader_success(self): u = mda.Universe(GRO) - prot = u.select_atoms('protein') + prot = u.select_atoms("protein") u2 = mda.Merge(prot) - assert u2.load_new( [ prot.positions ], format=mda.coordinates.memory.MemoryReader) is u2 + assert ( + u2.load_new( + [prot.positions], format=mda.coordinates.memory.MemoryReader + ) + is u2 + ) def test_load_new_memory_reader_fails(self): def load(): u = mda.Universe(GRO) - prot = u.select_atoms('protein') + prot = u.select_atoms("protein") u2 = mda.Merge(prot) - u2.load_new( [[ prot.positions ]], format=mda.coordinates.memory.MemoryReader) + u2.load_new( + [[prot.positions]], format=mda.coordinates.memory.MemoryReader + ) with pytest.raises(TypeError): load() @@ -200,13 +232,12 @@ def test_universe_kwargs(self): u = mda.Universe(PSF, PDB_small, fake_kwarg=True) assert_equal(len(u.atoms), 3341, "Loading universe failed somehow") - assert u.kwargs['fake_kwarg'] + assert u.kwargs["fake_kwarg"] # initialize new universe from pieces of existing one - u2 = mda.Universe(u.filename, u.trajectory.filename, - **u.kwargs) + u2 = mda.Universe(u.filename, u.trajectory.filename, **u.kwargs) - assert u2.kwargs['fake_kwarg'] + assert u2.kwargs["fake_kwarg"] assert_equal(u.kwargs, u2.kwargs) def test_universe_topology_class_with_coords(self): @@ -229,35 +260,37 @@ def setup_class(self): def test_default(self): smi = "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" - u = mda.Universe.from_smiles(smi, format='RDKIT') + u = mda.Universe.from_smiles(smi, format="RDKIT") assert u.atoms.n_atoms == 24 assert len(u.bonds.indices) == 25 def test_from_bad_smiles(self): with pytest.raises(SyntaxError) as e: - u = mda.Universe.from_smiles("J", format='RDKIT') + u = mda.Universe.from_smiles("J", format="RDKIT") assert "Error while parsing SMILES" in str(e.value) def test_no_Hs(self): smi = "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" - u = mda.Universe.from_smiles(smi, addHs=False, - generate_coordinates=False, format='RDKIT') + 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 def test_gencoords_without_Hs_error(self): with pytest.raises(ValueError) as e: - u = mda.Universe.from_smiles("CCO", addHs=False, - generate_coordinates=True, format='RDKIT') - assert "requires adding hydrogens" in str (e.value) + u = mda.Universe.from_smiles( + "CCO", addHs=False, generate_coordinates=True, format="RDKIT" + ) + assert "requires adding hydrogens" in str(e.value) def test_generate_coordinates_numConfs(self): with pytest.raises(SyntaxError) as e: - u = mda.Universe.from_smiles("CCO", numConfs=0, format='RDKIT') - assert "non-zero positive integer" in str (e.value) + u = mda.Universe.from_smiles("CCO", numConfs=0, format="RDKIT") + assert "non-zero positive integer" in str(e.value) with pytest.raises(SyntaxError) as e: - u = mda.Universe.from_smiles("CCO", numConfs=2.1, format='RDKIT') - assert "non-zero positive integer" in str (e.value) + u = mda.Universe.from_smiles("CCO", numConfs=2.1, format="RDKIT") + assert "non-zero positive integer" in str(e.value) def test_rdkit_kwargs(self): # test for bad kwarg: @@ -269,17 +302,18 @@ def test_rdkit_kwargs(self): except Exception as e: assert "did not match C++ signature" in str(e) else: - raise AssertionError("RDKit should have raised an ArgumentError " - "from Boost") + raise AssertionError( + "RDKit should have raised an ArgumentError " "from Boost" + ) # good kwarg 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, - u2.trajectory.coordinate_array) + assert_equal( + u1.trajectory.coordinate_array, u2.trajectory.coordinate_array + ) assert "Mismatched elements: 15 / 15 (100%)" in str(e.value) - def test_coordinates(self): # We manually create the molecule to compare coordinates # coordinate generation is pseudo random across different machines @@ -287,14 +321,16 @@ def test_coordinates(self): # coordinates against. See PR #4640 from rdkit import Chem from rdkit.Chem import AllChem - mol = Chem.MolFromSmiles('C', sanitize=True) + + mol = Chem.MolFromSmiles("C", sanitize=True) mol = Chem.AddHs(mol) AllChem.EmbedMultipleConfs(mol, numConfs=2, randomSeed=42) expected = [c.GetPositions() for c in mol.GetConformers()] # now the mda way - u = mda.Universe.from_smiles("C", numConfs=2, - rdkit_kwargs=dict(randomSeed=42)) + u = mda.Universe.from_smiles( + "C", numConfs=2, rdkit_kwargs=dict(randomSeed=42) + ) assert u.trajectory.n_frames == 2 assert_allclose(u.trajectory.coordinate_array, expected, rtol=1e-7) @@ -328,7 +364,7 @@ def test_load_new_TypeError(self): u = mda.Universe(PSF, DCD) def bad_load(uni): - return uni.load_new('filename.notarealextension') + return uni.load_new("filename.notarealextension") with pytest.raises(TypeError): bad_load(u) @@ -360,8 +396,7 @@ def test_pickle(self): new_u = pickle.loads(s) assert_equal(u.atoms.names, new_u.atoms.names) - - @pytest.mark.parametrize('dtype', (int, np.float32, np.float64)) + @pytest.mark.parametrize("dtype", (int, np.float32, np.float64)) def test_set_dimensions(self, dtype): u = mda.Universe(PSF, DCD) box = np.array([10, 11, 12, 90, 90, 90], dtype=dtype) @@ -371,19 +406,19 @@ def test_set_dimensions(self, dtype): class TestTransformations(object): - """Tests the transformations keyword - """ + """Tests the transformations keyword""" + def test_callable(self): - u = mda.Universe(PSF,DCD, transformations=translate([10,10,10])) - uref = mda.Universe(PSF,DCD) - ref = translate([10,10,10])(uref.trajectory.ts) + u = mda.Universe(PSF, DCD, transformations=translate([10, 10, 10])) + uref = mda.Universe(PSF, DCD) + ref = translate([10, 10, 10])(uref.trajectory.ts) assert_almost_equal(u.trajectory.ts.positions, ref, decimal=6) def test_list(self): - workflow = [translate([10,10,0]), translate([0,0,10])] - u = mda.Universe(PSF,DCD, transformations=workflow) - uref = mda.Universe(PSF,DCD) - ref = translate([10,10,10])(uref.trajectory.ts) + workflow = [translate([10, 10, 0]), translate([0, 0, 10])] + u = mda.Universe(PSF, DCD, transformations=workflow) + uref = mda.Universe(PSF, DCD) + ref = translate([10, 10, 10])(uref.trajectory.ts) assert_almost_equal(u.trajectory.ts.positions, ref, decimal=6) @@ -395,35 +430,35 @@ def test_automatic_type_and_mass_guessing(self): 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') + 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']) + 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']) + u.guess_TopologyAttrs(to_guess=["trash"]) def test_guess_masses_before_types(self): - u = mda.Universe(PDB_small, to_guess=('masses', 'types')) + 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']) + 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 - """ + """Tests the Mass Guesser in topology.guessers""" + def test_universe_loading_no_warning(self): assert_nowarns(UserWarning, lambda x: mda.Universe(x), GRO) @@ -438,9 +473,10 @@ class TestGuessBonds(object): - fail properly if not - work again if vdwradii are passed. """ - @pytest.fixture(scope='module') + + @pytest.fixture(scope="module") def vdw(self): - return {'A': 1.4, 'B': 0.5} + return {"A": 1.4, "B": 0.5} def _check_universe(self, u): """Verify that the Universe is created correctly""" @@ -453,13 +489,13 @@ def _check_universe(self, u): assert_equal(len(u.atoms[3].bonds), 2) assert_equal(len(u.atoms[4].bonds), 1) assert_equal(len(u.atoms[5].bonds), 1) - assert 'guess_bonds' in u.kwargs + assert "guess_bonds" in u.kwargs def test_universe_guess_bonds(self): """Test that making a Universe with guess_bonds works""" u = mda.Universe(two_water_gro, guess_bonds=True) self._check_universe(u) - assert u.kwargs['guess_bonds'] + assert u.kwargs["guess_bonds"] def test_universe_guess_bonds_no_vdwradii(self): """Make a Universe that has atoms with unknown vdwradii.""" @@ -468,18 +504,17 @@ 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) + u = mda.Universe(two_water_gro_nonames, guess_bonds=True, vdwradii=vdw) self._check_universe(u) - assert u.kwargs['guess_bonds'] - assert_equal(vdw, u.kwargs['vdwradii']) + assert u.kwargs["guess_bonds"] + assert_equal(vdw, u.kwargs["vdwradii"]) def test_universe_guess_bonds_off(self): u = mda.Universe(two_water_gro_nonames, guess_bonds=False) - for attr in ('bonds', 'angles', 'dihedrals'): + for attr in ("bonds", "angles", "dihedrals"): assert not hasattr(u, attr) - assert not u.kwargs['guess_bonds'] + assert not u.kwargs["guess_bonds"] def test_universe_guess_bonds_arguments(self): """Test if 'fudge_factor', and 'lower_bound' parameters @@ -510,11 +545,8 @@ def _check_atomgroup(self, ag, u): assert_equal(len(u.atoms[5].bonds), 0) @pytest.mark.parametrize( - 'ff, lb, nbonds', - [ - (0.55, 0.1, 2), (0.9, 1.6, 1), - (0.5, 0.2, 2), (0.1, 0.1, 0) - ] + "ff, lb, nbonds", + [(0.55, 0.1, 2), (0.9, 1.6, 1), (0.5, 0.2, 2), (0.1, 0.1, 0)], ) def test_atomgroup_guess_bonds(self, ff, lb, nbonds): """Test an atomgroup doing guess bonds""" @@ -554,8 +586,8 @@ def test_guess_bonds_periodicity(self): 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']) + has_bonds = hasattr(u.atoms, "bonds") + u.guess_TopologyAttrs(to_guess=["bonds"]) assert not has_bonds assert u.atoms.bonds @@ -567,157 +599,194 @@ def test_guess_read_bonds(self): class TestInMemoryUniverse(object): def test_reader_w_timeseries(self): universe = mda.Universe(PSF, DCD, in_memory=True) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 98, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 98, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_reader_wo_timeseries(self): universe = mda.Universe(GRO, TRR, in_memory=True) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (47681, 10, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (47681, 10, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_reader_w_timeseries_frame_interval(self): - universe = mda.Universe(PSF, DCD, in_memory=True, - in_memory_step=10) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 10, 3), - err_msg="Unexpected shape of trajectory timeseries") + universe = mda.Universe(PSF, DCD, in_memory=True, in_memory_step=10) + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 10, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_reader_wo_timeseries_frame_interval(self): - universe = mda.Universe(GRO, TRR, in_memory=True, - in_memory_step=3) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (47681, 4, 3), - err_msg="Unexpected shape of trajectory timeseries") + universe = mda.Universe(GRO, TRR, in_memory=True, in_memory_step=3) + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (47681, 4, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_existing_universe(self): universe = mda.Universe(PDB_small, DCD) universe.transfer_to_memory() - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 98, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 98, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_frame_interval_convention(self): universe1 = mda.Universe(PSF, DCD) array1 = universe1.trajectory.timeseries(step=10) - universe2 = mda.Universe(PSF, DCD, in_memory=True, - in_memory_step=10) + universe2 = mda.Universe(PSF, DCD, in_memory=True, in_memory_step=10) array2 = universe2.trajectory.timeseries() - assert_equal(array1, array2, - err_msg="Unexpected differences between arrays.") + assert_equal( + array1, array2, err_msg="Unexpected differences between arrays." + ) def test_slicing_with_start_stop(self): universe = MDAnalysis.Universe(PDB_small, DCD) # Skip only the last frame universe.transfer_to_memory(start=10, stop=20) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 10, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 10, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_slicing_without_start(self): universe = MDAnalysis.Universe(PDB_small, DCD) # Skip only the last frame universe.transfer_to_memory(stop=10) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 10, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 10, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_slicing_without_stop(self): universe = MDAnalysis.Universe(PDB_small, DCD) # Skip only the last frame universe.transfer_to_memory(start=10) print(universe.trajectory.timeseries(universe.atoms).shape) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 88, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 88, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_slicing_step_without_start_stop(self): universe = MDAnalysis.Universe(PDB_small, DCD) # Skip only the last frame universe.transfer_to_memory(step=2) print(universe.trajectory.timeseries(universe.atoms).shape) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 49, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 49, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_slicing_step_with_start_stop(self): universe = MDAnalysis.Universe(PDB_small, DCD) # Skip only the last frame universe.transfer_to_memory(start=10, stop=30, step=2) print(universe.trajectory.timeseries(universe.atoms).shape) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 10, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 10, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_slicing_step_dt(self): universe = MDAnalysis.Universe(PDB_small, DCD) dt = universe.trajectory.dt universe.transfer_to_memory(step=2) - assert_almost_equal(dt * 2, universe.trajectory.dt, - err_msg="Unexpected in-memory timestep: " - + "dt not updated with step information") + assert_almost_equal( + dt * 2, + universe.trajectory.dt, + err_msg="Unexpected in-memory timestep: " + + "dt not updated with step information", + ) def test_slicing_negative_start(self): universe = MDAnalysis.Universe(PDB_small, DCD) # Skip only the last frame universe.transfer_to_memory(start=-10) print(universe.trajectory.timeseries(universe.atoms).shape) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 10, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 10, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_slicing_negative_stop(self): universe = MDAnalysis.Universe(PDB_small, DCD) # Skip only the last frame universe.transfer_to_memory(stop=-20) print(universe.trajectory.timeseries(universe.atoms).shape) - assert_equal(universe.trajectory.timeseries(universe.atoms).shape, - (3341, 78, 3), - err_msg="Unexpected shape of trajectory timeseries") + assert_equal( + universe.trajectory.timeseries(universe.atoms).shape, + (3341, 78, 3), + err_msg="Unexpected shape of trajectory timeseries", + ) def test_transfer_to_memory_kwargs(self): u = mda.Universe(PSF, DCD) u.transfer_to_memory(example_kwarg=True) - assert(u.trajectory._kwargs['example_kwarg']) + assert u.trajectory._kwargs["example_kwarg"] def test_in_memory_kwargs(self): u = mda.Universe(PSF, DCD, in_memory=True, example_kwarg=True) - assert(u.trajectory._kwargs['example_kwarg']) + assert u.trajectory._kwargs["example_kwarg"] + class TestCustomReaders(object): """ Can pass a reader as kwarg on Universe creation """ + def test_custom_reader(self): # check that reader passing works - u = mda.Universe(TRZ_psf, TRZ, format=MDAnalysis.coordinates.TRZ.TRZReader) + u = mda.Universe( + TRZ_psf, TRZ, format=MDAnalysis.coordinates.TRZ.TRZReader + ) assert_equal(len(u.atoms), 8184) def test_custom_reader_singleframe(self): T = MDAnalysis.topology.GROParser.GROParser R = MDAnalysis.coordinates.GRO.GROReader - u = mda.Universe(two_water_gro, two_water_gro, - topology_format=T, format=R) + u = mda.Universe( + two_water_gro, two_water_gro, topology_format=T, format=R + ) assert_equal(len(u.atoms), 6) def test_custom_reader_singleframe_2(self): # Same as before, but only one argument to Universe T = MDAnalysis.topology.GROParser.GROParser R = MDAnalysis.coordinates.GRO.GROReader - u = mda.Universe(two_water_gro, - topology_format=T, format=R) + u = mda.Universe(two_water_gro, topology_format=T, format=R) assert_equal(len(u.atoms), 6) def test_custom_parser(self): # topology reader passing works - u = mda.Universe(TRZ_psf, TRZ, topology_format=MDAnalysis.topology.PSFParser.PSFParser) + u = mda.Universe( + TRZ_psf, + TRZ, + topology_format=MDAnalysis.topology.PSFParser.PSFParser, + ) assert_equal(len(u.atoms), 8184) def test_custom_both(self): # use custom for both - u = mda.Universe(TRZ_psf, TRZ, format=MDAnalysis.coordinates.TRZ.TRZReader, - topology_format=MDAnalysis.topology.PSFParser.PSFParser) + u = mda.Universe( + TRZ_psf, + TRZ, + format=MDAnalysis.coordinates.TRZ.TRZReader, + topology_format=MDAnalysis.topology.PSFParser.PSFParser, + ) assert_equal(len(u.atoms), 8184) @@ -728,34 +797,50 @@ def universe(self): def test_add_TA_fail(self, universe): with pytest.raises(ValueError): - universe.add_TopologyAttr('silly') + universe.add_TopologyAttr("silly") def test_nodefault_fail(self, universe): with pytest.raises(NotImplementedError): - universe.add_TopologyAttr('bonds') + universe.add_TopologyAttr("bonds") @pytest.mark.parametrize( - 'toadd,attrname,default', ( - ['charge', 'charges', 0.0], ['charges', 'charges', 0.0], - ['name', 'names', ''], ['names', 'names', ''], - ['type', 'types', ''], ['types', 'types', ''], - ['element', 'elements', ''], ['elements', 'elements', ''], - ['radius', 'radii', 0.0], ['radii', 'radii', 0.0], - ['chainID', 'chainIDs', ''], ['chainIDs', 'chainIDs', ''], - ['tempfactor', 'tempfactors', 0.0], - ['tempfactors', 'tempfactors', 0.0], - ['mass', 'masses', 0.0], ['masses', 'masses', 0.0], - ['charge', 'charges', 0.0], ['charges', 'charges', 0.0], - ['bfactor', 'bfactors', 0.0], ['bfactors', 'bfactors', 0.0], - ['occupancy', 'occupancies', 0.0], - ['occupancies', 'occupancies', 0.0], - ['altLoc', 'altLocs', ''], ['altLocs', 'altLocs', ''], - ['resid', 'resids', 1], ['resids', 'resids', 1], - ['resname', 'resnames', ''], ['resnames', 'resnames', ''], - ['resnum', 'resnums', 1], ['resnums', 'resnums', 1], - ['icode', 'icodes', ''], ['icodes', 'icodes', ''], - ['segid', 'segids', ''], ['segids', 'segids', ''], - ) + "toadd,attrname,default", + ( + ["charge", "charges", 0.0], + ["charges", "charges", 0.0], + ["name", "names", ""], + ["names", "names", ""], + ["type", "types", ""], + ["types", "types", ""], + ["element", "elements", ""], + ["elements", "elements", ""], + ["radius", "radii", 0.0], + ["radii", "radii", 0.0], + ["chainID", "chainIDs", ""], + ["chainIDs", "chainIDs", ""], + ["tempfactor", "tempfactors", 0.0], + ["tempfactors", "tempfactors", 0.0], + ["mass", "masses", 0.0], + ["masses", "masses", 0.0], + ["charge", "charges", 0.0], + ["charges", "charges", 0.0], + ["bfactor", "bfactors", 0.0], + ["bfactors", "bfactors", 0.0], + ["occupancy", "occupancies", 0.0], + ["occupancies", "occupancies", 0.0], + ["altLoc", "altLocs", ""], + ["altLocs", "altLocs", ""], + ["resid", "resids", 1], + ["resids", "resids", 1], + ["resname", "resnames", ""], + ["resnames", "resnames", ""], + ["resnum", "resnums", 1], + ["resnums", "resnums", 1], + ["icode", "icodes", ""], + ["icodes", "icodes", ""], + ["segid", "segids", ""], + ["segids", "segids", ""], + ), ) def test_add_charges(self, universe, toadd, attrname, default): universe.add_TopologyAttr(toadd) @@ -764,14 +849,15 @@ def test_add_charges(self, universe, toadd, attrname, default): assert getattr(universe.atoms, attrname)[0] == default @pytest.mark.parametrize( - 'attr,values', ( - ('bonds', [(1, 0), (1, 2)]), - ('bonds', [[1, 0], [1, 2]]), - ('bonds', set([(1, 0), (1, 2)])), - ('angles', [(1, 0, 2), (1, 2, 3), (2, 1, 4)]), - ('dihedrals', [[1, 2, 3, 1], (3, 1, 5, 2)]), - ('impropers', [[1, 2, 3, 1], (3, 1, 5, 2)]), - ) + "attr,values", + ( + ("bonds", [(1, 0), (1, 2)]), + ("bonds", [[1, 0], [1, 2]]), + ("bonds", set([(1, 0), (1, 2)])), + ("angles", [(1, 0, 2), (1, 2, 3), (2, 1, 4)]), + ("dihedrals", [[1, 2, 3, 1], (3, 1, 5, 2)]), + ("impropers", [[1, 2, 3, 1], (3, 1, 5, 2)]), + ), ) def test_add_connection(self, universe, attr, values): universe.add_TopologyAttr(attr, values) @@ -783,16 +869,17 @@ def test_add_connection(self, universe, attr, values): assert ix[0] <= ix[-1] @pytest.mark.parametrize( - 'attr,values', ( - ('bonds', [(1, 0, 0), (1, 2)]), - ('bonds', [['x', 'y'], [1, 2]]), - ('bonds', 'rubbish'), - ('bonds', [[1.01, 2.0]]), - ('angles', [(1, 0), (1, 2)]), - ('angles', 'rubbish'), - ('dihedrals', [[1, 1, 1, 0.1]]), - ('impropers', [(1, 2, 3)]), - ) + "attr,values", + ( + ("bonds", [(1, 0, 0), (1, 2)]), + ("bonds", [["x", "y"], [1, 2]]), + ("bonds", "rubbish"), + ("bonds", [[1.01, 2.0]]), + ("angles", [(1, 0), (1, 2)]), + ("angles", "rubbish"), + ("dihedrals", [[1, 1, 1, 0.1]]), + ("impropers", [(1, 2, 3)]), + ), ) def test_add_connection_error(self, universe, attr, values): with pytest.raises(ValueError): @@ -800,7 +887,9 @@ def test_add_connection_error(self, universe, 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)) + universe.add_TopologyAttr( + "masses", np.array([1, 2, 3], dtype=np.float64) + ) class TestDelTopologyAttr(object): @@ -812,7 +901,7 @@ def universe(self): def test_del_TA_fail(self, universe): with pytest.raises(ValueError, match="Unrecognised"): - universe.del_TopologyAttr('silly') + universe.del_TopologyAttr("silly") def test_absent_fail(self, universe): with pytest.raises(ValueError, match="not in Universe"): @@ -823,11 +912,12 @@ def test_wrongtype_fail(self, universe): universe.del_TopologyAttr(list) @pytest.mark.parametrize( - 'todel,attrname', [ + "todel,attrname", + [ ("charge", "charges"), ("charges", "charges"), ("bonds", "bonds"), - ] + ], ) def test_del_str(self, universe, todel, attrname): assert hasattr(universe.atoms, attrname) @@ -873,8 +963,7 @@ class RootVegetable(AtomStringAttr): transplants = defaultdict(list) def potatoes(self): - """🥔 - """ + """🥔""" return "potoooooooo" transplants["Universe"].append(("potatoes", potatoes)) @@ -890,22 +979,23 @@ def _a_or_reversed_in_b(a, b): """ Check if smaller array ``a`` or ``a[::-1]`` is in b """ - return (a==b).all(1).any() or (a[::-1]==b).all(1).any() + return (a == b).all(1).any() or (a[::-1] == b).all(1).any() + class TestAddTopologyObjects(object): small_atom_indices = ( - ('bonds', [[0, 1], [2, 3]]), - ('angles', [[0, 1, 2], [3, 4, 5]]), - ('dihedrals', [[8, 22, 1, 3], [4, 5, 6, 7], [11, 2, 3, 13]]), - ('impropers', [[1, 6, 7, 2], [5, 3, 4, 2]]), + ("bonds", [[0, 1], [2, 3]]), + ("angles", [[0, 1, 2], [3, 4, 5]]), + ("dihedrals", [[8, 22, 1, 3], [4, 5, 6, 7], [11, 2, 3, 13]]), + ("impropers", [[1, 6, 7, 2], [5, 3, 4, 2]]), ) large_atom_indices = ( - ('bonds', [[0, 111], [22, 3]]), - ('angles', [[0, 111, 2], [3, 44, 5]]), - ('dihedrals', [[8, 222, 1, 3], [44, 5, 6, 7], [111, 2, 3, 13]]), - ('impropers', [[1, 6, 771, 2], [5, 3, 433, 2]]), + ("bonds", [[0, 111], [22, 3]]), + ("angles", [[0, 111, 2], [3, 44, 5]]), + ("dihedrals", [[8, 222, 1, 3], [44, 5, 6, 7], [111, 2, 3, 13]]), + ("impropers", [[1, 6, 771, 2], [5, 3, 433, 2]]), ) @pytest.fixture() @@ -918,189 +1008,203 @@ def universe(self): def _check_valid_added_to_empty(self, u, attr, values, to_add): assert not hasattr(u, attr) - _add_func = getattr(u, 'add_'+attr) + _add_func = getattr(u, "add_" + attr) _add_func(to_add) u_attr = getattr(u, attr) assert len(u_attr) == len(values) - assert all(_a_or_reversed_in_b(x, u_attr.indices) - for x in values) + assert all(_a_or_reversed_in_b(x, u_attr.indices) for x in values) def _check_valid_added_to_populated(self, u, attr, values, to_add): assert hasattr(u, attr) u_attr = getattr(u, attr) original_length = len(u_attr) - _add_func = getattr(u, 'add_'+attr) + _add_func = getattr(u, "add_" + attr) _add_func(to_add) u_attr = getattr(u, attr) assert len(u_attr) == len(values) + original_length - assert all(_a_or_reversed_in_b(x, u_attr.indices) - for x in values) + assert all(_a_or_reversed_in_b(x, u_attr.indices) for x in values) def _check_invalid_addition(self, u, attr, to_add, err_msg): - _add_func = getattr(u, 'add_'+attr) + _add_func = getattr(u, "add_" + attr) with pytest.raises(ValueError) as excinfo: _add_func(to_add) assert err_msg in str(excinfo.value) - @pytest.mark.parametrize( - 'attr,values', small_atom_indices - ) + @pytest.mark.parametrize("attr,values", small_atom_indices) def test_add_indices_to_empty(self, empty, attr, values): self._check_valid_added_to_empty(empty, attr, values, values) def test_add_reversed_duplicates(self, empty): - assert not hasattr(empty, 'bonds') + assert not hasattr(empty, "bonds") empty.add_bonds([[0, 1], [1, 0]]) assert len(empty.bonds) == 1 assert_array_equal(empty.bonds.indices, np.array([[0, 1]])) - @pytest.mark.parametrize( - 'attr,values', large_atom_indices - ) + @pytest.mark.parametrize("attr,values", large_atom_indices) def test_add_indices_to_populated(self, universe, attr, values): self._check_valid_added_to_populated(universe, attr, values, values) - @pytest.mark.parametrize( - 'attr,values', small_atom_indices - ) + @pytest.mark.parametrize("attr,values", small_atom_indices) def test_add_atomgroup_to_empty(self, empty, attr, values): ag = [empty.atoms[x] for x in values] self._check_valid_added_to_empty(empty, attr, values, ag) - @pytest.mark.parametrize( - 'attr,values', large_atom_indices - ) + @pytest.mark.parametrize("attr,values", large_atom_indices) def test_add_atomgroup_to_populated(self, universe, attr, values): ag = [universe.atoms[x] for x in values] self._check_valid_added_to_populated(universe, attr, values, ag) - @pytest.mark.parametrize( - 'attr,values', small_atom_indices - ) - def test_add_atomgroup_wrong_universe_error(self, universe, empty, attr, values): + @pytest.mark.parametrize("attr,values", small_atom_indices) + def test_add_atomgroup_wrong_universe_error( + self, universe, empty, attr, values + ): ag = [empty.atoms[x] for x in values] - self._check_invalid_addition(universe, attr, ag, 'different Universes') + self._check_invalid_addition(universe, attr, ag, "different Universes") - @pytest.mark.parametrize( - 'attr,values', large_atom_indices - ) + @pytest.mark.parametrize("attr,values", large_atom_indices) def test_add_topologyobjects_to_populated(self, universe, attr, values): - topologyobjects = [getattr(universe.atoms[x], attr[:-1]) for x in values] - self._check_valid_added_to_populated(universe, attr, values, topologyobjects) + topologyobjects = [ + getattr(universe.atoms[x], attr[:-1]) for x in values + ] + self._check_valid_added_to_populated( + universe, attr, values, topologyobjects + ) - @pytest.mark.parametrize( - 'attr,values', small_atom_indices - ) - def test_add_topologyobjects_wrong_universe_error(self, universe, empty, attr, values): + @pytest.mark.parametrize("attr,values", small_atom_indices) + def test_add_topologyobjects_wrong_universe_error( + self, universe, empty, attr, values + ): tobj = [getattr(universe.atoms[x], attr[:-1]) for x in values] - self._check_invalid_addition(empty, attr, tobj, 'different Universes') + self._check_invalid_addition(empty, attr, tobj, "different Universes") - @pytest.mark.parametrize( - 'attr,values', large_atom_indices - ) + @pytest.mark.parametrize("attr,values", large_atom_indices) def test_add_topologygroups_to_populated(self, universe, attr, values): - topologygroup = mda.core.topologyobjects.TopologyGroup(np.array(values), - universe) - self._check_valid_added_to_populated(universe, attr, values, topologygroup) - - @pytest.mark.parametrize( - 'attr,values', small_atom_indices - ) - def test_add_topologygroup_wrong_universe_error(self, universe, empty, attr, values): - tg = mda.core.topologyobjects.TopologyGroup(np.array(values), - universe) - self._check_invalid_addition(empty, attr, tg, 'different Universes') + topologygroup = mda.core.topologyobjects.TopologyGroup( + np.array(values), universe + ) + self._check_valid_added_to_populated( + universe, attr, values, topologygroup + ) - @pytest.mark.parametrize( - 'attr,values', small_atom_indices - ) - def test_add_topologygroup_different_universe(self, universe, empty, attr, values): - tg = mda.core.topologyobjects.TopologyGroup(np.array(values), - universe) + @pytest.mark.parametrize("attr,values", small_atom_indices) + def test_add_topologygroup_wrong_universe_error( + self, universe, empty, attr, values + ): + tg = mda.core.topologyobjects.TopologyGroup(np.array(values), universe) + self._check_invalid_addition(empty, attr, tg, "different Universes") + + @pytest.mark.parametrize("attr,values", small_atom_indices) + def test_add_topologygroup_different_universe( + self, universe, empty, attr, values + ): + tg = mda.core.topologyobjects.TopologyGroup(np.array(values), universe) self._check_valid_added_to_empty(empty, attr, values, tg.to_indices()) @pytest.mark.parametrize( - 'attr,values', ( - ('impropers', [[0, 111], [22, 3]]), - ('dihedrals', [[0, 111, 2], [3, 44, 5]]), - ('angles', [[8, 222, 1, 3], [44, 5, 6, 7], [111, 2, 3, 13]]), - ('bonds', [[1, 6, 771, 2], [5, 3, 433, 2]]), - ) + "attr,values", + ( + ("impropers", [[0, 111], [22, 3]]), + ("dihedrals", [[0, 111, 2], [3, 44, 5]]), + ("angles", [[8, 222, 1, 3], [44, 5, 6, 7], [111, 2, 3, 13]]), + ("bonds", [[1, 6, 771, 2], [5, 3, 433, 2]]), + ), ) def test_add_wrong_topologygroup_error(self, universe, attr, values): arr = np.array(values) tg = mda.core.topologyobjects.TopologyGroup(arr, universe) - self._check_invalid_addition(universe, attr, tg, 'iterable of tuples with') + self._check_invalid_addition( + universe, attr, tg, "iterable of tuples with" + ) @pytest.mark.parametrize( - 'attr,values', ( - ('bonds', [[0, -111], [22, 3]]), - ('angles', [[0, 11111, 2], [3, 44, 5]]), - ('dihedrals', [[8, 222, 28888, 3], [44, 5, 6, 7], [111, 2, 3, 13]]), - ('impropers', [[1, 6, 77133, 2], [5, 3, 433, 2]]), - ) + "attr,values", + ( + ("bonds", [[0, -111], [22, 3]]), + ("angles", [[0, 11111, 2], [3, 44, 5]]), + ( + "dihedrals", + [[8, 222, 28888, 3], [44, 5, 6, 7], [111, 2, 3, 13]], + ), + ("impropers", [[1, 6, 77133, 2], [5, 3, 433, 2]]), + ), ) def test_add_nonexistent_indices_error(self, universe, attr, values): - self._check_invalid_addition(universe, attr, values, 'nonexistent atom indices') + self._check_invalid_addition( + universe, attr, values, "nonexistent atom indices" + ) @pytest.mark.parametrize( - 'attr,n', ( - ('bonds', 2), - ('angles', 3), - ('dihedrals', 4), - ('impropers', 4), - ) + "attr,n", + ( + ("bonds", 2), + ("angles", 3), + ("dihedrals", 4), + ("impropers", 4), + ), ) def test_add_wrong_number_of_atoms_error(self, universe, attr, n): - errmsg = ('{} must be an iterable of ' - 'tuples with {} atom indices').format(attr, n) + errmsg = ( + "{} must be an iterable of " "tuples with {} atom indices" + ).format(attr, n) idx = [(0, 1), (0, 1, 2), (8, 22, 1, 3), (5, 3, 4, 2)] self._check_invalid_addition(universe, attr, idx, errmsg) def test_add_bonds_refresh_fragments(self, empty): with pytest.raises(NoDataError): - getattr(empty.atoms, 'fragments') + getattr(empty.atoms, "fragments") empty.add_bonds([empty.atoms[:2]]) - assert len(empty.atoms.fragments) == len(empty.atoms)-1 + assert len(empty.atoms.fragments) == len(empty.atoms) - 1 empty.add_bonds([empty.atoms[2:4]]) - assert len(empty.atoms.fragments) == len(empty.atoms)-2 + assert len(empty.atoms.fragments) == len(empty.atoms) - 2 - @pytest.mark.parametrize( - 'attr,values', small_atom_indices - ) + @pytest.mark.parametrize("attr,values", small_atom_indices) def test_roundtrip(self, empty, attr, values): - _add_func = getattr(empty, 'add_'+attr) + _add_func = getattr(empty, "add_" + attr) _add_func(values) u_attr = getattr(empty, attr) assert len(u_attr) == len(values) - _delete_func = getattr(empty, 'delete_'+attr) + _delete_func = getattr(empty, "delete_" + attr) _delete_func(values) u_attr = getattr(empty, attr) assert len(u_attr) == 0 + class TestDeleteTopologyObjects(object): - TOP = {'bonds': [(0, 1), (2, 3), (3, 4), (4, 5), (7, 8)], - 'angles': [(0, 1, 2), (3, 4, 5), (8, 2, 4)], - 'dihedrals': [(9, 2, 3, 4), (1, 3, 4, 2), (8, 22, 1, 3), (4, 5, 6, 7), (11, 2, 3, 13)], - 'impropers': [(1, 3, 5, 2), (1, 6, 7, 2), (5, 3, 4, 2)]} + TOP = { + "bonds": [(0, 1), (2, 3), (3, 4), (4, 5), (7, 8)], + "angles": [(0, 1, 2), (3, 4, 5), (8, 2, 4)], + "dihedrals": [ + (9, 2, 3, 4), + (1, 3, 4, 2), + (8, 22, 1, 3), + (4, 5, 6, 7), + (11, 2, 3, 13), + ], + "impropers": [(1, 3, 5, 2), (1, 6, 7, 2), (5, 3, 4, 2)], + } existing_atom_indices = ( - ('bonds', [[0, 1], [2, 3]]), - ('angles', [[0, 1, 2], [3, 4, 5]]), - ('dihedrals', [[8, 22, 1, 3], [4, 5, 6, 7], [11, 2, 3, 13]]), - ('impropers', [[1, 6, 7, 2], [5, 3, 4, 2]]), - ) + ("bonds", [[0, 1], [2, 3]]), + ("angles", [[0, 1, 2], [3, 4, 5]]), + ("dihedrals", [[8, 22, 1, 3], [4, 5, 6, 7], [11, 2, 3, 13]]), + ("impropers", [[1, 6, 7, 2], [5, 3, 4, 2]]), + ) nonexisting_atom_indices = ( - ('bonds', [[2, 3], [7, 8], [0, 4]]), - ('angles', [[0, 1, 2], [8, 2, 8], [1, 1, 1]]), - ('dihedrals', [[0, 0, 0, 0], [1, 1, 1, 1]]), - ('impropers', [[8, 22, 1, 3],]), - ) + ("bonds", [[2, 3], [7, 8], [0, 4]]), + ("angles", [[0, 1, 2], [8, 2, 8], [1, 1, 1]]), + ("dihedrals", [[0, 0, 0, 0], [1, 1, 1, 1]]), + ( + "impropers", + [ + [8, 22, 1, 3], + ], + ), + ) @pytest.fixture() def universe(self): @@ -1121,124 +1225,120 @@ def _check_valid_deleted(self, u, attr, values, to_delete): original_length = len(self.TOP[attr]) assert len(u_attr) == original_length - _delete_func = getattr(u, 'delete_'+attr) + _delete_func = getattr(u, "delete_" + attr) _delete_func(to_delete) u_attr = getattr(u, attr) - assert len(u_attr) == original_length-len(values) + assert len(u_attr) == original_length - len(values) not_deleted = [x for x in self.TOP[attr] if list(x) not in values] - assert all([x in u_attr.indices or x[::-1] in u_attr.indices - for x in not_deleted]) + assert all( + [ + x in u_attr.indices or x[::-1] in u_attr.indices + for x in not_deleted + ] + ) def _check_invalid_deleted(self, u, attr, to_delete, err_msg): u_attr = getattr(u, attr) original_length = len(self.TOP[attr]) assert len(u_attr) == original_length - _delete_func = getattr(u, 'delete_'+attr) + _delete_func = getattr(u, "delete_" + attr) with pytest.raises(ValueError) as excinfo: _delete_func(to_delete) assert err_msg in str(excinfo.value) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) + @pytest.mark.parametrize("attr,values", existing_atom_indices) def test_delete_valid_indices(self, universe, attr, values): self._check_valid_deleted(universe, attr, values, values) - @pytest.mark.parametrize( - 'attr,values', nonexisting_atom_indices - ) + @pytest.mark.parametrize("attr,values", nonexisting_atom_indices) def test_delete_missing_indices(self, universe, attr, values): - self._check_invalid_deleted(universe, attr, values, 'Cannot delete nonexistent') + self._check_invalid_deleted( + universe, attr, values, "Cannot delete nonexistent" + ) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) + @pytest.mark.parametrize("attr,values", existing_atom_indices) def test_delete_valid_atomgroup(self, universe, attr, values): ag = [universe.atoms[x] for x in values] self._check_valid_deleted(universe, attr, values, ag) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) - def test_delete_atomgroup_wrong_universe_error(self, universe, universe2, attr, values): + @pytest.mark.parametrize("attr,values", existing_atom_indices) + def test_delete_atomgroup_wrong_universe_error( + self, universe, universe2, attr, values + ): ag = [universe.atoms[x] for x in values] - self._check_invalid_deleted(universe2, attr, ag, 'different Universes') + self._check_invalid_deleted(universe2, attr, ag, "different Universes") - @pytest.mark.parametrize( - 'attr,values', nonexisting_atom_indices - ) + @pytest.mark.parametrize("attr,values", nonexisting_atom_indices) def test_delete_missing_atomgroup(self, universe, attr, values): ag = [universe.atoms[x] for x in values] - self._check_invalid_deleted(universe, attr, ag, 'Cannot delete nonexistent') + self._check_invalid_deleted( + universe, attr, ag, "Cannot delete nonexistent" + ) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) + @pytest.mark.parametrize("attr,values", existing_atom_indices) def test_delete_mixed_type(self, universe, attr, values): mixed = [universe.atoms[values[0]]] + values[1:] self._check_valid_deleted(universe, attr, values, mixed) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) + @pytest.mark.parametrize("attr,values", existing_atom_indices) def test_delete_valid_topologyobjects(self, universe, attr, values): to = [getattr(universe.atoms[x], attr[:-1]) for x in values] self._check_valid_deleted(universe, attr, values, to) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) - def test_delete_topologyobjects_wrong_universe(self, universe, universe2, attr, values): + @pytest.mark.parametrize("attr,values", existing_atom_indices) + def test_delete_topologyobjects_wrong_universe( + self, universe, universe2, attr, values + ): u1 = [getattr(universe.atoms[x], attr[:-1]) for x in values[:-1]] u2 = [getattr(universe2.atoms[values[-1]], attr[:-1])] - self._check_invalid_deleted(universe, attr, u1+u2, 'different Universes') + self._check_invalid_deleted( + universe, attr, u1 + u2, "different Universes" + ) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) + @pytest.mark.parametrize("attr,values", existing_atom_indices) def test_delete_valid_topologygroup(self, universe, attr, values): arr = np.array(values) tg = mda.core.topologyobjects.TopologyGroup(arr, universe) self._check_valid_deleted(universe, attr, values, tg) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) - def test_delete_topologygroup_wrong_universe_error(self, universe, universe2, attr, values): + @pytest.mark.parametrize("attr,values", existing_atom_indices) + def test_delete_topologygroup_wrong_universe_error( + self, universe, universe2, attr, values + ): arr = np.array(values) tg = mda.core.topologyobjects.TopologyGroup(arr, universe2) - self._check_invalid_deleted(universe, attr, tg, 'different Universes') + self._check_invalid_deleted(universe, attr, tg, "different Universes") - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) - def test_delete_topologygroup_different_universe(self, universe, universe2, attr, values): + @pytest.mark.parametrize("attr,values", existing_atom_indices) + def test_delete_topologygroup_different_universe( + self, universe, universe2, attr, values + ): arr = np.array(values) tg = mda.core.topologyobjects.TopologyGroup(arr, universe2) self._check_valid_deleted(universe, attr, values, tg.to_indices()) @pytest.mark.parametrize( - 'attr,n', ( - ('bonds', 2), - ('angles', 3), - ('dihedrals', 4), - ('impropers', 4), - ) + "attr,n", + ( + ("bonds", 2), + ("angles", 3), + ("dihedrals", 4), + ("impropers", 4), + ), ) def test_delete_wrong_number_of_atoms_error(self, universe, attr, n): idx = [(0, 1), (0, 1, 2), (8, 22, 1, 3), (5, 3, 4, 2)] - errmsg = ('{} must be an iterable of ' - 'tuples with {} atom indices').format(attr, n) + errmsg = ( + "{} must be an iterable of " "tuples with {} atom indices" + ).format(attr, n) self._check_invalid_deleted(universe, attr, idx, errmsg) - @pytest.mark.parametrize( - 'attr,values', existing_atom_indices - ) + @pytest.mark.parametrize("attr,values", existing_atom_indices) def test_delete_missing_attr(self, attr, values): u = make_Universe() assert not hasattr(u, attr) - _delete_func = getattr(u, 'delete_'+attr) + _delete_func = getattr(u, "delete_" + attr) with pytest.raises(ValueError) as excinfo: _delete_func(values) assert "There are no" in str(excinfo.value) @@ -1248,30 +1348,27 @@ 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) - ]) + @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 - ) + @pytest.mark.parametrize("attr,values", existing_atom_indices) def test_roundtrip(self, universe, attr, values): u_attr = getattr(universe, attr) original_length = len(self.TOP[attr]) assert len(u_attr) == original_length - _delete_func = getattr(universe, 'delete_'+attr) + _delete_func = getattr(universe, "delete_" + attr) _delete_func(values) nu_attr = getattr(universe, attr) - assert len(nu_attr) == original_length-len(values) + assert len(nu_attr) == original_length - len(values) - _add_func = getattr(universe, 'add_'+attr) + _add_func = getattr(universe, "add_" + attr) _add_func(values) nu_attr = getattr(universe, attr) assert len(nu_attr) == original_length @@ -1280,36 +1377,39 @@ def test_roundtrip(self, universe, attr, values): class TestAllCoordinatesKwarg(object): - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u_GRO_TRR(self): return mda.Universe(GRO, TRR) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u_GRO_TRR_allcoords(self): return mda.Universe(GRO, TRR, all_coordinates=True) - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def u_GRO(self): return mda.Universe(GRO) def test_all_coordinates_length(self, u_GRO_TRR, u_GRO_TRR_allcoords): # length with all_coords should be +1 - assert (len(u_GRO_TRR.trajectory) + 1 == - len(u_GRO_TRR_allcoords.trajectory)) + assert len(u_GRO_TRR.trajectory) + 1 == len( + u_GRO_TRR_allcoords.trajectory + ) def test_all_coordinates_frame(self, u_GRO_TRR_allcoords, u_GRO): # check that first frame in u(GRO, TRR, allcords) # are the coordinates from GRO - assert_array_equal(u_GRO_TRR_allcoords.atoms.positions, - u_GRO.atoms.positions) + assert_array_equal( + u_GRO_TRR_allcoords.atoms.positions, u_GRO.atoms.positions + ) def test_second_frame(self, u_GRO_TRR_allcoords, u_GRO_TRR): # check that second frame in u(GRO, TRR, allcoords) # are the coordinates from TRR[0] u_GRO_TRR_allcoords.trajectory[1] - assert_array_equal(u_GRO_TRR_allcoords.atoms.positions, - u_GRO_TRR.atoms.positions) + assert_array_equal( + u_GRO_TRR_allcoords.atoms.positions, u_GRO_TRR.atoms.positions + ) class TestEmpty(object): @@ -1322,10 +1422,10 @@ def test_empty(self): def test_empty_extra(self): u = mda.Universe.empty( - n_atoms=12, n_residues=3, n_segments=2, - atom_resindex=np.array([0, 0, 0, 0, 0, - 1, 1, 1, 1, 1, - 2, 2]), + n_atoms=12, + n_residues=3, + n_segments=2, + atom_resindex=np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2]), residue_segindex=np.array([0, 0, 1]), ) @@ -1345,12 +1445,12 @@ def test_no_resindex_warning(self): u = mda.Universe.empty(n_atoms=10, n_residues=2, n_segments=1) def test_no_segindex_warning(self): - res = np.array([0, 0, 0, 0, 0, - 1, 1, 1, 1, 1]) + res = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) with pytest.warns(UserWarning): - u = mda.Universe.empty(n_atoms=10, n_residues=2, n_segments=2, - atom_resindex=res) + u = mda.Universe.empty( + n_atoms=10, n_residues=2, n_segments=2, atom_resindex=res + ) def test_no_trivial_warning(self): """ @@ -1376,7 +1476,7 @@ def test_trajectory_iteration(self): u = mda.Universe.empty(10, trajectory=True) assert len(u.trajectory) == 1 - timesteps =[] + timesteps = [] for ts in u.trajectory: timesteps.append(ts.frame) assert len(timesteps) == 1 @@ -1402,7 +1502,7 @@ def test_empty_no_atoms(self): def test_empty_creation_raises_error(self): with pytest.raises(TypeError) as exc: u = mda.Universe() - assert 'Universe.empty' in str(exc.value) + assert "Universe.empty" in str(exc.value) def test_deprecate_b_tempfactors(): @@ -1419,7 +1519,7 @@ def __init__(self, val): class ThingyParser(TopologyReaderBase): - format='THINGY' + format = "THINGY" @staticmethod def _format_hint(thing): @@ -1434,8 +1534,7 @@ def test_only_top(self): # issue 3443 t = Thingy(20) - with pytest.warns(UserWarning, - match="No coordinate reader found for"): + with pytest.warns(UserWarning, match="No coordinate reader found for"): u = mda.Universe(t, to_guess=()) assert len(u.atoms) == 10 diff --git a/testsuite/MDAnalysisTests/core/test_unwrap.py b/testsuite/MDAnalysisTests/core/test_unwrap.py index fe69d945760..e6da223ee2b 100644 --- a/testsuite/MDAnalysisTests/core/test_unwrap.py +++ b/testsuite/MDAnalysisTests/core/test_unwrap.py @@ -22,8 +22,11 @@ # import numpy as np import re -from numpy.testing import (assert_raises, assert_almost_equal, - assert_array_equal) +from numpy.testing import ( + assert_raises, + assert_almost_equal, + assert_array_equal, +) import pytest import MDAnalysis as mda @@ -36,71 +39,79 @@ class TestUnwrap(object): """Tests the functionality of *Group.unwrap() using the UnWrapUniverse, which is specifically designed for wrapping and unwrapping tests. """ + precision = 5 - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("reference", ("com", "cog", None)) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_unwrap_pass(self, level, compound, reference, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 - elif compound == 'segments': - group = u.atoms[23:47] # molecules 10, 11, 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 + elif compound == "segments": + group = u.atoms[23:47] # molecules 10, 11, 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions # get the expected result: ref_unwrapped_pos = u.unwrapped_coords(compound, reference) - if compound == 'group': - ref_unwrapped_pos = ref_unwrapped_pos[39:47] # molecule 12 - elif compound == 'segments': - ref_unwrapped_pos = ref_unwrapped_pos[23:47] # molecules 10, 11, 12 + if compound == "group": + ref_unwrapped_pos = ref_unwrapped_pos[39:47] # molecule 12 + elif compound == "segments": + ref_unwrapped_pos = ref_unwrapped_pos[ + 23:47 + ] # molecules 10, 11, 12 # first, do the unwrapping out-of-place: - unwrapped_pos = group.unwrap(compound=compound, reference=reference, - inplace=False) + unwrapped_pos = group.unwrap( + compound=compound, reference=reference, inplace=False + ) # check for correct result: - assert_almost_equal(unwrapped_pos, ref_unwrapped_pos, - decimal=self.precision) + assert_almost_equal( + unwrapped_pos, ref_unwrapped_pos, decimal=self.precision + ) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) # now, do the unwrapping inplace: - unwrapped_pos2 = group.unwrap(compound=compound, reference=reference, - inplace=True) + unwrapped_pos2 = group.unwrap( + compound=compound, reference=reference, inplace=True + ) # check that result is the same as for out-of-place computation: assert_array_equal(unwrapped_pos, unwrapped_pos2) # check that unwrapped positions are applied: assert_array_equal(group.atoms.positions, unwrapped_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("reference", ("com", "cog", None)) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_wrap_unwrap_cycle(self, level, compound, reference, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 - elif compound == 'segments': - group = u.atoms[23:47] # molecules 10, 11, 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 + elif compound == "segments": + group = u.atoms[23:47] # molecules 10, 11, 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # wrap: group.wrap() @@ -111,18 +122,20 @@ def test_wrap_unwrap_cycle(self, level, compound, reference, is_triclinic): # wrap again: group.wrap() # make sure wrapped atom positions are as before: - assert_almost_equal(group.atoms.positions, orig_wrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.atoms.positions, orig_wrapped_pos, decimal=self.precision + ) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("reference", ("com", "cog", None)) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_unwrap_partial_frags(self, compound, reference, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) # select group with one atom missing - group = u.atoms[39:46] # molecule 12 without its last atom + group = u.atoms[39:46] # molecule 12 without its last atom # store original position of last atom of molecule 12: orig_pos = u.atoms[46].position # get the expected result: @@ -130,35 +143,41 @@ def test_unwrap_partial_frags(self, compound, reference, is_triclinic): # first, do the unwrapping out-of-place: group.unwrap(compound=compound, reference=reference, inplace=True) # check for correct result: - assert_almost_equal(group.positions, ref_unwrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.positions, ref_unwrapped_pos, decimal=self.precision + ) # make sure the position of molecule 12's last atom is unchanged: assert_array_equal(u.atoms[46].position, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) - @pytest.mark.parametrize('is_triclinic', (False, True)) - def test_unwrap_empty_group(self, level, compound, reference, is_triclinic): + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("reference", ("com", "cog", None)) + @pytest.mark.parametrize("is_triclinic", (False, True)) + def test_unwrap_empty_group( + self, level, compound, reference, is_triclinic + ): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) - if level == 'atoms': + if level == "atoms": group = mda.AtomGroup([], u) - elif level == 'residues': + elif level == "residues": group = mda.ResidueGroup([], u) - elif level == 'segments': + elif level == "segments": group = mda.SegmentGroup([], u) group.unwrap(compound=compound, reference=reference, inplace=True) # check for correct (empty) result: - assert_array_equal(group.atoms.positions, - np.empty((0, 3), dtype=np.float32)) + assert_array_equal( + group.atoms.positions, np.empty((0, 3), dtype=np.float32) + ) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("reference", ("com", "cog", None)) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_unwrap_duplicates(self, level, compound, reference, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) @@ -167,9 +186,9 @@ def test_unwrap_duplicates(self, level, compound, reference, is_triclinic): # select the rest of the universe's atoms: rest = u.atoms[:39] # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # duplicate the group: group += group @@ -181,14 +200,16 @@ def test_unwrap_duplicates(self, level, compound, reference, is_triclinic): # unwrap: group.unwrap(compound=compound, reference=reference, inplace=True) # check for correct result: - assert_almost_equal(group.atoms.positions, ref_unwrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.atoms.positions, ref_unwrapped_pos, decimal=self.precision + ) # check that the rest of the atoms are kept unmodified: assert_array_equal(rest.positions, orig_rest_pos) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_unwrap_com_cog_difference(self, compound, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) @@ -200,140 +221,151 @@ def test_unwrap_com_cog_difference(self, compound, is_triclinic): # the first unit cell in negative x-direction. group.masses = [100.0, 1.0, 1.0] # unwrap with center of geometry as reference: - unwrapped_pos_cog = group.unwrap(compound=compound, reference='cog', - inplace=False) + unwrapped_pos_cog = group.unwrap( + compound=compound, reference="cog", inplace=False + ) # get expected result: - ref_unwrapped_pos = u.unwrapped_coords(compound, 'cog')[6:9] + ref_unwrapped_pos = u.unwrapped_coords(compound, "cog")[6:9] # check for correctness: - assert_almost_equal(unwrapped_pos_cog, ref_unwrapped_pos, - decimal=self.precision) + assert_almost_equal( + unwrapped_pos_cog, ref_unwrapped_pos, decimal=self.precision + ) # unwrap with center of mass as reference: - unwrapped_pos_com = group.unwrap(compound=compound, reference='com', - inplace=False) + unwrapped_pos_com = group.unwrap( + compound=compound, reference="com", inplace=False + ) # assert that the com result is shifted with respect to the cog result # by one box length in the x-direction: shift = np.array([10.0, 0.0, 0.0], dtype=np.float32) - assert_almost_equal(unwrapped_pos_cog, unwrapped_pos_com - shift, - decimal=self.precision) + assert_almost_equal( + unwrapped_pos_cog, + unwrapped_pos_com - shift, + decimal=self.precision, + ) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) def test_unwrap_zero_mass_exception_safety(self, level, compound): # get a pristine test universe: u = UnWrapUniverse() # set masses of molecule 12 to zero: u.atoms[39:47].masses = 0.0 # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 - elif compound == 'segments': - group = u.atoms[23:47] # molecules 10, 11, 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 + elif compound == "segments": + group = u.atoms[23:47] # molecules 10, 11, 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions # try to unwrap: with pytest.raises(ValueError): - group.unwrap(compound=compound, reference='com', - inplace=True) + group.unwrap(compound=compound, reference="com", inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) def test_unwrap_wrong_reference_exception_safety(self, level, compound): # get a pristine test universe: u = UnWrapUniverse() # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 - elif compound == 'segments': - group = u.atoms[23:47] # molecules 10, 11, 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 + elif compound == "segments": + group = u.atoms[23:47] # molecules 10, 11, 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions # try to unwrap: with pytest.raises(ValueError): - group.unwrap(compound=compound, reference='wrong', inplace=True) + group.unwrap(compound=compound, reference="wrong", inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize("reference", ("com", "cog", None)) def test_unwrap_wrong_compound_exception_safety(self, level, reference): # get a pristine test universe: u = UnWrapUniverse() group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions # try to unwrap: with pytest.raises(ValueError): - group.unwrap(compound='wrong', reference=reference, inplace=True) + group.unwrap(compound="wrong", reference=reference, inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) def test_unwrap_no_masses_exception_safety(self, level, compound): # universe without masses: u = UnWrapUniverse(have_masses=False) # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 - elif compound == 'segments': - group = u.atoms[23:47] # molecules 10, 11, 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 + elif compound == "segments": + group = u.atoms[23:47] # molecules 10, 11, 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions # try to unwrap: with pytest.raises(NoDataError): - group.unwrap(compound=compound, reference='com', inplace=True) + group.unwrap(compound=compound, reference="com", inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('fragments', 'molecules', 'residues', - 'group', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) - def test_unwrap_no_bonds_exception_safety(self, level, compound, reference): + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", ("fragments", "molecules", "residues", "group", "segments") + ) + @pytest.mark.parametrize("reference", ("com", "cog", None)) + def test_unwrap_no_bonds_exception_safety( + self, level, compound, reference + ): # universe without bonds: u = UnWrapUniverse(have_bonds=False) # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 - elif compound == 'segments': - group = u.atoms[23:47] # molecules 10, 11, 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 + elif compound == "segments": + group = u.atoms[23:47] # molecules 10, 11, 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions @@ -348,30 +380,30 @@ def test_unwrap_no_bonds_exception_safety(self, level, compound, reference): # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('reference', ('com', 'cog', None)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize("reference", ("com", "cog", None)) def test_unwrap_no_molnums_exception_safety(self, level, reference): # universe without molnums: u = UnWrapUniverse(have_molnums=False) group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions with pytest.raises(NoDataError): - group.unwrap(compound='molecules', reference=reference, - inplace=True) + group.unwrap( + compound="molecules", reference=reference, inplace=True + ) assert_array_equal(group.atoms.positions, orig_pos) def test_uncontiguous(): - """Real-life case of fragment sparsity that triggers Issue 3352 - """ + """Real-life case of fragment sparsity that triggers Issue 3352""" precision = 5 - displacement_vec = [14.7, 0., 0.] + displacement_vec = [14.7, 0.0, 0.0] u = mda.Universe(CONECT) # This is one of the few residues that has bonds ag = u.residues[66].atoms @@ -380,10 +412,15 @@ def test_uncontiguous(): u.atoms.positions -= displacement_vec u.atoms.pack_into_box() # Let's make sure we really broke the fragment - assert_raises(AssertionError, assert_almost_equal, - ref_pos, ag.positions+displacement_vec, - decimal=precision) + assert_raises( + AssertionError, + assert_almost_equal, + ref_pos, + ag.positions + displacement_vec, + decimal=precision, + ) # Ok, let's make it whole again and check that we're good u.atoms.unwrap() - assert_almost_equal(ref_pos, ag.positions+displacement_vec, - decimal=precision) + assert_almost_equal( + ref_pos, ag.positions + displacement_vec, decimal=precision + ) diff --git a/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py b/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py index 51c3eecf500..fc41670a6ee 100644 --- a/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py +++ b/testsuite/MDAnalysisTests/core/test_updating_atomgroup.py @@ -44,18 +44,19 @@ def ag(self, u): @pytest.fixture() def ag_updating(self, u): - return u.select_atoms("prop x < 5 and prop y < 5 and prop z < 5", - updating=True) + return u.select_atoms( + "prop x < 5 and prop y < 5 and prop z < 5", updating=True + ) @pytest.fixture() def ag_updating_compounded(self, u, ag): - return u.select_atoms("around 2 group sele", - sele=ag, updating=True) + return u.select_atoms("around 2 group sele", sele=ag, updating=True) @pytest.fixture() def ag_updating_chained(self, u, ag_updating): - return u.select_atoms("around 2 group sele", - sele=ag_updating, updating=True) + return u.select_atoms( + "around 2 group sele", sele=ag_updating, updating=True + ) @pytest.fixture() def ag_updating_chained2(self, ag_updating): @@ -63,9 +64,24 @@ def ag_updating_chained2(self, ag_updating): def test_update(self, u, ag, ag_updating): assert_equal(ag_updating.indices, ag.indices) - target_idxs = np.array([4469, 4470, 4472, 6289, 6290, 6291, - 6292, 31313, 31314, 31315, 31316, 34661, - 34663, 34664]) + target_idxs = np.array( + [ + 4469, + 4470, + 4472, + 6289, + 6290, + 6291, + 6292, + 31313, + 31314, + 31315, + 31316, + 34661, + 34663, + 34664, + ] + ) next(u.trajectory) assert_equal(ag_updating._lastupdate, 0) assert ag_updating.is_uptodate is False @@ -75,30 +91,30 @@ def test_update(self, u, ag, ag_updating): assert ag_updating._lastupdate is None def test_compounded_update(self, u, ag_updating_compounded): - target_idxs0 = np.array([3650, 7406, 22703, 31426, 40357, - 40360, 41414]) - target_idxs1 = np.array([3650, 8146, 23469, 23472, 31426, - 31689, 31692, 34326, 41414]) - assert_equal(ag_updating_compounded.indices, - target_idxs0) + target_idxs0 = np.array( + [3650, 7406, 22703, 31426, 40357, 40360, 41414] + ) + target_idxs1 = np.array( + [3650, 8146, 23469, 23472, 31426, 31689, 31692, 34326, 41414] + ) + assert_equal(ag_updating_compounded.indices, target_idxs0) next(u.trajectory) - assert_equal(ag_updating_compounded.indices, - target_idxs1) + assert_equal(ag_updating_compounded.indices, target_idxs1) - def test_chained_update(self, u, ag_updating_chained, - ag_updating_compounded): + def test_chained_update( + self, u, ag_updating_chained, ag_updating_compounded + ): target_idxs = np.array([4471, 7406, 11973, 11975, 34662, 44042]) - assert_equal(ag_updating_chained.indices, - ag_updating_compounded.indices) + assert_equal( + ag_updating_chained.indices, ag_updating_compounded.indices + ) next(u.trajectory) assert_equal(ag_updating_chained.indices, target_idxs) def test_chained_update2(self, u, ag_updating, ag_updating_chained2): - assert_equal(ag_updating_chained2.indices, - ag_updating.indices) + assert_equal(ag_updating_chained2.indices, ag_updating.indices) next(u.trajectory) - assert_equal(ag_updating_chained2.indices, - ag_updating.indices) + assert_equal(ag_updating_chained2.indices, ag_updating.indices) def test_slice_is_static(self, u, ag, ag_updating): ag_static1 = ag_updating[:] @@ -168,7 +184,7 @@ class UAGReader(mda.coordinates.base.ReaderBase): """ def __init__(self, n_atoms): - super(UAGReader, self).__init__('UAGReader') + super(UAGReader, self).__init__("UAGReader") self._auxs = {} self.n_frames = 10 @@ -198,32 +214,34 @@ def _read_frame(self, frame): class TestUAGCallCount(object): # make sure updates are only called when required! - # + # # these tests check that potentially expensive selection operations are only # done when necessary @pytest.fixture() def u(self): - u = make_Universe(('names',)) + u = make_Universe(("names",)) u.trajectory = UAGReader(125) return u - @mock.patch.object(MDAnalysis.core.groups.UpdatingAtomGroup, - 'update_selection', - autospec=True, - # required to make it get self when called - ) + @mock.patch.object( + MDAnalysis.core.groups.UpdatingAtomGroup, + "update_selection", + autospec=True, + # required to make it get self when called + ) def test_updated_when_creating(self, mock_update_selection, u): - uag = u.select_atoms('name XYZ', updating=True) + uag = u.select_atoms("name XYZ", updating=True) assert mock_update_selection.call_count == 1 def test_updated_when_next(self, u): - uag = u.select_atoms('name XYZ', updating=True) + uag = u.select_atoms("name XYZ", updating=True) # Use mock.patch.object to start inspecting the uag update selection method # wraps= keyword makes it still function as normal, just we're spying on it now - with mock.patch.object(uag, 'update_selection', - wraps=uag.update_selection) as mock_update: + with mock.patch.object( + uag, "update_selection", wraps=uag.update_selection + ) as mock_update: next(u.trajectory) assert mock_update.call_count == 0 @@ -237,16 +255,16 @@ def test_updated_when_next(self, u): class TestDynamicUAG(object): @pytest.fixture() def u(self): - u = make_Universe(('names',)) + u = make_Universe(("names",)) u.trajectory = UAGReader(125) return u def test_nested_uags(self, u): bg = u.atoms[[3, 4]] - uag1 = u.select_atoms('around 1.5 group bg', bg=bg, updating=True) + uag1 = u.select_atoms("around 1.5 group bg", bg=bg, updating=True) - uag2 = u.select_atoms('around 1.5 group uag', uag=uag1, updating=True) + uag2 = u.select_atoms("around 1.5 group uag", uag=uag1, updating=True) for ts in u.trajectory: assert_equal(len(bg), 2) @@ -254,7 +272,7 @@ def test_nested_uags(self, u): assert_equal(len(uag2), 4) # doesn't include uag1 def test_driveby(self, u): - uag = u.select_atoms('prop x < 5.5', updating=True) + uag = u.select_atoms("prop x < 5.5", updating=True) n_init = 6 for i, ts in enumerate(u.trajectory): @@ -277,8 +295,9 @@ def test_representations(): rep = repr(ag_updating) assert "1 atom," in rep - ag_updating = u.atoms[:-1].select_atoms("bynum 1", "bynum 2", - updating=True) + ag_updating = u.atoms[:-1].select_atoms( + "bynum 1", "bynum 2", updating=True + ) rep = repr(ag_updating) assert "2 atoms," in rep assert "selections 'bynum 1' + 'bynum 2'" in rep @@ -289,6 +308,6 @@ def test_empty_UAG(): u = make_Universe() # technically possible to make a UAG without any selections.. - uag = mda.core.groups.UpdatingAtomGroup(u.atoms, (), '') + uag = mda.core.groups.UpdatingAtomGroup(u.atoms, (), "") assert isinstance(uag, mda.core.groups.UpdatingAtomGroup) diff --git a/testsuite/MDAnalysisTests/core/test_wrap.py b/testsuite/MDAnalysisTests/core/test_wrap.py index 186ac3dadee..76b0263a8e2 100644 --- a/testsuite/MDAnalysisTests/core/test_wrap.py +++ b/testsuite/MDAnalysisTests/core/test_wrap.py @@ -29,6 +29,7 @@ from MDAnalysisTests.core.util import UnWrapUniverse, assert_in_box from MDAnalysisTests.datafiles import TRZ_psf, TRZ + class TestWrap(object): """Tests the functionality of *Group.wrap() using the UnWrapUniverse, which is specifically designed for wrapping and unwrapping tests. @@ -36,66 +37,74 @@ class TestWrap(object): precision = 5 - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('center', ('com', 'cog')) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("center", ("com", "cog")) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_wrap_pass(self, level, compound, center, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions # get the expected result: ref_wrapped_pos = u.wrapped_coords(compound, center) # first, do the wrapping out-of-place: - wrapped_pos = group.wrap(compound=compound, center=center, - inplace=False) + wrapped_pos = group.wrap( + compound=compound, center=center, inplace=False + ) # check for correct result: - assert_almost_equal(wrapped_pos, ref_wrapped_pos, - decimal=self.precision) + assert_almost_equal( + wrapped_pos, ref_wrapped_pos, decimal=self.precision + ) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) # now, do the wrapping inplace: - wrapped_pos2 = group.wrap(compound=compound, center=center, - inplace=True) + wrapped_pos2 = group.wrap( + compound=compound, center=center, inplace=True + ) # check that result is the same as for out-of-place computation: assert_array_equal(wrapped_pos, wrapped_pos2) # check that wrapped positions are applied: assert_array_equal(group.atoms.positions, wrapped_pos) # check that nobody messed with the reference positions, # centers of compounds must lie within the primary unit cell: - if compound == 'atoms': + if compound == "atoms": assert_in_box(group.atoms.positions, group.dimensions) - elif center == 'com': + elif center == "com": compos = group.atoms.center_of_mass(wrap=False, compound=compound) assert_in_box(compos, group.dimensions) else: - cogpos = group.atoms.center_of_geometry(wrap=False, - compound=compound) + cogpos = group.atoms.center_of_geometry( + wrap=False, compound=compound + ) assert_in_box(cogpos, group.dimensions) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('center', ('com', 'cog')) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("center", ("com", "cog")) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_unwrap_wrap_cycle(self, level, compound, center, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) # set wrapped reference coordinates: - u.atoms.positions = u.wrapped_coords('atoms', 'com') + u.atoms.positions = u.wrapped_coords("atoms", "com") group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # unwrap: group.unwrap() @@ -106,34 +115,41 @@ def test_unwrap_wrap_cycle(self, level, compound, center, is_triclinic): # unwrap again: group.unwrap() # make sure unwrapped atom positions are as before: - assert_almost_equal(group.atoms.positions, orig_unwrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.atoms.positions, orig_unwrapped_pos, decimal=self.precision + ) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('center', ('com', 'cog')) - @pytest.mark.parametrize('is_triclinic', (False, True)) - def test_wrap_partial_compound(self, level, compound, center, is_triclinic): + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("center", ("com", "cog")) + @pytest.mark.parametrize("is_triclinic", (False, True)) + def test_wrap_partial_compound( + self, level, compound, center, is_triclinic + ): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) group = u.atoms ref_wrapped_pos = u.wrapped_coords(compound, center) # select topology level with one missing item and get expected result: - if level == 'atoms': + if level == "atoms": # first atom of molecule 12 missing missing = group[[-8]] group = group[:-8] + group[-7:] - ref_wrapped_pos = np.concatenate([ref_wrapped_pos[:-8], - ref_wrapped_pos[-7:]]) - elif level == 'residues': + ref_wrapped_pos = np.concatenate( + [ref_wrapped_pos[:-8], ref_wrapped_pos[-7:]] + ) + elif level == "residues": group = group.residues # first residue of molecule 12 missing missing = group[-2] group = group[:-2] + group[[-1]] - ref_wrapped_pos = np.concatenate([ref_wrapped_pos[:-8], - ref_wrapped_pos[-4:]]) - elif level == 'segments': + ref_wrapped_pos = np.concatenate( + [ref_wrapped_pos[:-8], ref_wrapped_pos[-4:]] + ) + elif level == "segments": group = group.segments # molecule 12 missing missing = group[-1] @@ -144,34 +160,40 @@ def test_wrap_partial_compound(self, level, compound, center, is_triclinic): # first, do the wrapping out-of-place: group.wrap(compound=compound, center=center, inplace=True) # check for correct result: - assert_almost_equal(group.atoms.positions, ref_wrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.atoms.positions, ref_wrapped_pos, decimal=self.precision + ) # make sure the positions of the missing item are unchanged: assert_array_equal(missing.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('center', ('com', 'cog')) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("center", ("com", "cog")) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_wrap_empty_group(self, level, compound, center, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) group = u.atoms[[]] - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments group.wrap(compound=compound, center=center, inplace=True) # check for correct (empty) result: - assert_array_equal(group.atoms.positions, - np.empty((0, 3), dtype=np.float32)) + assert_array_equal( + group.atoms.positions, np.empty((0, 3), dtype=np.float32) + ) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('center', ('com', 'cog')) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("center", ("com", "cog")) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_wrap_duplicates(self, level, compound, center, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) @@ -180,34 +202,37 @@ def test_wrap_duplicates(self, level, compound, center, is_triclinic): # select the rest of the universe's atoms: rest = u.atoms[:39] # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # duplicate the group: group += group # store original positions of the rest: orig_rest_pos = rest.positions # get the expected result with duplicates: - if compound == 'group': + if compound == "group": # reference positions of UnWrapUniverse are known to be incorrect # for compound='group' if the group is not the entire system, so we # have to correct for that: - ref_wrapped_pos = u.wrapped_coords('segments', center)[39:47] + ref_wrapped_pos = u.wrapped_coords("segments", center)[39:47] else: ref_wrapped_pos = u.wrapped_coords(compound, center)[39:47] ref_wrapped_pos = np.vstack((ref_wrapped_pos, ref_wrapped_pos)) # wrap: group.wrap(compound=compound, center=center, inplace=True) # check for correct result: - assert_almost_equal(group.atoms.positions, ref_wrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.atoms.positions, ref_wrapped_pos, decimal=self.precision + ) # check that the rest of the atoms are kept unmodified: assert_array_equal(rest.positions, orig_rest_pos) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('is_triclinic', (False, True)) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("is_triclinic", (False, True)) def test_wrap_com_cog_difference(self, compound, is_triclinic): # get a pristine test universe: u = UnWrapUniverse(is_triclinic=is_triclinic) @@ -219,145 +244,160 @@ def test_wrap_com_cog_difference(self, compound, is_triclinic): # the first unit cell in negative x-direction. group.masses = [100.0, 1.0, 1.0] # wrap with center='cog': - wrapped_pos_cog = group.wrap(compound=compound, center='cog', - inplace=False) + wrapped_pos_cog = group.wrap( + compound=compound, center="cog", inplace=False + ) # get expected result: - ref_wrapped_pos = u.wrapped_coords(compound, 'cog')[6:9] + ref_wrapped_pos = u.wrapped_coords(compound, "cog")[6:9] # check for correctness: - assert_almost_equal(wrapped_pos_cog, ref_wrapped_pos, - decimal=self.precision) + assert_almost_equal( + wrapped_pos_cog, ref_wrapped_pos, decimal=self.precision + ) # wrap with center='com': - wrapped_pos_com = group.wrap(compound=compound, center='com', - inplace=False) + wrapped_pos_com = group.wrap( + compound=compound, center="com", inplace=False + ) # assert that the com result is shifted with respect to the cog result # by one box length in the x-direction: shift = np.array([10.0, 0.0, 0.0], dtype=np.float32) - if compound == 'atoms': + if compound == "atoms": # center argument must be ignored for compound='atoms': shift[0] = 0.0 - assert_almost_equal(wrapped_pos_cog, wrapped_pos_com - shift, - decimal=self.precision) + assert_almost_equal( + wrapped_pos_cog, wrapped_pos_com - shift, decimal=self.precision + ) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) def test_wrap_zero_mass_exception_safety(self, level, compound): # get a pristine test universe: u = UnWrapUniverse() # set masses of molecule 12 to zero: u.atoms[39:47].masses = 0.0 # select group appropriate for compound: - if compound == 'group': - group = u.atoms[39:47] # molecule 12 + if compound == "group": + group = u.atoms[39:47] # molecule 12 else: group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions - if compound == 'atoms': + if compound == "atoms": # wrap() must not care about masses if compound == 'atoms': - group.wrap(compound=compound, center='com', inplace=True) - ref_wrapped_pos = u.wrapped_coords(compound, 'com') - assert_almost_equal(group.atoms.positions, ref_wrapped_pos, - decimal=self.precision) + group.wrap(compound=compound, center="com", inplace=True) + ref_wrapped_pos = u.wrapped_coords(compound, "com") + assert_almost_equal( + group.atoms.positions, ref_wrapped_pos, decimal=self.precision + ) else: # try to wrap: with pytest.raises(ValueError): - group.wrap(compound=compound, center='com', inplace=True) + group.wrap(compound=compound, center="com", inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) def test_wrap_wrong_center_exception_safety(self, level, compound): # get a pristine test universe: u = UnWrapUniverse() group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions - if compound == 'atoms': + if compound == "atoms": # wrap() must ignore the center argument if compound == 'atoms': - group.wrap(compound=compound, center='com', inplace=True) - ref_wrapped_pos = u.wrapped_coords(compound, 'com') - assert_almost_equal(group.atoms.positions, ref_wrapped_pos, - decimal=self.precision) + group.wrap(compound=compound, center="com", inplace=True) + ref_wrapped_pos = u.wrapped_coords(compound, "com") + assert_almost_equal( + group.atoms.positions, ref_wrapped_pos, decimal=self.precision + ) else: # try to wrap: with pytest.raises(ValueError): - group.wrap(compound=compound, center='wrong', inplace=True) + group.wrap(compound=compound, center="wrong", inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('center', ('com', 'cog')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize("center", ("com", "cog")) def test_wrap_wrong_compound_exception_safety(self, level, center): # get a pristine test universe: u = UnWrapUniverse() group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments # store original positions: orig_pos = group.atoms.positions # try to wrap: with pytest.raises(ValueError): - group.wrap(compound='wrong', center=center, inplace=True) + group.wrap(compound="wrong", center=center, inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) def test_unwrap_no_masses_exception_safety(self, level, compound): # universe without masses: u = UnWrapUniverse(have_masses=False) group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments - if compound == 'atoms': + if compound == "atoms": # wrap() must not care about mass presence if compound == 'atoms': - group.wrap(compound=compound, center='com', inplace=True) - ref_wrapped_pos = u.wrapped_coords(compound, 'com') - assert_almost_equal(group.atoms.positions, ref_wrapped_pos, - decimal=self.precision) + group.wrap(compound=compound, center="com", inplace=True) + ref_wrapped_pos = u.wrapped_coords(compound, "com") + assert_almost_equal( + group.atoms.positions, ref_wrapped_pos, decimal=self.precision + ) else: # store original positions: orig_pos = group.atoms.positions # try to wrap: with pytest.raises(NoDataError): - group.wrap(compound=compound, center='com', inplace=True) + group.wrap(compound=compound, center="com", inplace=True) # make sure atom positions are unchanged: assert_array_equal(group.atoms.positions, orig_pos) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('center', ('com', 'cog')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("center", ("com", "cog")) def test_wrap_no_bonds_exception_safety(self, level, compound, center): # universe without bonds: u = UnWrapUniverse(have_bonds=False) group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments - if compound == 'fragments': + if compound == "fragments": # store original positions: orig_pos = group.atoms.positions # must raise an exception for fragments @@ -369,23 +409,26 @@ def test_wrap_no_bonds_exception_safety(self, level, compound, center): # must not care about bonds if compound != 'fragments' group.wrap(compound=compound, center=center, inplace=True) ref_wrapped_pos = u.wrapped_coords(compound, center) - assert_almost_equal(group.atoms.positions, ref_wrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.atoms.positions, ref_wrapped_pos, decimal=self.precision + ) - @pytest.mark.parametrize('level', ('atoms', 'residues', 'segments')) - @pytest.mark.parametrize('compound', ('atoms', 'group', 'segments', - 'residues', 'molecules', 'fragments')) - @pytest.mark.parametrize('center', ('com', 'cog')) + @pytest.mark.parametrize("level", ("atoms", "residues", "segments")) + @pytest.mark.parametrize( + "compound", + ("atoms", "group", "segments", "residues", "molecules", "fragments"), + ) + @pytest.mark.parametrize("center", ("com", "cog")) def test_wrap_no_molnums_exception_safety(self, level, compound, center): # universe without bonds: u = UnWrapUniverse(have_molnums=False) group = u.atoms # select topology level: - if level == 'residues': + if level == "residues": group = group.residues - elif level == 'segments': + elif level == "segments": group = group.segments - if compound == 'molecules': + if compound == "molecules": # store original positions: orig_pos = group.atoms.positions # must raise an exception for molecules @@ -397,8 +440,10 @@ def test_wrap_no_molnums_exception_safety(self, level, compound, center): # must not care about molnums if compound != 'molecules' group.wrap(compound=compound, center=center, inplace=True) ref_wrapped_pos = u.wrapped_coords(compound, center) - assert_almost_equal(group.atoms.positions, ref_wrapped_pos, - decimal=self.precision) + assert_almost_equal( + group.atoms.positions, ref_wrapped_pos, decimal=self.precision + ) + class TestWrapTRZ(object): """Tests the functionality of AtomGroup.wrap() using a TRZ universe.""" @@ -409,7 +454,7 @@ class TestWrapTRZ(object): def u(self): return mda.Universe(TRZ_psf, TRZ) - @pytest.mark.parametrize('box', (np.array([1, 1]), np.zeros(6))) + @pytest.mark.parametrize("box", (np.array([1, 1]), np.zeros(6))) def test_wrap_box_fail(self, u, box): ag = u.atoms with pytest.raises(ValueError): @@ -431,47 +476,47 @@ def test_wrap_large_box(self, u): def test_wrap_atoms(self, u): ag = u.atoms[100:200] - rescom = ag.wrap(compound='atoms', center='com', inplace=False) + rescom = ag.wrap(compound="atoms", center="com", inplace=False) assert_in_box(rescom, u.dimensions) - rescog = ag.wrap(compound='atoms', center='cog', inplace=False) + rescog = ag.wrap(compound="atoms", center="cog", inplace=False) assert_almost_equal(rescom, rescog, decimal=self.precision) - @pytest.mark.parametrize('center', ('com', 'cog')) + @pytest.mark.parametrize("center", ("com", "cog")) def test_wrap_group(self, u, center): ag = u.atoms[:100] - ag.wrap(compound='group', center=center) - if center == 'com': - ctrpos = ag.center_of_mass(wrap=False, compound='group') - elif center == 'cog': - ctrpos = ag.center_of_geometry(wrap=False, compound='group') + ag.wrap(compound="group", center=center) + if center == "com": + ctrpos = ag.center_of_mass(wrap=False, compound="group") + elif center == "cog": + ctrpos = ag.center_of_geometry(wrap=False, compound="group") assert_in_box(ctrpos, u.dimensions) - @pytest.mark.parametrize('center', ('com', 'cog')) + @pytest.mark.parametrize("center", ("com", "cog")) def test_wrap_residues(self, u, center): ag = u.atoms[300:400].residues - ag.wrap(compound='residues', center=center) - if center == 'com': - ctrpos = ag.center_of_mass(wrap=False, compound='residues') - elif center == 'cog': - ctrpos = ag.center_of_geometry(wrap=False, compound='residues') + ag.wrap(compound="residues", center=center) + if center == "com": + ctrpos = ag.center_of_mass(wrap=False, compound="residues") + elif center == "cog": + ctrpos = ag.center_of_geometry(wrap=False, compound="residues") assert_in_box(ctrpos, u.dimensions) - @pytest.mark.parametrize('center', ('com', 'cog')) + @pytest.mark.parametrize("center", ("com", "cog")) def test_wrap_segments(self, u, center): ag = u.atoms[1000:1200] - ag.wrap(compound='segments', center=center) - if center == 'com': - ctrpos = ag.center_of_mass(wrap=False, compound='segments') - elif center == 'cog': - ctrpos = ag.center_of_geometry(wrap=False, compound='segments') + ag.wrap(compound="segments", center=center) + if center == "com": + ctrpos = ag.center_of_mass(wrap=False, compound="segments") + elif center == "cog": + ctrpos = ag.center_of_geometry(wrap=False, compound="segments") assert_in_box(ctrpos, u.dimensions) - @pytest.mark.parametrize('center', ('com', 'cog')) + @pytest.mark.parametrize("center", ("com", "cog")) def test_wrap_fragments(self, u, center): ag = u.atoms[:250] - ag.wrap(compound='fragments', center=center) - if center == 'com': - ctrpos = ag.center_of_mass(wrap=False, compound='fragments') - elif center == 'cog': - ctrpos = ag.center_of_geometry(wrap=False, compound='fragments') + ag.wrap(compound="fragments", center=center) + if center == "com": + ctrpos = ag.center_of_mass(wrap=False, compound="fragments") + elif center == "cog": + ctrpos = ag.center_of_geometry(wrap=False, compound="fragments") assert_in_box(ctrpos, u.dimensions) diff --git a/testsuite/MDAnalysisTests/core/util.py b/testsuite/MDAnalysisTests/core/util.py index 42f2c3a8ed9..52b83d64d62 100644 --- a/testsuite/MDAnalysisTests/core/util.py +++ b/testsuite/MDAnalysisTests/core/util.py @@ -137,15 +137,21 @@ class UnWrapUniverse(object): y directions. """ - def __new__(cls, have_bonds=True, have_masses=True, have_molnums=True, - have_charges=True, is_triclinic=False): + def __new__( + cls, + have_bonds=True, + have_masses=True, + have_molnums=True, + have_charges=True, + is_triclinic=False, + ): # box: - a = 10.0 # edge length + a = 10.0 # edge length tfac = 0.1 # factor for box vector shift of triclinic boxes (~84°) if is_triclinic: - box = triclinic_box([a, 0.0, 0.0], - [a * tfac, a, 0.0], - [a * tfac, a * tfac, a]) + box = triclinic_box( + [a, 0.0, 0.0], [a * tfac, a, 0.0], [a * tfac, a * tfac, a] + ) else: box = np.array([a, a, a, 90.0, 90.0, 90.0], dtype=np.float32) @@ -163,11 +169,11 @@ def __new__(cls, have_bonds=True, have_masses=True, have_molnums=True, rix += 1 # type B for i in range(3, 15, 3): - residx[i:i+3] = rix + residx[i : i + 3] = rix rix += 1 # type C & D for i in range(15, 47, 4): - residx[i:i+4] = rix + residx[i : i + 4] = rix rix += 1 # segindices: @@ -194,17 +200,17 @@ def __new__(cls, have_bonds=True, have_masses=True, have_molnums=True, ) # resnames: we always want those for selection purposes - resnames = ['A'] * 3 - resnames += ['B'] * 4 - resnames += ['C'] * 2 - resnames += ['D1', 'D2'] * 3 + resnames = ["A"] * 3 + resnames += ["B"] * 4 + resnames += ["C"] * 2 + resnames += ["D1", "D2"] * 3 u.add_TopologyAttr(topologyattrs.Resnames(resnames)) # moltypes: we always want those for selection purposes - moltypes = ['A'] * 3 - moltypes += ['B'] * 4 - moltypes += ['C'] * 2 - moltypes += ['D'] * 6 + moltypes = ["A"] * 3 + moltypes += ["B"] * 4 + moltypes += ["C"] * 2 + moltypes += ["D"] * 6 u.add_TopologyAttr(topologyattrs.Moltypes(moltypes)) # trajectory: @@ -215,56 +221,72 @@ def __new__(cls, have_bonds=True, have_masses=True, have_molnums=True, # positions: relpos = np.empty((n_atoms, 3), dtype=np.float32) # type A - relpos[0:3, :] = np.array([[0.5, 0.5, 0.5], - [1.4, 0.5, 0.5], - [2.1, 0.5, 0.5]], dtype=np.float32) + relpos[0:3, :] = np.array( + [[0.5, 0.5, 0.5], [1.4, 0.5, 0.5], [2.1, 0.5, 0.5]], + dtype=np.float32, + ) # type B - relpos[3:15, :] = np.array([[0.1, 0.1, 0.2], - [0.1, 0.1, 0.1], - [0.2, 0.1, 0.1], - [-0.05, 0.2, 0.05], - [0.05, 0.2, 0.05], - [0.05, 0.2, 0.95], - [-0.2, -0.9, 1.05], - [-0.2, 0.1, -0.05], - [-0.1, 0.1, -0.05], - [0.95, 0.2, 0.25], - [0.95, 0.2, 0.15], - [1.05, 0.2, 0.15]], dtype=np.float32) + relpos[3:15, :] = np.array( + [ + [0.1, 0.1, 0.2], + [0.1, 0.1, 0.1], + [0.2, 0.1, 0.1], + [-0.05, 0.2, 0.05], + [0.05, 0.2, 0.05], + [0.05, 0.2, 0.95], + [-0.2, -0.9, 1.05], + [-0.2, 0.1, -0.05], + [-0.1, 0.1, -0.05], + [0.95, 0.2, 0.25], + [0.95, 0.2, 0.15], + [1.05, 0.2, 0.15], + ], + dtype=np.float32, + ) # type C - relpos[15:23, :] = np.array([[0.4, 0.95, 1.05], - [0.4, 0.95, 0.95], - [0.4, 0.05, 0.95], - [0.4, 0.05, 1.05], - [0.6, 0.05, 0.25], - [0.6, 0.05, 0.15], - [0.6, 0.15, 0.15], - [0.6, 0.15, 0.25]], dtype=np.float32) + relpos[15:23, :] = np.array( + [ + [0.4, 0.95, 1.05], + [0.4, 0.95, 0.95], + [0.4, 0.05, 0.95], + [0.4, 0.05, 1.05], + [0.6, 0.05, 0.25], + [0.6, 0.05, 0.15], + [0.6, 0.15, 0.15], + [0.6, 0.15, 0.25], + ], + dtype=np.float32, + ) # type D - relpos[23:47, :] = np.array([[0.2, 0.7, 0.8], - [0.3, 0.7, 0.8], - [0.4, 0.7, 0.8], - [0.5, 0.7, 0.8], - [0.6, 0.7, 0.8], - [0.7, 0.7, 0.8], - [0.8, 0.7, 0.8], - [0.9, 0.7, 0.8], - [0.66, 0.75, 0.7], - [0.76, 0.75, 0.7], - [0.86, 0.75, 0.7], - [0.96, 0.75, 0.7], - [0.06, 0.75, 0.7], - [0.16, 0.75, 0.7], - [0.26, 0.75, 0.7], - [0.36, 0.75, 0.7], - [1.14, 0.65, -0.4], - [1.04, 0.65, -0.4], - [0.94, 0.65, -0.4], - [0.84, 0.65, -0.4], - [0.74, 0.65, -0.4], - [0.64, 0.65, -0.4], - [0.54, 0.65, -0.4], - [0.44, 0.65, -0.4]], dtype=np.float32) + relpos[23:47, :] = np.array( + [ + [0.2, 0.7, 0.8], + [0.3, 0.7, 0.8], + [0.4, 0.7, 0.8], + [0.5, 0.7, 0.8], + [0.6, 0.7, 0.8], + [0.7, 0.7, 0.8], + [0.8, 0.7, 0.8], + [0.9, 0.7, 0.8], + [0.66, 0.75, 0.7], + [0.76, 0.75, 0.7], + [0.86, 0.75, 0.7], + [0.96, 0.75, 0.7], + [0.06, 0.75, 0.7], + [0.16, 0.75, 0.7], + [0.26, 0.75, 0.7], + [0.36, 0.75, 0.7], + [1.14, 0.65, -0.4], + [1.04, 0.65, -0.4], + [0.94, 0.65, -0.4], + [0.84, 0.65, -0.4], + [0.74, 0.65, -0.4], + [0.64, 0.65, -0.4], + [0.54, 0.65, -0.4], + [0.44, 0.65, -0.4], + ], + dtype=np.float32, + ) # make a copy, we need the original later _relpos = relpos.copy() # apply y- and z-dependent shift of x and y coords for triclinic boxes: @@ -280,7 +302,7 @@ def __new__(cls, have_bonds=True, have_masses=True, have_molnums=True, if have_bonds: bonds = [] # type A has no bonds - #type B + # type B for base in range(3, 15, 3): for i in range(2): bonds.append((base + i, base + i + 1)) @@ -353,12 +375,18 @@ def unwrapped_coords(self, compound, reference): """ if reference is not None: ref = reference.lower() - if ref not in ['com', 'cog']: - raise ValueError("Unknown unwrap reference: {}" - "".format(reference)) + if ref not in ["com", "cog"]: + raise ValueError( + "Unknown unwrap reference: {}" "".format(reference) + ) comp = compound.lower() - if comp not in ['group', 'segments', 'residues', 'molecules', - 'fragments']: + if comp not in [ + "group", + "segments", + "residues", + "molecules", + "fragments", + ]: raise ValueError("Unknown unwrap compound: {}".format(compound)) # get relative positions: @@ -374,67 +402,91 @@ def unwrapped_coords(self, compound, reference): relpos[18, :] = [0.4, 1.05, 1.05] # type D # molecule 11, residue 1 - relpos[35:39, :] = np.array([[1.06, 0.75, 0.7], - [1.16, 0.75, 0.7], - [1.26, 0.75, 0.7], - [1.36, 0.75, 0.7]], dtype=np.float32) + relpos[35:39, :] = np.array( + [ + [1.06, 0.75, 0.7], + [1.16, 0.75, 0.7], + [1.26, 0.75, 0.7], + [1.36, 0.75, 0.7], + ], + dtype=np.float32, + ) # apply image shifts if necessary: if reference is None: - if comp == 'residues': - #second residue of molecule 11 - relpos[35:39, :] = np.array([[0.06, 0.75, 0.7], - [0.16, 0.75, 0.7], - [0.26, 0.75, 0.7], - [0.36, 0.75, 0.7]], - dtype=np.float32) + if comp == "residues": + # second residue of molecule 11 + relpos[35:39, :] = np.array( + [ + [0.06, 0.75, 0.7], + [0.16, 0.75, 0.7], + [0.26, 0.75, 0.7], + [0.36, 0.75, 0.7], + ], + dtype=np.float32, + ) else: # molecule 2 & 3 - relpos[1:3, :] = np.array([[0.4, 0.5, 0.5], - [0.1, 0.5, 0.5]], dtype=np.float32) + relpos[1:3, :] = np.array( + [[0.4, 0.5, 0.5], [0.1, 0.5, 0.5]], dtype=np.float32 + ) # molecule 6 - relpos[9:12, :] = np.array([[0.8, 0.1, 1.05], - [0.8, 0.1, 0.95], - [0.9, 0.1, 0.95]], dtype=np.float32) - #molecule 8 - relpos[15:19, :] = np.array([[0.4, -0.05, 0.05], - [0.4, -0.05, -0.05], - [0.4, 0.05, -0.05], - [0.4, 0.05, 0.05]], dtype=np.float32) - if comp == 'residues': - #molecule 11, residue 1 & molecule 12 - relpos[35:47, :] = np.array([[0.06, 0.75, 0.7], - [0.16, 0.75, 0.7], - [0.26, 0.75, 0.7], - [0.36, 0.75, 0.7], - [1.14, 0.65, 0.6], - [1.04, 0.65, 0.6], - [0.94, 0.65, 0.6], - [0.84, 0.65, 0.6], - [0.74, 0.65, 0.6], - [0.64, 0.65, 0.6], - [0.54, 0.65, 0.6], - [0.44, 0.65, 0.6]], - dtype=np.float32) + relpos[9:12, :] = np.array( + [[0.8, 0.1, 1.05], [0.8, 0.1, 0.95], [0.9, 0.1, 0.95]], + dtype=np.float32, + ) + # molecule 8 + relpos[15:19, :] = np.array( + [ + [0.4, -0.05, 0.05], + [0.4, -0.05, -0.05], + [0.4, 0.05, -0.05], + [0.4, 0.05, 0.05], + ], + dtype=np.float32, + ) + if comp == "residues": + # molecule 11, residue 1 & molecule 12 + relpos[35:47, :] = np.array( + [ + [0.06, 0.75, 0.7], + [0.16, 0.75, 0.7], + [0.26, 0.75, 0.7], + [0.36, 0.75, 0.7], + [1.14, 0.65, 0.6], + [1.04, 0.65, 0.6], + [0.94, 0.65, 0.6], + [0.84, 0.65, 0.6], + [0.74, 0.65, 0.6], + [0.64, 0.65, 0.6], + [0.54, 0.65, 0.6], + [0.44, 0.65, 0.6], + ], + dtype=np.float32, + ) else: - #molecule 11 & 12 - relpos[31:47, :] = np.array([[-0.34, 0.75, 0.7], - [-0.24, 0.75, 0.7], - [-0.14, 0.75, 0.7], - [-0.04, 0.75, 0.7], - [0.06, 0.75, 0.7], - [0.16, 0.75, 0.7], - [0.26, 0.75, 0.7], - [0.36, 0.75, 0.7], - [1.14, 0.65, 0.6], - [1.04, 0.65, 0.6], - [0.94, 0.65, 0.6], - [0.84, 0.65, 0.6], - [0.74, 0.65, 0.6], - [0.64, 0.65, 0.6], - [0.54, 0.65, 0.6], - [0.44, 0.65, 0.6]], - dtype=np.float32) + # molecule 11 & 12 + relpos[31:47, :] = np.array( + [ + [-0.34, 0.75, 0.7], + [-0.24, 0.75, 0.7], + [-0.14, 0.75, 0.7], + [-0.04, 0.75, 0.7], + [0.06, 0.75, 0.7], + [0.16, 0.75, 0.7], + [0.26, 0.75, 0.7], + [0.36, 0.75, 0.7], + [1.14, 0.65, 0.6], + [1.04, 0.65, 0.6], + [0.94, 0.65, 0.6], + [0.84, 0.65, 0.6], + [0.74, 0.65, 0.6], + [0.64, 0.65, 0.6], + [0.54, 0.65, 0.6], + [0.44, 0.65, 0.6], + ], + dtype=np.float32, + ) # apply y- and z-dependent shift of x and y coords for triclinic boxes: if self._is_triclinic: @@ -469,18 +521,24 @@ def wrapped_coords(self, compound, center): identical. """ ctr = center.lower() - if ctr not in ['com', 'cog']: + if ctr not in ["com", "cog"]: raise ValueError("Unknown unwrap reference: {}".format(center)) comp = compound.lower() - if comp not in ['atoms', 'group', 'segments', 'residues', - 'molecules', 'fragments']: + if comp not in [ + "atoms", + "group", + "segments", + "residues", + "molecules", + "fragments", + ]: raise ValueError("Unknown unwrap compound: {}".format(compound)) # wrapped relative positions: relpos = self._relpos.copy() # apply required box shifts: - if comp == 'atoms': + if comp == "atoms": # type A # type A # molecule 2: negative x-shift @@ -505,10 +563,10 @@ def wrapped_coords(self, compound, center): relpos[39:41, 0] -= 1.0 # molecule 12: positive z-shift relpos[39:47, 2] += 1.0 - elif comp == 'group': + elif comp == "group": # com or cog of entire system is within box, so no shift pass - elif comp == 'segments': + elif comp == "segments": # type A # molecules 1-3: negative x-shift relpos[0:3, 0] -= 1.0 @@ -521,13 +579,13 @@ def wrapped_coords(self, compound, center): relpos[1, 0] -= 1.0 # molecule 2: negative double x-shift relpos[2, 0] -= 2.0 - #type B + # type B # molecule 6: positive x- and y-shift relpos[9:12, :2] += 1.0 - #type C + # type C # molecule 8: negative z-shift relpos[15:19, 2] -= 1.0 - #type D + # type D # molecule 12: positive z-shift relpos[39:47, 2] += 1.0 @@ -564,41 +622,46 @@ def center(self, compound): relpos = self.unwrapped_coords(compound, reference=None) comp = compound.lower() - if comp not in ['group', 'segments', 'residues', 'molecules', - 'fragments']: + if comp not in [ + "group", + "segments", + "residues", + "molecules", + "fragments", + ]: raise ValueError("Unknown unwrap compound: {}".format(compound)) pos = 0 - if compound=="residues": + if compound == "residues": center_pos = np.zeros((15, 3), dtype=np.float32) else: center_pos = np.zeros((12, 3), dtype=np.float32) for base in range(3): loc_center = relpos[base, :] - center_pos[pos,:] = loc_center - pos+=1 + center_pos[pos, :] = loc_center + pos += 1 for base in range(3, 15, 3): - loc_center = np.mean(relpos[base:base + 3, :], axis=0) - center_pos[pos,:] = loc_center - pos+=1 + loc_center = np.mean(relpos[base : base + 3, :], axis=0) + center_pos[pos, :] = loc_center + pos += 1 - if compound=="residues": + if compound == "residues": for base in range(15, 47, 4): - loc_center = np.mean(relpos[base:base + 4, :], axis=0) - center_pos[pos,:] = loc_center - pos+=1 + loc_center = np.mean(relpos[base : base + 4, :], axis=0) + center_pos[pos, :] = loc_center + pos += 1 else: for base in range(15, 23, 4): - loc_center = np.mean(relpos[base:base + 4, :], axis=0) - center_pos[pos,:] = loc_center - pos+=1 + loc_center = np.mean(relpos[base : base + 4, :], axis=0) + center_pos[pos, :] = loc_center + pos += 1 for base in range(23, 47, 8): - loc_center = np.mean(relpos[base:base + 8, :], axis=0) - center_pos[pos,:] = loc_center - pos+=1 + loc_center = np.mean(relpos[base : base + 8, :], axis=0) + center_pos[pos, :] = loc_center + pos += 1 if compound == "group": center_pos = center_pos[11] @@ -606,4 +669,3 @@ def center(self, compound): center_pos = center_pos[9:] return center_pos - diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index 8e9ed998022..acac47ed0b9 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -171,6 +171,7 @@ setup\.py | MDAnalysisTests/formats/.*\.py | MDAnalysisTests/parallelism/.*\.py | MDAnalysisTests/scripts/.*\.py +| MDAnalysisTests/core/.*\.py | MDAnalysisTests/import/.*\.py | MDAnalysisTests/utils/.*\.py | MDAnalysisTests/visualization/.*\.py From 263bbe65047e535fbad981852ad9c7327826f93d Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Wed, 8 Jan 2025 23:06:39 +0100 Subject: [PATCH 57/58] format all (#4885) --- package/MDAnalysis/__init__.py | 48 +- package/MDAnalysis/lib/distances.py | 2 +- package/MDAnalysis/units.py | 229 +++-- package/doc/sphinx/source/conf.py | 197 ++-- package/pyproject.toml | 19 - testsuite/MDAnalysisTests/__init__.py | 2 +- testsuite/MDAnalysisTests/datafiles.py | 957 +++++++++++------- testsuite/MDAnalysisTests/dummy.py | 79 +- testsuite/MDAnalysisTests/test_api.py | 26 +- testsuite/MDAnalysisTests/util.py | 51 +- testsuite/pyproject.toml | 21 - .../scripts/modeller_make_A6PA6_alpha.py | 10 +- 12 files changed, 949 insertions(+), 692 deletions(-) diff --git a/package/MDAnalysis/__init__.py b/package/MDAnalysis/__init__.py index 69d992afef8..6843c3738ab 100644 --- a/package/MDAnalysis/__init__.py +++ b/package/MDAnalysis/__init__.py @@ -150,8 +150,7 @@ """ -__all__ = ['Universe', 'Writer', - 'AtomGroup', 'ResidueGroup', 'SegmentGroup'] +__all__ = ["Universe", "Writer", "AtomGroup", "ResidueGroup", "SegmentGroup"] import logging import warnings @@ -161,10 +160,11 @@ logger = logging.getLogger("MDAnalysis.__init__") from .version import __version__ + try: from .authors import __authors__ except ImportError: - logger.info('Could not find authors.py, __authors__ will be empty.') + logger.info("Could not find authors.py, __authors__ will be empty.") __authors__ = [] # Registry of Readers, Parsers and Writers known to MDAnalysis @@ -178,16 +178,23 @@ _SELECTION_WRITERS: Dict = {} _CONVERTERS: Dict = {} # Registry of TopologyAttributes -_TOPOLOGY_ATTRS: Dict = {} # {attrname: cls} -_TOPOLOGY_TRANSPLANTS: Dict = {} # {name: [attrname, method, transplant class]} -_TOPOLOGY_ATTRNAMES: Dict = {} # {lower case name w/o _ : name} +_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 ( - SelectionError, NoDataError, ApplicationError, SelectionWarning, - MissingDataWarning, ConversionWarning, FileFormatWarning, - StreamWarning + SelectionError, + NoDataError, + ApplicationError, + SelectionWarning, + MissingDataWarning, + ConversionWarning, + FileFormatWarning, + StreamWarning, ) from .lib import log @@ -197,8 +204,9 @@ del logging # only MDAnalysis DeprecationWarnings are loud by default -warnings.filterwarnings(action='once', category=DeprecationWarning, - module='MDAnalysis') +warnings.filterwarnings( + action="once", category=DeprecationWarning, module="MDAnalysis" +) from . import units @@ -213,11 +221,17 @@ from .due import due, Doi, BibTeX -due.cite(Doi("10.25080/majora-629e541a-00e"), - description="Molecular simulation analysis library", - path="MDAnalysis", cite_module=True) -due.cite(Doi("10.1002/jcc.21787"), - description="Molecular simulation analysis library", - path="MDAnalysis", cite_module=True) +due.cite( + Doi("10.25080/majora-629e541a-00e"), + description="Molecular simulation analysis library", + path="MDAnalysis", + cite_module=True, +) +due.cite( + Doi("10.1002/jcc.21787"), + description="Molecular simulation analysis library", + path="MDAnalysis", + cite_module=True, +) del Doi, BibTeX diff --git a/package/MDAnalysis/lib/distances.py b/package/MDAnalysis/lib/distances.py index 2759a0ffb32..bbd1e56dd5b 100644 --- a/package/MDAnalysis/lib/distances.py +++ b/package/MDAnalysis/lib/distances.py @@ -403,7 +403,7 @@ def self_distance_array( for j in range(i + 1, n): dist[i, j] = dist[j, i] = d[k] k += 1 - + .. versionchanged:: 0.13.0 Added *backend* keyword. .. versionchanged:: 0.19.0 diff --git a/package/MDAnalysis/units.py b/package/MDAnalysis/units.py index 1affd05367d..15d4111df6b 100644 --- a/package/MDAnalysis/units.py +++ b/package/MDAnalysis/units.py @@ -173,15 +173,17 @@ # Remove in 2.8.0 class DeprecatedKeyAccessDict(dict): - deprecated_kB = 'Boltzman_constant' + deprecated_kB = "Boltzman_constant" def __getitem__(self, key): if key == self.deprecated_kB: - wmsg = ("Please use 'Boltzmann_constant' henceforth. The key " - "'Boltzman_constant' was a typo and will be removed " - "in MDAnalysis 2.8.0.") + wmsg = ( + "Please use 'Boltzmann_constant' henceforth. The key " + "'Boltzman_constant' was a typo and will be removed " + "in MDAnalysis 2.8.0." + ) warnings.warn(wmsg, DeprecationWarning) - key = 'Boltzmann_constant' + key = "Boltzmann_constant" return super().__getitem__(key) @@ -202,24 +204,31 @@ def __getitem__(self, key): #: http://physics.nist.gov/Pubs/SP811/appenB8.html#C #: #: .. versionadded:: 0.9.0 -constants = DeprecatedKeyAccessDict({ - 'N_Avogadro': 6.02214129e+23, # mol**-1 - 'elementary_charge': 1.602176565e-19, # As - 'calorie': 4.184, # J - 'Boltzmann_constant': 8.314462159e-3, # KJ (mol K)**-1 - 'electric_constant': 5.526350e-3, # As (Angstroms Volts)**-1 -}) +constants = DeprecatedKeyAccessDict( + { + "N_Avogadro": 6.02214129e23, # mol**-1 + "elementary_charge": 1.602176565e-19, # As + "calorie": 4.184, # J + "Boltzmann_constant": 8.314462159e-3, # KJ (mol K)**-1 + "electric_constant": 5.526350e-3, # As (Angstroms Volts)**-1 + } +) #: The basic unit of *length* in MDAnalysis is the Angstrom. #: Conversion factors between the base unit and other lengthUnits *x* are stored. #: Conversions follow `L/x = L/Angstrom * lengthUnit_factor[x]`. #: *x* can be *nm*/*nanometer* or *fm*. lengthUnit_factor = { - 'Angstrom': 1.0, 'A': 1.0, 'angstrom': 1.0, - u'\u212b': 1.0, # Unicode and UTF-8 encoded symbol for angstroms - 'nm': 1.0 / 10, 'nanometer': 1.0 / 10, - 'pm': 1e2, 'picometer': 1e2, - 'fm': 1e5, 'femtometer': 1e5, + "Angstrom": 1.0, + "A": 1.0, + "angstrom": 1.0, + "\u212b": 1.0, # Unicode and UTF-8 encoded symbol for angstroms + "nm": 1.0 / 10, + "nanometer": 1.0 / 10, + "pm": 1e2, + "picometer": 1e2, + "fm": 1e5, + "femtometer": 1e5, } @@ -235,8 +244,11 @@ def __getitem__(self, key): #: #: and molar mass 18.016 g mol**-1. water = { - 'exp': 0.997, 'SPC': 0.985, 'TIP3P': 1.002, 'TIP4P': 1.001, # in g cm**-3 - 'MolarMass': 18.016, # in g mol**-1 + "exp": 0.997, + "SPC": 0.985, + "TIP3P": 1.002, + "TIP4P": 1.001, # in g cm**-3 + "MolarMass": 18.016, # in g mol**-1 } #: The basic unit for *densities* is Angstrom**(-3), i.e. @@ -244,78 +256,97 @@ def __getitem__(self, key): #: it can be convenient to measure the density relative to bulk, and #: hence a number of values are pre-stored in :data:`water`. densityUnit_factor = { - 'Angstrom^{-3}': 1 / 1.0, 'A^{-3}': 1 / 1.0, - '\u212b^{-3}': 1 / 1.0, - 'nm^{-3}': 1 / 1e-3, 'nanometer^{-3}': 1 / 1e-3, - 'Molar': 1 / (1e-27 * constants['N_Avogadro']), - 'SPC': 1 / (1e-24 * constants['N_Avogadro'] * water['SPC'] / water['MolarMass']), - 'TIP3P': 1 / (1e-24 * constants['N_Avogadro'] * water['TIP3P'] / water['MolarMass']), - 'TIP4P': 1 / (1e-24 * constants['N_Avogadro'] * water['TIP4P'] / water['MolarMass']), - 'water': 1 / (1e-24 * constants['N_Avogadro'] * water['exp'] / water['MolarMass']), + "Angstrom^{-3}": 1 / 1.0, + "A^{-3}": 1 / 1.0, + "\u212b^{-3}": 1 / 1.0, + "nm^{-3}": 1 / 1e-3, + "nanometer^{-3}": 1 / 1e-3, + "Molar": 1 / (1e-27 * constants["N_Avogadro"]), + "SPC": 1 + / (1e-24 * constants["N_Avogadro"] * water["SPC"] / water["MolarMass"]), + "TIP3P": 1 + / (1e-24 * constants["N_Avogadro"] * water["TIP3P"] / water["MolarMass"]), + "TIP4P": 1 + / (1e-24 * constants["N_Avogadro"] * water["TIP4P"] / water["MolarMass"]), + "water": 1 + / (1e-24 * constants["N_Avogadro"] * water["exp"] / water["MolarMass"]), } #: For *time*, the basic unit is ps; in particular CHARMM's #: 1 AKMA_ time unit = 4.888821E-14 sec is supported. timeUnit_factor = { - 'ps': 1.0, 'picosecond': 1.0, # 1/1.0 - 'fs': 1e3, 'femtosecond': 1e3, # 1/1e-3, - 'ns': 1e-3, 'nanosecond': 1e-3, # 1/1e3, - 'ms': 1e-9, 'millisecond': 1e-9, # 1/1e9, - 'us': 1e-6, 'microsecond': 1e-6, '\u03BCs': 1e-6, # 1/1e6, - 'second': 1e-12, 'sec': 1e-12, 's': 1e-12, # 1/1e12, - 'AKMA': 1 / 4.888821e-2, + "ps": 1.0, + "picosecond": 1.0, # 1/1.0 + "fs": 1e3, + "femtosecond": 1e3, # 1/1e-3, + "ns": 1e-3, + "nanosecond": 1e-3, # 1/1e3, + "ms": 1e-9, + "millisecond": 1e-9, # 1/1e9, + "us": 1e-6, + "microsecond": 1e-6, + "\u03BCs": 1e-6, # 1/1e6, + "second": 1e-12, + "sec": 1e-12, + "s": 1e-12, # 1/1e12, + "AKMA": 1 / 4.888821e-2, } # getting the factor f: 1200ps * f = 1.2 ns ==> f = 1/1000 ns/ps #: For *speed*, the basic unit is Angstrom/ps. speedUnit_factor = { - 'Angstrom/ps': 1.0, 'A/ps': 1.0, '\u212b/ps': 1.0, - 'Angstrom/picosecond': 1.0, - 'angstrom/picosecond': 1.0, # 1 - 'Angstrom/fs': 1.0 * 1e3, - 'Angstrom/femtosecond': 1.0 * 1e3, - 'angstrom/femtosecond': 1.0 * 1e3, - 'angstrom/fs': 1.0 * 1e3, - 'A/fs': 1.0 * 1e3, - 'Angstrom/ms': 1.0 * 1e-9, - 'Angstrom/millisecond': 1.0 * 1e-9, - 'angstrom/millisecond': 1.0 * 1e-9, - 'angstrom/ms': 1.0 * 1e-9, - 'A/ms': 1.0 * 1e-9, - 'Angstrom/us': 1.0 * 1e-6, - 'angstrom/us': 1.0 * 1e-6, - 'A/us': 1.0 * 1e-6, - 'Angstrom/microsecond': 1.0 * 1e-6, - 'angstrom/microsecond': 1.0 * 1e-6, - 'Angstrom/\u03BCs': 1.0 * 1e-6, - 'angstrom/\u03BCs': 1.0 * 1e-6, - 'Angstrom/AKMA': 4.888821e-2, - 'A/AKMA': 4.888821e-2, - 'nm/ps': 0.1, 'nanometer/ps': 0.1, 'nanometer/picosecond': 0.1, # 1/10 - 'nm/ns': 0.1 / 1e-3, - 'pm/ps': 1e2, - 'm/s': 1e-10 / 1e-12, + "Angstrom/ps": 1.0, + "A/ps": 1.0, + "\u212b/ps": 1.0, + "Angstrom/picosecond": 1.0, + "angstrom/picosecond": 1.0, # 1 + "Angstrom/fs": 1.0 * 1e3, + "Angstrom/femtosecond": 1.0 * 1e3, + "angstrom/femtosecond": 1.0 * 1e3, + "angstrom/fs": 1.0 * 1e3, + "A/fs": 1.0 * 1e3, + "Angstrom/ms": 1.0 * 1e-9, + "Angstrom/millisecond": 1.0 * 1e-9, + "angstrom/millisecond": 1.0 * 1e-9, + "angstrom/ms": 1.0 * 1e-9, + "A/ms": 1.0 * 1e-9, + "Angstrom/us": 1.0 * 1e-6, + "angstrom/us": 1.0 * 1e-6, + "A/us": 1.0 * 1e-6, + "Angstrom/microsecond": 1.0 * 1e-6, + "angstrom/microsecond": 1.0 * 1e-6, + "Angstrom/\u03BCs": 1.0 * 1e-6, + "angstrom/\u03BCs": 1.0 * 1e-6, + "Angstrom/AKMA": 4.888821e-2, + "A/AKMA": 4.888821e-2, + "nm/ps": 0.1, + "nanometer/ps": 0.1, + "nanometer/picosecond": 0.1, # 1/10 + "nm/ns": 0.1 / 1e-3, + "pm/ps": 1e2, + "m/s": 1e-10 / 1e-12, } # (TODO: build this combinatorically from lengthUnit and timeUnit) #: *Energy* is measured in kJ/mol. energyUnit_factor = { - 'kJ/mol': 1.0, - 'kcal/mol': 1/constants['calorie'], - 'J': 1e3/constants['N_Avogadro'], - 'eV': 1e3/(constants['N_Avogadro'] * constants['elementary_charge']), - } + "kJ/mol": 1.0, + "kcal/mol": 1 / constants["calorie"], + "J": 1e3 / constants["N_Avogadro"], + "eV": 1e3 / (constants["N_Avogadro"] * constants["elementary_charge"]), +} #: For *force* the basic unit is kJ/(mol*Angstrom). forceUnit_factor = { - 'kJ/(mol*Angstrom)': 1.0, 'kJ/(mol*A)': 1.0, - 'kJ/(mol*\u212b)': 1.0, - 'kJ/(mol*nm)': 10.0, - 'Newton': 1e13/constants['N_Avogadro'], - 'N': 1e13/constants['N_Avogadro'], - 'J/m': 1e13/constants['N_Avogadro'], - 'kcal/(mol*Angstrom)': 1/constants['calorie'], + "kJ/(mol*Angstrom)": 1.0, + "kJ/(mol*A)": 1.0, + "kJ/(mol*\u212b)": 1.0, + "kJ/(mol*nm)": 10.0, + "Newton": 1e13 / constants["N_Avogadro"], + "N": 1e13 / constants["N_Avogadro"], + "J/m": 1e13 / constants["N_Avogadro"], + "kcal/(mol*Angstrom)": 1 / constants["calorie"], } # (TODO: build this combinatorically from lengthUnit and energyUnit) @@ -329,22 +360,23 @@ def __getitem__(self, key): #: Use CODATA 2010 value for *elementary charge*, which differs from the previously used value #: *e* = 1.602176487 x 10**(-19) C by 7.8000000e-27 C. chargeUnit_factor = { - 'e': 1.0, - 'Amber': 18.2223, # http://ambermd.org/formats.html#parm - 'C': constants['elementary_charge'], 'As': constants['elementary_charge'], + "e": 1.0, + "Amber": 18.2223, # http://ambermd.org/formats.html#parm + "C": constants["elementary_charge"], + "As": constants["elementary_charge"], } #: :data:`conversion_factor` is used by :func:`get_conversion_factor` #: NOTE: any observable with a unit (i.e. one with an entry in #: the :attr:`unit` attribute) needs an entry in :data:`conversion_factor` conversion_factor = { - 'length': lengthUnit_factor, - 'density': densityUnit_factor, - 'time': timeUnit_factor, - 'charge': chargeUnit_factor, - 'speed': speedUnit_factor, - 'force': forceUnit_factor, - 'energy': energyUnit_factor, + "length": lengthUnit_factor, + "density": densityUnit_factor, + "time": timeUnit_factor, + "charge": chargeUnit_factor, + "speed": speedUnit_factor, + "force": forceUnit_factor, + "energy": energyUnit_factor, } #: Generated lookup table (dict): returns the type of unit for a known input unit. @@ -357,12 +389,14 @@ def __getitem__(self, key): unit_types[unit] = utype #: Lookup table for base units in MDAnalysis by unit type. -MDANALYSIS_BASE_UNITS = {"length": "A", - "time": "ps", - "energy": "kJ/mol", - "charge": "e", - "force": "kJ/(mol*A)", - "speed": "A/ps"} +MDANALYSIS_BASE_UNITS = { + "length": "A", + "time": "ps", + "energy": "kJ/mol", + "charge": "e", + "force": "kJ/(mol*A)", + "speed": "A/ps", +} def get_conversion_factor(unit_type, u1, u2): @@ -397,17 +431,22 @@ def convert(x, u1, u2): try: ut1 = unit_types[u1] except KeyError: - errmsg = (f"unit '{u1}' not recognized.\n" - f"It must be one of {', '.join(unit_types)}.") + errmsg = ( + f"unit '{u1}' not recognized.\n" + f"It must be one of {', '.join(unit_types)}." + ) raise ValueError(errmsg) from None - + try: ut2 = unit_types[u2] except KeyError: - errmsg = (f"unit '{u2}' not recognized.\n" - f"It must be one of {', '.join(unit_types)}.") + errmsg = ( + f"unit '{u2}' not recognized.\n" + f"It must be one of {', '.join(unit_types)}." + ) raise ValueError(errmsg) from None if ut1 != ut2: - raise ValueError("Cannot convert between unit types " - "{0} --> {1}".format(u1, u2)) + raise ValueError( + "Cannot convert between unit types " "{0} --> {1}".format(u1, u2) + ) return x * get_conversion_factor(ut1, u1, u2) diff --git a/package/doc/sphinx/source/conf.py b/package/doc/sphinx/source/conf.py index 40a096c0275..d1a288ed346 100644 --- a/package/doc/sphinx/source/conf.py +++ b/package/doc/sphinx/source/conf.py @@ -15,6 +15,7 @@ import os import datetime import MDAnalysis as mda + # Custom MDA Formating from pybtex.style.formatting.unsrt import Style as UnsrtStyle from pybtex.style.labels import BaseLabelStyle @@ -26,29 +27,29 @@ # here. # make sure sphinx always uses the current branch -sys.path.insert(0, os.path.abspath('../../..')) +sys.path.insert(0, os.path.abspath("../../..")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', - 'sphinx.ext.todo', - 'sphinx_sitemap', - 'mdanalysis_sphinx_theme', - 'sphinxcontrib.bibtex', - 'sphinx.ext.doctest', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx_sitemap", + "mdanalysis_sphinx_theme", + "sphinxcontrib.bibtex", + "sphinx.ext.doctest", ] -bibtex_bibfiles = ['references.bib'] +bibtex_bibfiles = ["references.bib"] # Define custom MDA style for references @@ -56,20 +57,20 @@ class KeyLabelStyle(BaseLabelStyle): def format_labels(self, entries): entry_list = [] for entry in entries: - author = str(entry.persons['author'][0]).split(",")[0] - year = entry.fields['year'] + author = str(entry.persons["author"][0]).split(",")[0] + year = entry.fields["year"] entry_list.append(f"{author}{year}") return entry_list class KeyStyle(UnsrtStyle): - default_label_style = 'keylabel' + default_label_style = "keylabel" -register_plugin('pybtex.style.labels', 'keylabel', KeyLabelStyle) -register_plugin('pybtex.style.formatting', 'MDA', KeyStyle) +register_plugin("pybtex.style.labels", "keylabel", KeyLabelStyle) +register_plugin("pybtex.style.formatting", "MDA", KeyStyle) -mathjax_path = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML' +mathjax_path = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML" # for sitemap with https://github.com/jdillard/sphinx-sitemap # This sitemap is correct both for the development and release docs, which @@ -84,31 +85,31 @@ class KeyStyle(UnsrtStyle): # templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. # 'index' has the advantage that it is immediately picked up by the webserver -master_doc = 'index' +master_doc = "index" # General information about the project. # (take the list from AUTHORS) # Ordering: (1) Naveen (2) Elizabeth, then all contributors in alphabetical order # (last) Oliver author_list = mda.__authors__ -authors = u', '.join(author_list[:-1]) + u', and ' + author_list[-1] -project = u'MDAnalysis' +authors = ", ".join(author_list[:-1]) + ", and " + author_list[-1] +project = "MDAnalysis" now = datetime.datetime.now() -copyright = u'2005-{}, '.format(now.year) + authors +copyright = "2005-{}, ".format(now.year) + authors # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # Dynamically calculate the version -packageversion = __import__('MDAnalysis').__version__ +packageversion = __import__("MDAnalysis").__version__ # The short X.Y version. # version = '.'.join(packageversion.split('.')[:2]) version = packageversion # needed for right sitemap.xml URLs @@ -117,40 +118,40 @@ class KeyStyle(UnsrtStyle): # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'default' +pygments_style = "default" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # to include decorated objects like __init__ -autoclass_content = 'both' +autoclass_content = "both" # to prevent including of member entries in toctree toc_object_entries = False @@ -159,7 +160,7 @@ class KeyStyle(UnsrtStyle): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'mdanalysis_sphinx_theme' +html_theme = "mdanalysis_sphinx_theme" extra_nav_links = {} extra_nav_links["MDAnalysis"] = "http://mdanalysis.org" @@ -176,15 +177,15 @@ class KeyStyle(UnsrtStyle): # further. For a list of options available for each theme, see the # documentation. html_context = { - 'versions_json_url': 'https://docs.mdanalysis.org/versions.json' + "versions_json_url": "https://docs.mdanalysis.org/versions.json" } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -194,162 +195,158 @@ class KeyStyle(UnsrtStyle): # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # alabaster sidebars html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', - 'searchbox.html', + "**": [ + "about.html", + "navigation.html", + "relations.html", + "searchbox.html", ] } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -html_use_opensearch = 'https://docs.mdanalysis.org' +html_use_opensearch = "https://docs.mdanalysis.org" # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'MDAnalysisdoc' +htmlhelp_basename = "MDAnalysisdoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('MDAnalysis.tex', u'MDAnalysis Documentation', - authors, 'manual'), + ("MDAnalysis.tex", "MDAnalysis Documentation", authors, "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('mdanalysis', u'MDAnalysis Documentation', - [authors], 1) -] +man_pages = [("mdanalysis", "MDAnalysis Documentation", [authors], 1)] # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'MDAnalysis' +epub_title = "MDAnalysis" epub_author = authors -epub_publisher = 'Arizona State University, Tempe, Arizona, USA' -epub_copyright = u'2015, '+authors +epub_publisher = "Arizona State University, Tempe, Arizona, USA" +epub_copyright = "2015, " + authors # The language of the text. It defaults to the language option # or en if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. -#epub_exclude_files = [] +# epub_exclude_files = [] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True # Configuration for intersphinx: refer to the Python standard library # and other packages used by MDAnalysis intersphinx_mapping = { - 'h5py': ('https://docs.h5py.org/en/stable', None), - 'python': ('https://docs.python.org/3/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/', None), - 'gsd': ('https://gsd.readthedocs.io/en/stable/', None), - 'maplotlib': ('https://matplotlib.org/stable/', None), - 'griddataformats': ('https://mdanalysis.org/GridDataFormats/', None), - 'pmda': ('https://mdanalysis.org/pmda/', None), - 'networkx': ('https://networkx.org/documentation/stable/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'parmed': ('https://parmed.github.io/ParmEd/html/', None), - 'rdkit': ('https://rdkit.org/docs/', None), - 'waterdynamics': ('https://www.mdanalysis.org/waterdynamics/', None), - 'pathsimanalysis': ('https://www.mdanalysis.org/PathSimAnalysis/', None), - 'mdahole2': ('https://www.mdanalysis.org/mdahole2/', None), - 'dask': ('https://docs.dask.org/en/stable/', None), + "h5py": ("https://docs.h5py.org/en/stable", None), + "python": ("https://docs.python.org/3/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "gsd": ("https://gsd.readthedocs.io/en/stable/", None), + "maplotlib": ("https://matplotlib.org/stable/", None), + "griddataformats": ("https://mdanalysis.org/GridDataFormats/", None), + "pmda": ("https://mdanalysis.org/pmda/", None), + "networkx": ("https://networkx.org/documentation/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "parmed": ("https://parmed.github.io/ParmEd/html/", None), + "rdkit": ("https://rdkit.org/docs/", None), + "waterdynamics": ("https://www.mdanalysis.org/waterdynamics/", None), + "pathsimanalysis": ("https://www.mdanalysis.org/PathSimAnalysis/", None), + "mdahole2": ("https://www.mdanalysis.org/mdahole2/", None), + "dask": ("https://docs.dask.org/en/stable/", None), } diff --git a/package/pyproject.toml b/package/pyproject.toml index 04e76fdbfed..7e0ac361f26 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -126,25 +126,6 @@ MDAnalysis = [ [tool.black] line-length = 79 target-version = ['py310', 'py311', 'py312', 'py313'] -include = ''' -( -tables\.py -| due\.py -| setup\.py -| MDAnalysis/auxiliary/.*\.py -| MDAnalysis/visualization/.*\.py -| MDAnalysis/lib/.*\.py^ -| MDAnalysis/transformations/.*\.py -| MDAnalysis/topology/.*\.py -| MDAnalysis/analysis/.*\.py -| MDAnalysis/guesser/.*\.py -| MDAnalysis/converters/.*\.py -| MDAnalysis/coordinates/.*\.py -| MDAnalysis/tests/.*\.py -| MDAnalysis/selections/.*\.py -| MDAnalysis/core/.*\.py -) -''' extend-exclude = ''' ( __pycache__ diff --git a/testsuite/MDAnalysisTests/__init__.py b/testsuite/MDAnalysisTests/__init__.py index ea7b2d55706..75acebf06be 100644 --- a/testsuite/MDAnalysisTests/__init__.py +++ b/testsuite/MDAnalysisTests/__init__.py @@ -114,7 +114,7 @@ # collection of citations on the first `import MDAnalysis` so the environment # variable *must* come before MDAnalysis is imported the first time. See # issue #412 https://github.com/MDAnalysis/mdanalysis/issues/412 and PR #1822. -os.environ['DUECREDIT_ENABLE'] = 'yes' +os.environ["DUECREDIT_ENABLE"] = "yes" # Any tests that plot with matplotlib need to run with the simple agg backend # because on Travis there is no DISPLAY set. diff --git a/testsuite/MDAnalysisTests/datafiles.py b/testsuite/MDAnalysisTests/datafiles.py index 3d3d7d93129..deef782d773 100644 --- a/testsuite/MDAnalysisTests/datafiles.py +++ b/testsuite/MDAnalysisTests/datafiles.py @@ -37,14 +37,21 @@ """ __all__ = [ - "PSF", "DCD", "CRD", # CHARMM (AdK example, DIMS trajectory from JMB 2009 paper) + "PSF", + "DCD", + "CRD", # CHARMM (AdK example, DIMS trajectory from JMB 2009 paper) "DCD2", # CHARMM (AdK example, DIMS trajectory from PLOS Comput Biol paper) - "PSF_notop", "PSF_BAD", # Same as PSF but no bonds etc, malformed version of previous + "PSF_notop", + "PSF_BAD", # Same as PSF but no bonds etc, malformed version of previous "DCD_empty", - "PSF_TRICLINIC", "DCD_TRICLINIC", # CHARMM c36 new unitcell, NPT 125 TIP3P (box vectors, see Issue 187 for details) - "PSF_NAMD", "PDB_NAMD", # NAMD - "PSF_NAMD_TRICLINIC", "DCD_NAMD_TRICLINIC", # NAMD, triclinic unitcell (Issue 187) - "PSF_NAMD_GBIS", "DCD_NAMD_GBIS", # NAMD, implicit solvent, 100 steps, #1819 + "PSF_TRICLINIC", + "DCD_TRICLINIC", # CHARMM c36 new unitcell, NPT 125 TIP3P (box vectors, see Issue 187 for details) + "PSF_NAMD", + "PDB_NAMD", # NAMD + "PSF_NAMD_TRICLINIC", + "DCD_NAMD_TRICLINIC", # NAMD, triclinic unitcell (Issue 187) + "PSF_NAMD_GBIS", + "DCD_NAMD_GBIS", # NAMD, implicit solvent, 100 steps, #1819 "PSF_nosegid", # psf without a segid, Issue 121 "PSF_cmap", # ala3 PSF from ParmEd test files with cmap "PSF_inscode", # PSF file with insertion codes @@ -58,63 +65,130 @@ "PDB_icodes", # stripped down version of 1osm, has icodes! "PDB_varying", # varying occupancies and tempfactors "XPDB_small", - "PDB_full", # PDB 4E43 (full HEADER, TITLE, COMPND, REMARK, altloc) + "PDB_full", # PDB 4E43 (full HEADER, TITLE, COMPND, REMARK, altloc) "ALIGN", # Various way to align atom names in PDB files - "RNA_PSF", "RNA_PDB", # nucleic acid (PDB 1K5I in CHARMM36m) + "RNA_PSF", + "RNA_PDB", # nucleic acid (PDB 1K5I in CHARMM36m) "INC_PDB", # incomplete PDB file (Issue #396) # for testing cryst before/after model headers - "PDB_cm", "PDB_cm_bz2", "PDB_cm_gz", - "PDB_mc", "PDB_mc_bz2", "PDB_mc_gz", + "PDB_cm", + "PDB_cm_bz2", + "PDB_cm_gz", + "PDB_mc", + "PDB_mc_bz2", + "PDB_mc_gz", "PDB_chainidnewres", # Issue 1110 - "PDB_sameresid_diffresname", #Case where two residues share the same resid + "PDB_sameresid_diffresname", # Case where two residues share the same resid "PDB_chainidrepeat", # Issue #1107 - "PDB", "GRO", "XTC", "TRR", "TPR", "GRO_velocity", # Gromacs (AdK) + "PDB", + "GRO", + "XTC", + "TRR", + "TPR", + "GRO_velocity", # Gromacs (AdK) "GRO_incomplete_vels", "COORDINATES_GRO_BZ2", - "GRO_large", #atom number truncation at > 100,000 particles, Issue 550 + "GRO_large", # atom number truncation at > 100,000 particles, Issue 550 "GRO_residwrap", # resids wrapping because of 5 digit field (Issue #728) "GRO_residwrap_0base", # corner case of #728 with resid=0 for first atom "GRO_sameresid_diffresname", # Case where two residues share the same resid - "PDB_xvf", "TPR_xvf", "TRR_xvf", # Gromacs coords/veloc/forces (cobrotoxin, OPLS-AA, Gromacs 4.5.5 tpr) + "PDB_xvf", + "TPR_xvf", + "TRR_xvf", # Gromacs coords/veloc/forces (cobrotoxin, OPLS-AA, Gromacs 4.5.5 tpr) "H5MD_xvf", # TPR_xvf + TRR_xvf converted to h5md format "H5MD_energy", # H5MD trajectory with observables/atoms/energy "H5MD_malformed", # H5MD trajectory with malformed observable group "XVG_BZ2", # Compressed xvg file about cobrotoxin "PDB_xlserial", - "TPR400", "TPR402", "TPR403", "TPR404", "TPR405", "TPR406", "TPR407", - "TPR450", "TPR451", "TPR452", "TPR453", "TPR454", "TPR455", "TPR455Double", - "TPR460", "TPR461", "TPR502", "TPR504", "TPR505", "TPR510", "TPR2016", - "TPR2018", "TPR2019B3", "TPR2020B2", "TPR2020", "TPR2020Double", - "TPR2021", "TPR2021Double", "TPR2022RC1", "TPR2023", "TPR2024", + "TPR400", + "TPR402", + "TPR403", + "TPR404", + "TPR405", + "TPR406", + "TPR407", + "TPR450", + "TPR451", + "TPR452", + "TPR453", + "TPR454", + "TPR455", + "TPR455Double", + "TPR460", + "TPR461", + "TPR502", + "TPR504", + "TPR505", + "TPR510", + "TPR2016", + "TPR2018", + "TPR2019B3", + "TPR2020B2", + "TPR2020", + "TPR2020Double", + "TPR2021", + "TPR2021Double", + "TPR2022RC1", + "TPR2023", + "TPR2024", "TPR2024_4", - "TPR510_bonded", "TPR2016_bonded", "TPR2018_bonded", "TPR2019B3_bonded", - "TPR2020B2_bonded", "TPR2020_bonded", "TPR2020_double_bonded", - "TPR2021_bonded", "TPR2021_double_bonded", "TPR2022RC1_bonded", - "TPR334_bonded", "TPR2023_bonded", "TPR2024_bonded", + "TPR510_bonded", + "TPR2016_bonded", + "TPR2018_bonded", + "TPR2019B3_bonded", + "TPR2020B2_bonded", + "TPR2020_bonded", + "TPR2020_double_bonded", + "TPR2021_bonded", + "TPR2021_double_bonded", + "TPR2022RC1_bonded", + "TPR334_bonded", + "TPR2023_bonded", + "TPR2024_bonded", "TPR2024_4_bonded", - "TPR_EXTRA_2021", "TPR_EXTRA_2020", "TPR_EXTRA_2018", - "TPR_EXTRA_2016", "TPR_EXTRA_407", "TPR_EXTRA_2022RC1", - "TPR_EXTRA_2023", "TPR_EXTRA_2024", "TPR_EXTRA_2024_4", - "PDB_sub_sol", "PDB_sub_dry", # TRRReader sub selection + "TPR_EXTRA_2021", + "TPR_EXTRA_2020", + "TPR_EXTRA_2018", + "TPR_EXTRA_2016", + "TPR_EXTRA_407", + "TPR_EXTRA_2022RC1", + "TPR_EXTRA_2023", + "TPR_EXTRA_2024", + "TPR_EXTRA_2024_4", + "PDB_sub_sol", + "PDB_sub_dry", # TRRReader sub selection "TRR_sub_sol", "XTC_sub_sol", - "XYZ", "XYZ_psf", "XYZ_bz2", - "XYZ_mini", "XYZ_five", # 3 and 5 atoms xyzs for an easy topology - "TXYZ", "ARC", "ARC_PBC", # Tinker files + "XYZ", + "XYZ_psf", + "XYZ_bz2", + "XYZ_mini", + "XYZ_five", # 3 and 5 atoms xyzs for an easy topology + "TXYZ", + "ARC", + "ARC_PBC", # Tinker files "PRM", "PRM_chainid_bz2", "TRJ", "TRJ_bz2", # Amber (no periodic box) "INPCRD", - "PRMpbc", "TRJpbc_bz2", # Amber (periodic box) - "PRM7", "NCDFtruncoct", # Amber (cpptrj test trajectory, see Issue 488) - "PRM12", "TRJ12_bz2", # Amber (v12 format, Issue 100) - "PRMncdf", "TRJncdf", "NCDF", # Amber (netcdf) - "PFncdf_Top", "PFncdf_Trj", # Amber ncdf with Positions and Forces - "CPPTRAJ_TRAJ_TOP", "CPPTRAJ_TRAJ", # Amber ncdf extracted from CPPTRAJ without time variable + "PRMpbc", + "TRJpbc_bz2", # Amber (periodic box) + "PRM7", + "NCDFtruncoct", # Amber (cpptrj test trajectory, see Issue 488) + "PRM12", + "TRJ12_bz2", # Amber (v12 format, Issue 100) + "PRMncdf", + "TRJncdf", + "NCDF", # Amber (netcdf) + "PFncdf_Top", + "PFncdf_Trj", # Amber ncdf with Positions and Forces + "CPPTRAJ_TRAJ_TOP", + "CPPTRAJ_TRAJ", # Amber ncdf extracted from CPPTRAJ without time variable "PRMcs", # Amber (format, Issue 1331) "PRMNCRST", # Amber ncrst with positions/forces/velocities - "PRM_NCBOX", "TRJ_NCBOX", # Amber parm7 + nc w/ pos/forces/vels/box + "PRM_NCBOX", + "TRJ_NCBOX", # Amber parm7 + nc w/ pos/forces/vels/box "PRMNEGATIVE", # Amber negative ATOMIC_NUMBER (Issue 2306) "PRMErr1", # Amber TOP files to check raised errors "PRMErr2", @@ -122,7 +196,8 @@ "PRMErr4", "PRMErr5", "PRM_UreyBradley", # prmtop from ParmEd test files with Urey-Bradley angles - "PRM7_ala2", "RST7_ala2", # prmtop and rst files from ParmEd example files + "PRM7_ala2", + "RST7_ala2", # prmtop and rst files from ParmEd example files "PRM19SBOPC", # prmtop w/ ff19SB CMAP terms and OPC water (Issue #2449) "PQR", # PQR v1 "PQR_icodes", # PQR v2 with icodes @@ -140,7 +215,8 @@ "DMS_NO_SEGID", # ADK closed with no segids or chains "CONECT", # HIV Reverse Transcriptase with inhibitor "CONECT_ERROR", # PDB file with corrupt CONECT - "TRZ", "TRZ_psf", + "TRZ", + "TRZ_psf", "TRIC", "XTC_multi_frame", "TRR_multi_frame", @@ -148,15 +224,32 @@ "TNG_traj_gro", # topology for argon_npt_compressed_traj "TNG_traj_uneven_blocks", # TNG trajectory with pos and vel deposited on different strides "TNG_traj_vels_forces", # similar to above but with velocities and forces - "merge_protein", "merge_ligand", "merge_water", - "mol2_molecules", "mol2_molecule", "mol2_broken_molecule", - "mol2_zinc", "mol2_comments_header", "mol2_ligand", "mol2_sodium_ion", - "capping_input", "capping_output", "capping_ace", "capping_nma", - "contacts_villin_folded", "contacts_villin_unfolded", "contacts_file", - "LAMMPSdata", "trz4data", "LAMMPSdata_mini", - "LAMMPSdata2", "LAMMPSdcd2", - "LAMMPScnt", "LAMMPScnt2", # triclinic box - "LAMMPShyd", "LAMMPShyd2", + "merge_protein", + "merge_ligand", + "merge_water", + "mol2_molecules", + "mol2_molecule", + "mol2_broken_molecule", + "mol2_zinc", + "mol2_comments_header", + "mol2_ligand", + "mol2_sodium_ion", + "capping_input", + "capping_output", + "capping_ace", + "capping_nma", + "contacts_villin_folded", + "contacts_villin_unfolded", + "contacts_file", + "LAMMPSdata", + "trz4data", + "LAMMPSdata_mini", + "LAMMPSdata2", + "LAMMPSdcd2", + "LAMMPScnt", + "LAMMPScnt2", # triclinic box + "LAMMPShyd", + "LAMMPShyd2", "LAMMPSdata_many_bonds", "LAMMPSdata_deletedatoms", # with deleted atoms "LAMMPSdata_triclinic", # lammpsdata file to test triclinic dimension parsing, albite with most atoms deleted @@ -169,22 +262,29 @@ "LAMMPSDUMP_triclinic", # lammpsdump file to test triclinic dimension parsing, albite with most atoms deleted "LAMMPSDUMP_image_vf", # Lammps dump file with image flags, velocities, and forces. "LAMMPS_image_vf", # Lammps data file to go with LAMMPSDUMP_image_vf - "LAMMPSDUMP_chain1", # Lammps dump file with chain reader - "LAMMPSDUMP_chain2", # Lammps dump file with chain reader - "LAMMPS_chain", # Lammps data file with chain reader + "LAMMPSDUMP_chain1", # Lammps dump file with chain reader + "LAMMPSDUMP_chain2", # Lammps dump file with chain reader + "LAMMPS_chain", # Lammps data file with chain reader "LAMMPSDUMP_additional_columns", # lammpsdump file with additional data (an additional charge column) "unordered_res", # pdb file with resids non sequential "GMS_ASYMOPT", # GAMESS C1 optimization - "GMS_SYMOPT", # GAMESS D4h optimization + "GMS_SYMOPT", # GAMESS D4h optimization "GMS_ASYMSURF", # GAMESS C1 surface - "two_water_gro", "two_water_gro_nonames", # for bond guessing, 2 water molecules, one with weird names + "two_water_gro", + "two_water_gro_nonames", # for bond guessing, 2 water molecules, one with weird names "two_water_gro_multiframe", "two_water_gro_widebox", # Issue #548 - "DLP_CONFIG", "DLP_CONFIG_order", "DLP_CONFIG_minimal", # dl_poly 4 config file - "DLP_HISTORY", "DLP_HISTORY_order", "DLP_HISTORY_minimal", # dl_poly 4 history file + "DLP_CONFIG", + "DLP_CONFIG_order", + "DLP_CONFIG_minimal", # dl_poly 4 config file + "DLP_HISTORY", + "DLP_HISTORY_order", + "DLP_HISTORY_minimal", # dl_poly 4 history file "DLP_HISTORY_minimal_cell", # dl_poly 4 history file with cell parameters "DLP_HISTORY_classic", # dl_poly classic history file - "waterPSF","waterDCD","rmsfArray", + "waterPSF", + "waterDCD", + "rmsfArray", "HoomdXMLdata", "Make_Whole", # for testing the function lib.mdamath.make_whole, has 9 atoms "fullerene", # for make_whole, a nice friendly C60 with bonds @@ -201,37 +301,60 @@ "COORDINATES_DCD", "COORDINATES_TOPOLOGY", "NUCLsel", - "GRO_empty_atom", "GRO_missing_atomname", # for testing GROParser exception raise - "ENT", #for testing ENT file extension + "GRO_empty_atom", + "GRO_missing_atomname", # for testing GROParser exception raise + "ENT", # for testing ENT file extension "RANDOM_WALK", "RANDOM_WALK_TOPO", # garbage topology to go along with XTC positions above - "AUX_XVG", "XVG_BAD_NCOL", #for testing .xvg auxiliary reader - "AUX_XVG_LOWF", "AUX_XVG_HIGHF", - "AUX_EDR", "AUX_EDR_TPR", - "AUX_EDR_XTC", "AUX_EDR_RAW", + "AUX_XVG", + "XVG_BAD_NCOL", # for testing .xvg auxiliary reader + "AUX_XVG_LOWF", + "AUX_XVG_HIGHF", + "AUX_EDR", + "AUX_EDR_TPR", + "AUX_EDR_XTC", + "AUX_EDR_RAW", "AUX_EDR_SINGLE_FRAME", # for testing .edr auxiliary reader - "MMTF", "MMTF_gz", 'MMTF_skinny', # skinny - some optional fields stripped out + "MMTF", + "MMTF_gz", + "MMTF_skinny", # skinny - some optional fields stripped out "MMTF_skinny2", "ALIGN_BOUND", # two component bound system "ALIGN_UNBOUND", # two component unbound system "legacy_DCD_ADK_coords", # frames 5 and 29 read in for adk_dims.dcd using legacy DCD reader "legacy_DCD_NAMD_coords", # frame 0 read in for SiN_tric_namd.dcd using legacy DCD reader "legacy_DCD_c36_coords", # frames 1 and 4 read in for tip125_tric_C36.dcd using legacy DCD reader - "GSD", "GSD_bonds", "GSD_long", - "TRC_PDB_VAC", "TRC_TRAJ1_VAC", "TRC_TRAJ2_VAC", # 2x 3 frames of vacuum trajectory from GROMOS11 tutorial + "GSD", + "GSD_bonds", + "GSD_long", + "TRC_PDB_VAC", + "TRC_TRAJ1_VAC", + "TRC_TRAJ2_VAC", # 2x 3 frames of vacuum trajectory from GROMOS11 tutorial "TRC_CLUSTER_VAC", # three frames without TIMESTEP and GENBOX block but with unsupported POSITION block - "TRC_TRICLINIC_SOLV", "TRC_TRUNCOCT_VAC", - "TRC_GENBOX_ORIGIN", "TRC_GENBOX_EULER", + "TRC_TRICLINIC_SOLV", + "TRC_TRUNCOCT_VAC", + "TRC_GENBOX_ORIGIN", + "TRC_GENBOX_EULER", "TRC_EMPTY", # Empty file containing only one space - "TRC_PDB_SOLV", "TRC_TRAJ_SOLV", # 2 frames of solvated trajectory from GROMOS11 tutorial - "GRO_MEMPROT", "XTC_MEMPROT", # YiiP transporter in POPE:POPG lipids with Na+, Cl-, Zn2+ dummy model without water - "DihedralArray", "DihedralsArray", # time series of single dihedral - "RamaArray", "GLYRamaArray", # time series of phi/psi angles - "JaninArray", "LYSJaninArray", # time series of chi1/chi2 angles - "PDB_rama", "PDB_janin", # for testing failures of Ramachandran and Janin classes + "TRC_PDB_SOLV", + "TRC_TRAJ_SOLV", # 2 frames of solvated trajectory from GROMOS11 tutorial + "GRO_MEMPROT", + "XTC_MEMPROT", # YiiP transporter in POPE:POPG lipids with Na+, Cl-, Zn2+ dummy model without water + "DihedralArray", + "DihedralsArray", # time series of single dihedral + "RamaArray", + "GLYRamaArray", # time series of phi/psi angles + "JaninArray", + "LYSJaninArray", # time series of chi1/chi2 angles + "PDB_rama", + "PDB_janin", # for testing failures of Ramachandran and Janin classes "BATArray", # time series of bond-angle-torsion coordinates array from Molecule_comments_header.mol2 # DOS line endings - "WIN_PDB_multiframe", "WIN_DLP_HISTORY", "WIN_TRJ", "WIN_LAMMPSDUMP", "WIN_ARC", + "WIN_PDB_multiframe", + "WIN_DLP_HISTORY", + "WIN_TRJ", + "WIN_LAMMPSDUMP", + "WIN_ARC", "GRO_huge_box", # for testing gro parser with hige box sizes "ITP", # for GROMACS generated itps "ITP_nomass", # for ATB generated itps @@ -261,228 +384,274 @@ from importlib import resources import MDAnalysisTests.data -_data_ref = resources.files('MDAnalysisTests.data') - -WIN_PDB_multiframe = (_data_ref / 'windows/WIN_nmr_neopetrosiamide.pdb').as_posix() -WIN_DLP_HISTORY = (_data_ref / 'windows/WIN_HISTORY').as_posix() -WIN_TRJ = (_data_ref / 'windows/WIN_ache.mdcrd').as_posix() -WIN_ARC = (_data_ref / 'windows/WIN_test.arc').as_posix() -WIN_LAMMPSDUMP = (_data_ref / 'windows/WIN_wat.lammpstrj').as_posix() - -legacy_DCD_NAMD_coords = (_data_ref / 'legacy_DCD_NAMD_coords.npy').as_posix() -legacy_DCD_ADK_coords = (_data_ref / 'legacy_DCD_adk_coords.npy').as_posix() -legacy_DCD_c36_coords = (_data_ref / 'legacy_DCD_c36_coords.npy').as_posix() -AUX_XVG_LOWF = (_data_ref / 'test_lowf.xvg').as_posix() -AUX_XVG_HIGHF = (_data_ref / 'test_highf.xvg').as_posix() -XVG_BAD_NCOL = (_data_ref / 'bad_num_col.xvg').as_posix() -AUX_XVG = (_data_ref / 'test.xvg').as_posix() -AUX_EDR = (_data_ref / 'test.edr').as_posix() -AUX_EDR_RAW = (_data_ref / 'aux_edr_raw.txt').as_posix() -AUX_EDR_TPR = (_data_ref / 'aux_edr.tpr').as_posix() -AUX_EDR_XTC = (_data_ref / 'aux_edr.xtc').as_posix() -AUX_EDR_SINGLE_FRAME = (_data_ref / 'single_frame.edr').as_posix() -ENT = (_data_ref / 'testENT.ent').as_posix() -GRO_missing_atomname = (_data_ref / 'missing_atomname.gro').as_posix() -GRO_empty_atom = (_data_ref / 'empty_atom.gro').as_posix() -GRO_huge_box = (_data_ref / 'huge_box.gro').as_posix() - -COORDINATES_GRO = (_data_ref / 'coordinates/test.gro').as_posix() -COORDINATES_GRO_INCOMPLETE_VELOCITY = (_data_ref / 'coordinates/test_incomplete_vel.gro').as_posix() -COORDINATES_GRO_BZ2 = (_data_ref / 'coordinates/test.gro.bz2').as_posix() -COORDINATES_XYZ = (_data_ref / 'coordinates/test.xyz').as_posix() -COORDINATES_XYZ_BZ2 = (_data_ref / 'coordinates/test.xyz.bz2').as_posix() -COORDINATES_XTC = (_data_ref / 'coordinates/test.xtc').as_posix() -COORDINATES_TRR = (_data_ref / 'coordinates/test.trr').as_posix() -COORDINATES_TNG = (_data_ref / 'coordinates/test.tng').as_posix() -COORDINATES_H5MD = (_data_ref / 'coordinates/test.h5md').as_posix() -COORDINATES_DCD = (_data_ref / 'coordinates/test.dcd').as_posix() -COORDINATES_TOPOLOGY = (_data_ref / 'coordinates/test_topology.pdb').as_posix() - -PSF = (_data_ref / 'adk.psf').as_posix() -PSF_notop = (_data_ref / 'adk_notop.psf').as_posix() -PSF_BAD = (_data_ref / 'adk_notop_BAD.psf').as_posix() -DCD = (_data_ref / 'adk_dims.dcd').as_posix() -DCD_empty = (_data_ref / 'empty.dcd').as_posix() -CRD = (_data_ref / 'adk_open.crd').as_posix() -PSF_TRICLINIC = (_data_ref / 'tip125_tric_C36.psf').as_posix() -DCD_TRICLINIC = (_data_ref / 'tip125_tric_C36.dcd').as_posix() -DCD2 = (_data_ref / 'adk_dims2.dcd').as_posix() - -PSF_NAMD = (_data_ref / 'namd_cgenff.psf').as_posix() -PDB_NAMD = (_data_ref / 'namd_cgenff.pdb').as_posix() -PDB_multipole = (_data_ref / 'water_methane_acetic-acid_ammonia.pdb').as_posix() -PSF_NAMD_TRICLINIC = (_data_ref / 'SiN_tric_namd.psf').as_posix() -DCD_NAMD_TRICLINIC = (_data_ref / 'SiN_tric_namd.dcd').as_posix() -PSF_NAMD_GBIS = (_data_ref / 'adk_closed_NAMD.psf').as_posix() -DCD_NAMD_GBIS = (_data_ref / 'adk_gbis_tmd-fast1_NAMD.dcd').as_posix() - -PSF_nosegid = (_data_ref / 'nosegid.psf').as_posix() - -PSF_cmap = (_data_ref / 'parmed_ala3.psf').as_posix() - -PSF_inscode = (_data_ref / '1a2c_ins_code.psf').as_posix() - -PDB_varying = (_data_ref / 'varying_occ_tmp.pdb').as_posix() -PDB_small = (_data_ref / 'adk_open.pdb').as_posix() -PDB_closed = (_data_ref / 'adk_closed.pdb').as_posix() - -ALIGN = (_data_ref / 'align.pdb').as_posix() -RNA_PSF = (_data_ref / 'analysis/1k5i_c36.psf.gz').as_posix() -RNA_PDB = (_data_ref / 'analysis/1k5i_c36.pdb.gz').as_posix() -INC_PDB = (_data_ref / 'incomplete.pdb').as_posix() -PDB_cm = (_data_ref / 'cryst_then_model.pdb').as_posix() -PDB_cm_gz = (_data_ref / 'cryst_then_model.pdb.gz').as_posix() -PDB_cm_bz2 = (_data_ref / 'cryst_then_model.pdb.bz2').as_posix() -PDB_mc = (_data_ref / 'model_then_cryst.pdb').as_posix() -PDB_mc_gz = (_data_ref / 'model_then_cryst.pdb.gz').as_posix() -PDB_mc_bz2 = (_data_ref / 'model_then_cryst.pdb.bz2').as_posix() -PDB_chainidnewres = (_data_ref / 'chainIDnewres.pdb.gz').as_posix() -PDB_sameresid_diffresname = (_data_ref / 'sameresid_diffresname.pdb').as_posix() -PDB_chainidrepeat = (_data_ref / 'chainIDrepeat.pdb.gz').as_posix() -PDB_multiframe = (_data_ref / 'nmr_neopetrosiamide.pdb').as_posix() -PDB_helix = (_data_ref / 'A6PA6_alpha.pdb').as_posix() -PDB_conect = (_data_ref / 'conect_parsing.pdb').as_posix() -PDB_conect2TER = (_data_ref / 'CONECT2TER.pdb').as_posix() -PDB_singleconect = (_data_ref / 'SINGLECONECT.pdb').as_posix() -PDB_icodes = (_data_ref / '1osm.pdb.gz').as_posix() -PDB_CRYOEM_BOX = (_data_ref / '5a7u.pdb').as_posix() -PDB_CHECK_RIGHTHAND_PA = (_data_ref / '6msm.pdb.bz2').as_posix() -FHIAIMS = (_data_ref / 'fhiaims.in').as_posix() - -GRO = (_data_ref / 'adk_oplsaa.gro').as_posix() -GRO_velocity = (_data_ref / 'sample_velocity_file.gro').as_posix() -GRO_incomplete_vels = (_data_ref / 'grovels.gro').as_posix() -GRO_large = (_data_ref / 'bigbox.gro.bz2').as_posix() -GRO_residwrap = (_data_ref / 'residwrap.gro').as_posix() -GRO_residwrap_0base = (_data_ref / 'residwrap_0base.gro').as_posix() -GRO_sameresid_diffresname = (_data_ref / 'sameresid_diffresname.gro').as_posix() -PDB = (_data_ref / 'adk_oplsaa.pdb').as_posix() -XTC = (_data_ref / 'adk_oplsaa.xtc').as_posix() -TRR = (_data_ref / 'adk_oplsaa.trr').as_posix() -TPR = (_data_ref / 'adk_oplsaa.tpr').as_posix() -PDB_sub_dry = (_data_ref / 'cobrotoxin_dry_neutral_0.pdb').as_posix() -TRR_sub_sol = (_data_ref / 'cobrotoxin.trr').as_posix() -XTC_sub_sol = (_data_ref / 'cobrotoxin.xtc').as_posix() -PDB_sub_sol = (_data_ref / 'cobrotoxin.pdb').as_posix() -PDB_xlserial = (_data_ref / 'xl_serial.pdb').as_posix() -GRO_MEMPROT = (_data_ref / 'analysis/YiiP_lipids.gro.gz').as_posix() -XTC_MEMPROT = (_data_ref / 'analysis/YiiP_lipids.xtc').as_posix() -XTC_multi_frame = (_data_ref / 'xtc_test_only_10_frame_10_atoms.xtc').as_posix() -TRR_multi_frame = (_data_ref / 'trr_test_only_10_frame_10_atoms.trr').as_posix() -TNG_traj = (_data_ref / 'argon_npt_compressed.tng').as_posix() -TNG_traj_gro = (_data_ref / 'argon_npt_compressed.gro.gz').as_posix() -TNG_traj_uneven_blocks = (_data_ref / 'argon_npt_compressed_uneven.tng').as_posix() -TNG_traj_vels_forces = (_data_ref / 'argon_npt_compressed_vels_forces.tng').as_posix() -PDB_xvf = (_data_ref / 'cobrotoxin.pdb').as_posix() -TPR_xvf = (_data_ref / 'cobrotoxin.tpr').as_posix() -TRR_xvf = (_data_ref / 'cobrotoxin.trr').as_posix() -H5MD_xvf = (_data_ref / 'cobrotoxin.h5md').as_posix() -H5MD_energy = (_data_ref / 'cu.h5md').as_posix() -H5MD_malformed = (_data_ref / 'cu_malformed.h5md').as_posix() -XVG_BZ2 = (_data_ref / 'cobrotoxin_protein_forces.xvg.bz2').as_posix() - -XPDB_small = (_data_ref / '5digitResid.pdb').as_posix() +_data_ref = resources.files("MDAnalysisTests.data") + +WIN_PDB_multiframe = ( + _data_ref / "windows/WIN_nmr_neopetrosiamide.pdb" +).as_posix() +WIN_DLP_HISTORY = (_data_ref / "windows/WIN_HISTORY").as_posix() +WIN_TRJ = (_data_ref / "windows/WIN_ache.mdcrd").as_posix() +WIN_ARC = (_data_ref / "windows/WIN_test.arc").as_posix() +WIN_LAMMPSDUMP = (_data_ref / "windows/WIN_wat.lammpstrj").as_posix() + +legacy_DCD_NAMD_coords = (_data_ref / "legacy_DCD_NAMD_coords.npy").as_posix() +legacy_DCD_ADK_coords = (_data_ref / "legacy_DCD_adk_coords.npy").as_posix() +legacy_DCD_c36_coords = (_data_ref / "legacy_DCD_c36_coords.npy").as_posix() +AUX_XVG_LOWF = (_data_ref / "test_lowf.xvg").as_posix() +AUX_XVG_HIGHF = (_data_ref / "test_highf.xvg").as_posix() +XVG_BAD_NCOL = (_data_ref / "bad_num_col.xvg").as_posix() +AUX_XVG = (_data_ref / "test.xvg").as_posix() +AUX_EDR = (_data_ref / "test.edr").as_posix() +AUX_EDR_RAW = (_data_ref / "aux_edr_raw.txt").as_posix() +AUX_EDR_TPR = (_data_ref / "aux_edr.tpr").as_posix() +AUX_EDR_XTC = (_data_ref / "aux_edr.xtc").as_posix() +AUX_EDR_SINGLE_FRAME = (_data_ref / "single_frame.edr").as_posix() +ENT = (_data_ref / "testENT.ent").as_posix() +GRO_missing_atomname = (_data_ref / "missing_atomname.gro").as_posix() +GRO_empty_atom = (_data_ref / "empty_atom.gro").as_posix() +GRO_huge_box = (_data_ref / "huge_box.gro").as_posix() + +COORDINATES_GRO = (_data_ref / "coordinates/test.gro").as_posix() +COORDINATES_GRO_INCOMPLETE_VELOCITY = ( + _data_ref / "coordinates/test_incomplete_vel.gro" +).as_posix() +COORDINATES_GRO_BZ2 = (_data_ref / "coordinates/test.gro.bz2").as_posix() +COORDINATES_XYZ = (_data_ref / "coordinates/test.xyz").as_posix() +COORDINATES_XYZ_BZ2 = (_data_ref / "coordinates/test.xyz.bz2").as_posix() +COORDINATES_XTC = (_data_ref / "coordinates/test.xtc").as_posix() +COORDINATES_TRR = (_data_ref / "coordinates/test.trr").as_posix() +COORDINATES_TNG = (_data_ref / "coordinates/test.tng").as_posix() +COORDINATES_H5MD = (_data_ref / "coordinates/test.h5md").as_posix() +COORDINATES_DCD = (_data_ref / "coordinates/test.dcd").as_posix() +COORDINATES_TOPOLOGY = (_data_ref / "coordinates/test_topology.pdb").as_posix() + +PSF = (_data_ref / "adk.psf").as_posix() +PSF_notop = (_data_ref / "adk_notop.psf").as_posix() +PSF_BAD = (_data_ref / "adk_notop_BAD.psf").as_posix() +DCD = (_data_ref / "adk_dims.dcd").as_posix() +DCD_empty = (_data_ref / "empty.dcd").as_posix() +CRD = (_data_ref / "adk_open.crd").as_posix() +PSF_TRICLINIC = (_data_ref / "tip125_tric_C36.psf").as_posix() +DCD_TRICLINIC = (_data_ref / "tip125_tric_C36.dcd").as_posix() +DCD2 = (_data_ref / "adk_dims2.dcd").as_posix() + +PSF_NAMD = (_data_ref / "namd_cgenff.psf").as_posix() +PDB_NAMD = (_data_ref / "namd_cgenff.pdb").as_posix() +PDB_multipole = ( + _data_ref / "water_methane_acetic-acid_ammonia.pdb" +).as_posix() +PSF_NAMD_TRICLINIC = (_data_ref / "SiN_tric_namd.psf").as_posix() +DCD_NAMD_TRICLINIC = (_data_ref / "SiN_tric_namd.dcd").as_posix() +PSF_NAMD_GBIS = (_data_ref / "adk_closed_NAMD.psf").as_posix() +DCD_NAMD_GBIS = (_data_ref / "adk_gbis_tmd-fast1_NAMD.dcd").as_posix() + +PSF_nosegid = (_data_ref / "nosegid.psf").as_posix() + +PSF_cmap = (_data_ref / "parmed_ala3.psf").as_posix() + +PSF_inscode = (_data_ref / "1a2c_ins_code.psf").as_posix() + +PDB_varying = (_data_ref / "varying_occ_tmp.pdb").as_posix() +PDB_small = (_data_ref / "adk_open.pdb").as_posix() +PDB_closed = (_data_ref / "adk_closed.pdb").as_posix() + +ALIGN = (_data_ref / "align.pdb").as_posix() +RNA_PSF = (_data_ref / "analysis/1k5i_c36.psf.gz").as_posix() +RNA_PDB = (_data_ref / "analysis/1k5i_c36.pdb.gz").as_posix() +INC_PDB = (_data_ref / "incomplete.pdb").as_posix() +PDB_cm = (_data_ref / "cryst_then_model.pdb").as_posix() +PDB_cm_gz = (_data_ref / "cryst_then_model.pdb.gz").as_posix() +PDB_cm_bz2 = (_data_ref / "cryst_then_model.pdb.bz2").as_posix() +PDB_mc = (_data_ref / "model_then_cryst.pdb").as_posix() +PDB_mc_gz = (_data_ref / "model_then_cryst.pdb.gz").as_posix() +PDB_mc_bz2 = (_data_ref / "model_then_cryst.pdb.bz2").as_posix() +PDB_chainidnewres = (_data_ref / "chainIDnewres.pdb.gz").as_posix() +PDB_sameresid_diffresname = ( + _data_ref / "sameresid_diffresname.pdb" +).as_posix() +PDB_chainidrepeat = (_data_ref / "chainIDrepeat.pdb.gz").as_posix() +PDB_multiframe = (_data_ref / "nmr_neopetrosiamide.pdb").as_posix() +PDB_helix = (_data_ref / "A6PA6_alpha.pdb").as_posix() +PDB_conect = (_data_ref / "conect_parsing.pdb").as_posix() +PDB_conect2TER = (_data_ref / "CONECT2TER.pdb").as_posix() +PDB_singleconect = (_data_ref / "SINGLECONECT.pdb").as_posix() +PDB_icodes = (_data_ref / "1osm.pdb.gz").as_posix() +PDB_CRYOEM_BOX = (_data_ref / "5a7u.pdb").as_posix() +PDB_CHECK_RIGHTHAND_PA = (_data_ref / "6msm.pdb.bz2").as_posix() +FHIAIMS = (_data_ref / "fhiaims.in").as_posix() + +GRO = (_data_ref / "adk_oplsaa.gro").as_posix() +GRO_velocity = (_data_ref / "sample_velocity_file.gro").as_posix() +GRO_incomplete_vels = (_data_ref / "grovels.gro").as_posix() +GRO_large = (_data_ref / "bigbox.gro.bz2").as_posix() +GRO_residwrap = (_data_ref / "residwrap.gro").as_posix() +GRO_residwrap_0base = (_data_ref / "residwrap_0base.gro").as_posix() +GRO_sameresid_diffresname = ( + _data_ref / "sameresid_diffresname.gro" +).as_posix() +PDB = (_data_ref / "adk_oplsaa.pdb").as_posix() +XTC = (_data_ref / "adk_oplsaa.xtc").as_posix() +TRR = (_data_ref / "adk_oplsaa.trr").as_posix() +TPR = (_data_ref / "adk_oplsaa.tpr").as_posix() +PDB_sub_dry = (_data_ref / "cobrotoxin_dry_neutral_0.pdb").as_posix() +TRR_sub_sol = (_data_ref / "cobrotoxin.trr").as_posix() +XTC_sub_sol = (_data_ref / "cobrotoxin.xtc").as_posix() +PDB_sub_sol = (_data_ref / "cobrotoxin.pdb").as_posix() +PDB_xlserial = (_data_ref / "xl_serial.pdb").as_posix() +GRO_MEMPROT = (_data_ref / "analysis/YiiP_lipids.gro.gz").as_posix() +XTC_MEMPROT = (_data_ref / "analysis/YiiP_lipids.xtc").as_posix() +XTC_multi_frame = ( + _data_ref / "xtc_test_only_10_frame_10_atoms.xtc" +).as_posix() +TRR_multi_frame = ( + _data_ref / "trr_test_only_10_frame_10_atoms.trr" +).as_posix() +TNG_traj = (_data_ref / "argon_npt_compressed.tng").as_posix() +TNG_traj_gro = (_data_ref / "argon_npt_compressed.gro.gz").as_posix() +TNG_traj_uneven_blocks = ( + _data_ref / "argon_npt_compressed_uneven.tng" +).as_posix() +TNG_traj_vels_forces = ( + _data_ref / "argon_npt_compressed_vels_forces.tng" +).as_posix() +PDB_xvf = (_data_ref / "cobrotoxin.pdb").as_posix() +TPR_xvf = (_data_ref / "cobrotoxin.tpr").as_posix() +TRR_xvf = (_data_ref / "cobrotoxin.trr").as_posix() +H5MD_xvf = (_data_ref / "cobrotoxin.h5md").as_posix() +H5MD_energy = (_data_ref / "cu.h5md").as_posix() +H5MD_malformed = (_data_ref / "cu_malformed.h5md").as_posix() +XVG_BZ2 = (_data_ref / "cobrotoxin_protein_forces.xvg.bz2").as_posix() + +XPDB_small = (_data_ref / "5digitResid.pdb").as_posix() # number is the gromacs version -TPR400 = (_data_ref / 'tprs/2lyz_gmx_4.0.tpr').as_posix() -TPR402 = (_data_ref / 'tprs/2lyz_gmx_4.0.2.tpr').as_posix() -TPR403 = (_data_ref / 'tprs/2lyz_gmx_4.0.3.tpr').as_posix() -TPR404 = (_data_ref / 'tprs/2lyz_gmx_4.0.4.tpr').as_posix() -TPR405 = (_data_ref / 'tprs/2lyz_gmx_4.0.5.tpr').as_posix() -TPR406 = (_data_ref / 'tprs/2lyz_gmx_4.0.6.tpr').as_posix() -TPR407 = (_data_ref / 'tprs/2lyz_gmx_4.0.7.tpr').as_posix() -TPR450 = (_data_ref / 'tprs/2lyz_gmx_4.5.tpr').as_posix() -TPR451 = (_data_ref / 'tprs/2lyz_gmx_4.5.1.tpr').as_posix() -TPR452 = (_data_ref / 'tprs/2lyz_gmx_4.5.2.tpr').as_posix() -TPR453 = (_data_ref / 'tprs/2lyz_gmx_4.5.3.tpr').as_posix() -TPR454 = (_data_ref / 'tprs/2lyz_gmx_4.5.4.tpr').as_posix() -TPR455 = (_data_ref / 'tprs/2lyz_gmx_4.5.5.tpr').as_posix() -TPR502 = (_data_ref / 'tprs/2lyz_gmx_5.0.2.tpr').as_posix() -TPR504 = (_data_ref / 'tprs/2lyz_gmx_5.0.4.tpr').as_posix() -TPR505 = (_data_ref / 'tprs/2lyz_gmx_5.0.5.tpr').as_posix() -TPR510 = (_data_ref / 'tprs/2lyz_gmx_5.1.tpr').as_posix() -TPR2016 = (_data_ref / 'tprs/2lyz_gmx_2016.tpr').as_posix() -TPR2018 = (_data_ref / 'tprs/2lyz_gmx_2018.tpr').as_posix() -TPR2019B3 = (_data_ref / 'tprs/2lyz_gmx_2019-beta3.tpr').as_posix() -TPR2020B2 = (_data_ref / 'tprs/2lyz_gmx_2020-beta2.tpr').as_posix() -TPR2020 = (_data_ref / 'tprs/2lyz_gmx_2020.tpr').as_posix() -TPR2021 = (_data_ref / 'tprs/2lyz_gmx_2021.tpr').as_posix() -TPR2022RC1 = (_data_ref / 'tprs/2lyz_gmx_2022-rc1.tpr').as_posix() -TPR2023 = (_data_ref / 'tprs/2lyz_gmx_2023.tpr').as_posix() -TPR2024 = (_data_ref / 'tprs/2lyz_gmx_2024.tpr').as_posix() -TPR2024_4 = (_data_ref / 'tprs/2lyz_gmx_2024_4.tpr').as_posix() +TPR400 = (_data_ref / "tprs/2lyz_gmx_4.0.tpr").as_posix() +TPR402 = (_data_ref / "tprs/2lyz_gmx_4.0.2.tpr").as_posix() +TPR403 = (_data_ref / "tprs/2lyz_gmx_4.0.3.tpr").as_posix() +TPR404 = (_data_ref / "tprs/2lyz_gmx_4.0.4.tpr").as_posix() +TPR405 = (_data_ref / "tprs/2lyz_gmx_4.0.5.tpr").as_posix() +TPR406 = (_data_ref / "tprs/2lyz_gmx_4.0.6.tpr").as_posix() +TPR407 = (_data_ref / "tprs/2lyz_gmx_4.0.7.tpr").as_posix() +TPR450 = (_data_ref / "tprs/2lyz_gmx_4.5.tpr").as_posix() +TPR451 = (_data_ref / "tprs/2lyz_gmx_4.5.1.tpr").as_posix() +TPR452 = (_data_ref / "tprs/2lyz_gmx_4.5.2.tpr").as_posix() +TPR453 = (_data_ref / "tprs/2lyz_gmx_4.5.3.tpr").as_posix() +TPR454 = (_data_ref / "tprs/2lyz_gmx_4.5.4.tpr").as_posix() +TPR455 = (_data_ref / "tprs/2lyz_gmx_4.5.5.tpr").as_posix() +TPR502 = (_data_ref / "tprs/2lyz_gmx_5.0.2.tpr").as_posix() +TPR504 = (_data_ref / "tprs/2lyz_gmx_5.0.4.tpr").as_posix() +TPR505 = (_data_ref / "tprs/2lyz_gmx_5.0.5.tpr").as_posix() +TPR510 = (_data_ref / "tprs/2lyz_gmx_5.1.tpr").as_posix() +TPR2016 = (_data_ref / "tprs/2lyz_gmx_2016.tpr").as_posix() +TPR2018 = (_data_ref / "tprs/2lyz_gmx_2018.tpr").as_posix() +TPR2019B3 = (_data_ref / "tprs/2lyz_gmx_2019-beta3.tpr").as_posix() +TPR2020B2 = (_data_ref / "tprs/2lyz_gmx_2020-beta2.tpr").as_posix() +TPR2020 = (_data_ref / "tprs/2lyz_gmx_2020.tpr").as_posix() +TPR2021 = (_data_ref / "tprs/2lyz_gmx_2021.tpr").as_posix() +TPR2022RC1 = (_data_ref / "tprs/2lyz_gmx_2022-rc1.tpr").as_posix() +TPR2023 = (_data_ref / "tprs/2lyz_gmx_2023.tpr").as_posix() +TPR2024 = (_data_ref / "tprs/2lyz_gmx_2024.tpr").as_posix() +TPR2024_4 = (_data_ref / "tprs/2lyz_gmx_2024_4.tpr").as_posix() # double precision -TPR455Double = (_data_ref / 'tprs/drew_gmx_4.5.5.double.tpr').as_posix() -TPR460 = (_data_ref / 'tprs/ab42_gmx_4.6.tpr').as_posix() -TPR461 = (_data_ref / 'tprs/ab42_gmx_4.6.1.tpr').as_posix() -TPR2020Double = (_data_ref / 'tprs/2lyz_gmx_2020_double.tpr').as_posix() -TPR2021Double = (_data_ref / 'tprs/2lyz_gmx_2021_double.tpr').as_posix() +TPR455Double = (_data_ref / "tprs/drew_gmx_4.5.5.double.tpr").as_posix() +TPR460 = (_data_ref / "tprs/ab42_gmx_4.6.tpr").as_posix() +TPR461 = (_data_ref / "tprs/ab42_gmx_4.6.1.tpr").as_posix() +TPR2020Double = (_data_ref / "tprs/2lyz_gmx_2020_double.tpr").as_posix() +TPR2021Double = (_data_ref / "tprs/2lyz_gmx_2021_double.tpr").as_posix() # all bonded interactions -TPR334_bonded = (_data_ref / 'tprs/all_bonded/dummy_3.3.4.tpr').as_posix() -TPR510_bonded = (_data_ref / 'tprs/all_bonded/dummy_5.1.tpr').as_posix() -TPR2016_bonded = (_data_ref / 'tprs/all_bonded/dummy_2016.tpr').as_posix() -TPR2018_bonded = (_data_ref / 'tprs/all_bonded/dummy_2018.tpr').as_posix() -TPR2019B3_bonded = (_data_ref / 'tprs/all_bonded/dummy_2019-beta3.tpr').as_posix() -TPR2020B2_bonded = (_data_ref / 'tprs/all_bonded/dummy_2020-beta2.tpr').as_posix() -TPR2020_bonded = (_data_ref / 'tprs/all_bonded/dummy_2020.tpr').as_posix() -TPR2020_double_bonded = (_data_ref / 'tprs/all_bonded/dummy_2020_double.tpr').as_posix() -TPR2021_bonded = (_data_ref / 'tprs/all_bonded/dummy_2021.tpr').as_posix() -TPR2021_double_bonded = (_data_ref / 'tprs/all_bonded/dummy_2021_double.tpr').as_posix() -TPR2022RC1_bonded = (_data_ref / 'tprs/all_bonded/dummy_2022-rc1.tpr').as_posix() -TPR2023_bonded = (_data_ref / 'tprs/all_bonded/dummy_2023.tpr').as_posix() -TPR2024_bonded = (_data_ref / 'tprs/all_bonded/dummy_2024.tpr').as_posix() -TPR2024_4_bonded = (_data_ref / 'tprs/all_bonded/dummy_2024_4.tpr').as_posix() +TPR334_bonded = (_data_ref / "tprs/all_bonded/dummy_3.3.4.tpr").as_posix() +TPR510_bonded = (_data_ref / "tprs/all_bonded/dummy_5.1.tpr").as_posix() +TPR2016_bonded = (_data_ref / "tprs/all_bonded/dummy_2016.tpr").as_posix() +TPR2018_bonded = (_data_ref / "tprs/all_bonded/dummy_2018.tpr").as_posix() +TPR2019B3_bonded = ( + _data_ref / "tprs/all_bonded/dummy_2019-beta3.tpr" +).as_posix() +TPR2020B2_bonded = ( + _data_ref / "tprs/all_bonded/dummy_2020-beta2.tpr" +).as_posix() +TPR2020_bonded = (_data_ref / "tprs/all_bonded/dummy_2020.tpr").as_posix() +TPR2020_double_bonded = ( + _data_ref / "tprs/all_bonded/dummy_2020_double.tpr" +).as_posix() +TPR2021_bonded = (_data_ref / "tprs/all_bonded/dummy_2021.tpr").as_posix() +TPR2021_double_bonded = ( + _data_ref / "tprs/all_bonded/dummy_2021_double.tpr" +).as_posix() +TPR2022RC1_bonded = ( + _data_ref / "tprs/all_bonded/dummy_2022-rc1.tpr" +).as_posix() +TPR2023_bonded = (_data_ref / "tprs/all_bonded/dummy_2023.tpr").as_posix() +TPR2024_bonded = (_data_ref / "tprs/all_bonded/dummy_2024.tpr").as_posix() +TPR2024_4_bonded = (_data_ref / "tprs/all_bonded/dummy_2024_4.tpr").as_posix() # all interactions -TPR_EXTRA_2024_4 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2024_4.tpr').as_posix() -TPR_EXTRA_2024 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2024.tpr').as_posix() -TPR_EXTRA_2023 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2023.tpr').as_posix() -TPR_EXTRA_2022RC1 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2022-rc1.tpr').as_posix() -TPR_EXTRA_2021 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2021.tpr').as_posix() -TPR_EXTRA_2020 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2020.tpr').as_posix() -TPR_EXTRA_2018 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2018.tpr').as_posix() -TPR_EXTRA_2016 = (_data_ref / 'tprs/virtual_sites/extra-interactions-2016.3.tpr').as_posix() -TPR_EXTRA_407 = (_data_ref / 'tprs/virtual_sites/extra-interactions-4.0.7.tpr').as_posix() - -XYZ_psf = (_data_ref / '2r9r-1b.psf').as_posix() -XYZ_bz2 = (_data_ref / '2r9r-1b.xyz.bz2').as_posix() -XYZ = (_data_ref / '2r9r-1b.xyz').as_posix() -XYZ_mini = (_data_ref / 'mini.xyz').as_posix() -XYZ_five = (_data_ref / 'five.xyz').as_posix() -TXYZ = (_data_ref / 'coordinates/test.txyz').as_posix() -ARC = (_data_ref / 'coordinates/test.arc').as_posix() -ARC_PBC = (_data_ref / 'coordinates/new_hexane.arc').as_posix() - -PRM = (_data_ref / 'Amber/ache.prmtop').as_posix() -TRJ = (_data_ref / 'Amber/ache.mdcrd').as_posix() -INPCRD = (_data_ref / 'Amber/test.inpcrd').as_posix() -TRJ_bz2 = (_data_ref / 'Amber/ache.mdcrd.bz2').as_posix() -PFncdf_Top = (_data_ref / 'Amber/posfor.top').as_posix() -PFncdf_Trj = (_data_ref / 'Amber/posfor.ncdf').as_posix() +TPR_EXTRA_2024_4 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2024_4.tpr" +).as_posix() +TPR_EXTRA_2024 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2024.tpr" +).as_posix() +TPR_EXTRA_2023 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2023.tpr" +).as_posix() +TPR_EXTRA_2022RC1 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2022-rc1.tpr" +).as_posix() +TPR_EXTRA_2021 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2021.tpr" +).as_posix() +TPR_EXTRA_2020 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2020.tpr" +).as_posix() +TPR_EXTRA_2018 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2018.tpr" +).as_posix() +TPR_EXTRA_2016 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-2016.3.tpr" +).as_posix() +TPR_EXTRA_407 = ( + _data_ref / "tprs/virtual_sites/extra-interactions-4.0.7.tpr" +).as_posix() + +XYZ_psf = (_data_ref / "2r9r-1b.psf").as_posix() +XYZ_bz2 = (_data_ref / "2r9r-1b.xyz.bz2").as_posix() +XYZ = (_data_ref / "2r9r-1b.xyz").as_posix() +XYZ_mini = (_data_ref / "mini.xyz").as_posix() +XYZ_five = (_data_ref / "five.xyz").as_posix() +TXYZ = (_data_ref / "coordinates/test.txyz").as_posix() +ARC = (_data_ref / "coordinates/test.arc").as_posix() +ARC_PBC = (_data_ref / "coordinates/new_hexane.arc").as_posix() + +PRM = (_data_ref / "Amber/ache.prmtop").as_posix() +TRJ = (_data_ref / "Amber/ache.mdcrd").as_posix() +INPCRD = (_data_ref / "Amber/test.inpcrd").as_posix() +TRJ_bz2 = (_data_ref / "Amber/ache.mdcrd.bz2").as_posix() +PFncdf_Top = (_data_ref / "Amber/posfor.top").as_posix() +PFncdf_Trj = (_data_ref / "Amber/posfor.ncdf").as_posix() PRM_chainid_bz2 = (_data_ref / "Amber/ache_chainid.prmtop.bz2").as_posix() -CPPTRAJ_TRAJ_TOP = (_data_ref / 'Amber/cpptraj_traj.prmtop').as_posix() -CPPTRAJ_TRAJ = (_data_ref / 'Amber/cpptraj_traj.nc').as_posix() +CPPTRAJ_TRAJ_TOP = (_data_ref / "Amber/cpptraj_traj.prmtop").as_posix() +CPPTRAJ_TRAJ = (_data_ref / "Amber/cpptraj_traj.nc").as_posix() -PRMpbc = (_data_ref / 'Amber/capped-ala.prmtop').as_posix() -TRJpbc_bz2 = (_data_ref / 'Amber/capped-ala.mdcrd.bz2').as_posix() +PRMpbc = (_data_ref / "Amber/capped-ala.prmtop").as_posix() +TRJpbc_bz2 = (_data_ref / "Amber/capped-ala.mdcrd.bz2").as_posix() -PRMncdf = (_data_ref / 'Amber/bala.prmtop').as_posix() -TRJncdf = (_data_ref / 'Amber/bala.trj').as_posix() -NCDF = (_data_ref / 'Amber/bala.ncdf').as_posix() +PRMncdf = (_data_ref / "Amber/bala.prmtop").as_posix() +TRJncdf = (_data_ref / "Amber/bala.trj").as_posix() +NCDF = (_data_ref / "Amber/bala.ncdf").as_posix() -PRM12 = (_data_ref / 'Amber/anti.top').as_posix() -TRJ12_bz2 = (_data_ref / 'Amber/anti_md1.mdcrd.bz2').as_posix() +PRM12 = (_data_ref / "Amber/anti.top").as_posix() +TRJ12_bz2 = (_data_ref / "Amber/anti_md1.mdcrd.bz2").as_posix() -PRM7 = (_data_ref / 'Amber/tz2.truncoct.parm7.bz2').as_posix() -NCDFtruncoct = (_data_ref / 'Amber/tz2.truncoct.nc').as_posix() +PRM7 = (_data_ref / "Amber/tz2.truncoct.parm7.bz2").as_posix() +NCDFtruncoct = (_data_ref / "Amber/tz2.truncoct.nc").as_posix() -PRMcs = (_data_ref / 'Amber/chitosan.prmtop').as_posix() +PRMcs = (_data_ref / "Amber/chitosan.prmtop").as_posix() -PRMNCRST = (_data_ref / 'Amber/ace_mbondi3.parm7').as_posix() +PRMNCRST = (_data_ref / "Amber/ace_mbondi3.parm7").as_posix() -PRM_NCBOX = (_data_ref / 'Amber/ace_tip3p.parm7').as_posix() -TRJ_NCBOX = (_data_ref / 'Amber/ace_tip3p.nc').as_posix() +PRM_NCBOX = (_data_ref / "Amber/ace_tip3p.parm7").as_posix() +TRJ_NCBOX = (_data_ref / "Amber/ace_tip3p.nc").as_posix() -PRMNEGATIVE = (_data_ref / 'Amber/ace_mbondi3.negative.parm7').as_posix() +PRMNEGATIVE = (_data_ref / "Amber/ace_mbondi3.negative.parm7").as_posix() PRMErr1 = (_data_ref / "Amber/ace_mbondi3.error1.parm7").as_posix() PRMErr2 = (_data_ref / "Amber/ace_mbondi3.error2.parm7").as_posix() @@ -490,37 +659,41 @@ PRMErr4 = (_data_ref / "Amber/ace_mbondi3.error4.parm7").as_posix() PRMErr5 = (_data_ref / "Amber/ache_chainid.error5.prmtop.bz2").as_posix() -PRM_UreyBradley = (_data_ref / 'Amber/parmed_fad.prmtop').as_posix() -PRM7_ala2 = (_data_ref / 'Amber/parmed_ala2_solv.parm7').as_posix() -RST7_ala2 = (_data_ref / 'Amber/parmed_ala2_solv.rst7').as_posix() +PRM_UreyBradley = (_data_ref / "Amber/parmed_fad.prmtop").as_posix() +PRM7_ala2 = (_data_ref / "Amber/parmed_ala2_solv.parm7").as_posix() +RST7_ala2 = (_data_ref / "Amber/parmed_ala2_solv.rst7").as_posix() -PRM19SBOPC = (_data_ref / 'Amber/ala.ff19SB.OPC.parm7.bz2').as_posix() +PRM19SBOPC = (_data_ref / "Amber/ala.ff19SB.OPC.parm7.bz2").as_posix() -PQR = (_data_ref / 'adk_open.pqr').as_posix() -PQR_icodes = (_data_ref / '1A2C.pqr').as_posix() +PQR = (_data_ref / "adk_open.pqr").as_posix() +PQR_icodes = (_data_ref / "1A2C.pqr").as_posix() PDBQT_input = (_data_ref / "pdbqt_inputpdbqt.pdbqt").as_posix() PDBQT_querypdb = (_data_ref / "pdbqt_querypdb.pdb").as_posix() PDBQT_tyrosol = (_data_ref / "tyrosol.pdbqt.bz2").as_posix() -FASTA = (_data_ref / 'test.fasta').as_posix() -HELANAL_BENDING_MATRIX = (_data_ref / 'helanal_bending_matrix_AdK_DIMS_H8.dat').as_posix() -HELANAL_BENDING_MATRIX_SUBSET = (_data_ref / 'helanal_bending_matrix_AdK_DIMS_H8_frames10to79.dat').as_posix() +FASTA = (_data_ref / "test.fasta").as_posix() +HELANAL_BENDING_MATRIX = ( + _data_ref / "helanal_bending_matrix_AdK_DIMS_H8.dat" +).as_posix() +HELANAL_BENDING_MATRIX_SUBSET = ( + _data_ref / "helanal_bending_matrix_AdK_DIMS_H8_frames10to79.dat" +).as_posix() -PDB_HOLE = (_data_ref / '1grm_single.pdb').as_posix() -MULTIPDB_HOLE = (_data_ref / '1grm_elNemo_mode7.pdb.bz2').as_posix() +PDB_HOLE = (_data_ref / "1grm_single.pdb").as_posix() +MULTIPDB_HOLE = (_data_ref / "1grm_elNemo_mode7.pdb.bz2").as_posix() -DMS = (_data_ref / 'adk_closed.dms').as_posix() -DMS_DOMAINS = (_data_ref / 'adk_closed_domains.dms').as_posix() -DMS_NO_SEGID = (_data_ref / 'adk_closed_no_segid.dms').as_posix() +DMS = (_data_ref / "adk_closed.dms").as_posix() +DMS_DOMAINS = (_data_ref / "adk_closed_domains.dms").as_posix() +DMS_NO_SEGID = (_data_ref / "adk_closed_no_segid.dms").as_posix() -CONECT = (_data_ref / '1hvr.pdb').as_posix() -CONECT_ERROR = (_data_ref / 'conect_error.pdb').as_posix() +CONECT = (_data_ref / "1hvr.pdb").as_posix() +CONECT_ERROR = (_data_ref / "conect_error.pdb").as_posix() -TRZ = (_data_ref / 'trzfile.trz').as_posix() -TRZ_psf = (_data_ref / 'trz_psf.psf').as_posix() +TRZ = (_data_ref / "trzfile.trz").as_posix() +TRZ_psf = (_data_ref / "trz_psf.psf").as_posix() -TRIC = (_data_ref / 'dppc_vesicle_hg.gro').as_posix() +TRIC = (_data_ref / "dppc_vesicle_hg.gro").as_posix() PDB_full = (_data_ref / "4E43.pdb").as_posix() @@ -532,7 +705,9 @@ mol2_molecule = (_data_ref / "mol2/Molecule.mol2").as_posix() mol2_ligand = (_data_ref / "mol2/Ligand.mol2").as_posix() mol2_broken_molecule = (_data_ref / "mol2/BrokenMolecule.mol2").as_posix() -mol2_comments_header = (_data_ref / "mol2/Molecule_comments_header.mol2").as_posix() +mol2_comments_header = ( + _data_ref / "mol2/Molecule_comments_header.mol2" +).as_posix() # MOL2 file without substructure field mol2_zinc = (_data_ref / "mol2/zinc_856218.mol2").as_posix() # MOL2 file without bonds @@ -543,8 +718,12 @@ capping_ace = (_data_ref / "capping/ace.pdb").as_posix() capping_nma = (_data_ref / "capping/nma.pdb").as_posix() -contacts_villin_folded = (_data_ref / "contacts/villin_folded.gro.bz2").as_posix() -contacts_villin_unfolded = (_data_ref / "contacts/villin_unfolded.gro.bz2").as_posix() +contacts_villin_folded = ( + _data_ref / "contacts/villin_folded.gro.bz2" +).as_posix() +contacts_villin_unfolded = ( + _data_ref / "contacts/villin_unfolded.gro.bz2" +).as_posix() contacts_file = (_data_ref / "contacts/2F4K_qlist5_remap.dat").as_posix() trz4data = (_data_ref / "lammps/datatest.trz").as_posix() @@ -556,31 +735,43 @@ LAMMPScnt2 = (_data_ref / "lammps/cnt-hexagonal-class1.data2").as_posix() LAMMPShyd = (_data_ref / "lammps/hydrogen-class1.data").as_posix() LAMMPShyd2 = (_data_ref / "lammps/hydrogen-class1.data2").as_posix() -LAMMPSdata_deletedatoms = (_data_ref / 'lammps/deletedatoms.data').as_posix() +LAMMPSdata_deletedatoms = (_data_ref / "lammps/deletedatoms.data").as_posix() LAMMPSdata_triclinic = (_data_ref / "lammps/albite_triclinic.data").as_posix() LAMMPSdata_PairIJ = (_data_ref / "lammps/pairij_coeffs.data.bz2").as_posix() LAMMPSDUMP = (_data_ref / "lammps/wat.lammpstrj.bz2").as_posix() LAMMPSDUMP_long = (_data_ref / "lammps/wat.lammpstrj_long.bz2").as_posix() -LAMMPSDUMP_allcoords = (_data_ref / "lammps/spce_all_coords.lammpstrj.bz2").as_posix() -LAMMPSDUMP_nocoords = (_data_ref / "lammps/spce_no_coords.lammpstrj.bz2").as_posix() +LAMMPSDUMP_allcoords = ( + _data_ref / "lammps/spce_all_coords.lammpstrj.bz2" +).as_posix() +LAMMPSDUMP_nocoords = ( + _data_ref / "lammps/spce_no_coords.lammpstrj.bz2" +).as_posix() LAMMPSDUMP_triclinic = (_data_ref / "lammps/albite_triclinic.dump").as_posix() LAMMPSDUMP_image_vf = (_data_ref / "lammps/image_vf.lammpstrj").as_posix() LAMMPS_image_vf = (_data_ref / "lammps/image_vf.data").as_posix() LAMMPSDUMP_chain1 = (_data_ref / "lammps/chain_dump_1.lammpstrj").as_posix() LAMMPSDUMP_chain2 = (_data_ref / "lammps/chain_dump_2.lammpstrj").as_posix() LAMMPS_chain = (_data_ref / "lammps/chain_initial.data").as_posix() -LAMMPSdata_many_bonds = (_data_ref / "lammps/a_lot_of_bond_types.data").as_posix() -LAMMPSdata_additional_columns = (_data_ref / "lammps/additional_columns.data").as_posix() -LAMMPSDUMP_additional_columns = (_data_ref / "lammps/additional_columns.lammpstrj").as_posix() +LAMMPSdata_many_bonds = ( + _data_ref / "lammps/a_lot_of_bond_types.data" +).as_posix() +LAMMPSdata_additional_columns = ( + _data_ref / "lammps/additional_columns.data" +).as_posix() +LAMMPSDUMP_additional_columns = ( + _data_ref / "lammps/additional_columns.lammpstrj" +).as_posix() unordered_res = (_data_ref / "unordered_res.pdb").as_posix() -GMS_ASYMOPT = (_data_ref / "gms/c1opt.gms.gz").as_posix() -GMS_SYMOPT = (_data_ref / "gms/symopt.gms").as_posix() -GMS_ASYMSURF = (_data_ref / "gms/surf2wat.gms").as_posix() +GMS_ASYMOPT = (_data_ref / "gms/c1opt.gms.gz").as_posix() +GMS_SYMOPT = (_data_ref / "gms/symopt.gms").as_posix() +GMS_ASYMSURF = (_data_ref / "gms/surf2wat.gms").as_posix() two_water_gro = (_data_ref / "two_water_gro.gro").as_posix() -two_water_gro_multiframe = (_data_ref / "two_water_gro_multiframe.gro").as_posix() +two_water_gro_multiframe = ( + _data_ref / "two_water_gro_multiframe.gro" +).as_posix() two_water_gro_nonames = (_data_ref / "two_water_gro_nonames.gro").as_posix() two_water_gro_widebox = (_data_ref / "two_water_gro_widebox.gro").as_posix() @@ -590,91 +781,103 @@ DLP_HISTORY = (_data_ref / "dlpoly/HISTORY").as_posix() DLP_HISTORY_order = (_data_ref / "dlpoly/HISTORY_order").as_posix() DLP_HISTORY_minimal = (_data_ref / "dlpoly/HISTORY_minimal").as_posix() -DLP_HISTORY_minimal_cell = (_data_ref / "dlpoly/HISTORY_minimal_cell").as_posix() +DLP_HISTORY_minimal_cell = ( + _data_ref / "dlpoly/HISTORY_minimal_cell" +).as_posix() DLP_HISTORY_classic = (_data_ref / "dlpoly/HISTORY_classic").as_posix() -waterPSF = (_data_ref / 'watdyn.psf').as_posix() -waterDCD = (_data_ref / 'watdyn.dcd').as_posix() +waterPSF = (_data_ref / "watdyn.psf").as_posix() +waterDCD = (_data_ref / "watdyn.dcd").as_posix() -rmsfArray = (_data_ref / 'adk_oplsaa_CA_rmsf.npy').as_posix() +rmsfArray = (_data_ref / "adk_oplsaa_CA_rmsf.npy").as_posix() -HoomdXMLdata = (_data_ref / 'C12x64.xml.bz2').as_posix() +HoomdXMLdata = (_data_ref / "C12x64.xml.bz2").as_posix() -Make_Whole = (_data_ref / 'make_whole.gro').as_posix() -fullerene = (_data_ref / 'fullerene.pdb.gz').as_posix() +Make_Whole = (_data_ref / "make_whole.gro").as_posix() +fullerene = (_data_ref / "fullerene.pdb.gz").as_posix() -Plength = (_data_ref / 'plength.gro').as_posix() -Martini_membrane_gro = (_data_ref / 'martini_dppc_chol_bilayer.gro').as_posix() +Plength = (_data_ref / "plength.gro").as_posix() +Martini_membrane_gro = (_data_ref / "martini_dppc_chol_bilayer.gro").as_posix() # Contains one of each residue in 'nucleic' selections -NUCLsel = (_data_ref / 'nucl_res.pdb').as_posix() +NUCLsel = (_data_ref / "nucl_res.pdb").as_posix() -RANDOM_WALK = (_data_ref / 'xyz_random_walk.xtc').as_posix() -RANDOM_WALK_TOPO = (_data_ref / 'RANDOM_WALK_TOPO.pdb').as_posix() +RANDOM_WALK = (_data_ref / "xyz_random_walk.xtc").as_posix() +RANDOM_WALK_TOPO = (_data_ref / "RANDOM_WALK_TOPO.pdb").as_posix() -MMTF = (_data_ref / '173D.mmtf').as_posix() -MMTF_gz = (_data_ref / '5KIH.mmtf.gz').as_posix() -MMTF_skinny = (_data_ref / '1ubq-less-optional.mmtf').as_posix() -MMTF_skinny2 = (_data_ref / '3NJW-onlyrequired.mmtf').as_posix() +MMTF = (_data_ref / "173D.mmtf").as_posix() +MMTF_gz = (_data_ref / "5KIH.mmtf.gz").as_posix() +MMTF_skinny = (_data_ref / "1ubq-less-optional.mmtf").as_posix() +MMTF_skinny2 = (_data_ref / "3NJW-onlyrequired.mmtf").as_posix() MMTF_NOCRYST = (_data_ref / "6QYR.mmtf.gz").as_posix() -ALIGN_BOUND = (_data_ref / 'analysis/align_bound.pdb.gz').as_posix() -ALIGN_UNBOUND = (_data_ref / 'analysis/align_unbound.pdb.gz').as_posix() - -GSD = (_data_ref / 'example.gsd').as_posix() -GSD_bonds = (_data_ref / 'example_bonds.gsd').as_posix() -GSD_long = (_data_ref / 'example_longer.gsd').as_posix() - -TRC_PDB_VAC = (_data_ref / 'gromos11/gromos11_traj_vac.pdb.gz').as_posix() -TRC_TRAJ1_VAC = (_data_ref / 'gromos11/gromos11_traj_vac_1.trc.gz').as_posix() -TRC_TRAJ2_VAC = (_data_ref / 'gromos11/gromos11_traj_vac_2.trc.gz').as_posix() -TRC_PDB_SOLV = (_data_ref / 'gromos11/gromos11_traj_solv.pdb.gz').as_posix() -TRC_TRAJ_SOLV = (_data_ref / 'gromos11/gromos11_traj_solv.trc.gz').as_posix() -TRC_CLUSTER_VAC = (_data_ref / 'gromos11/gromos11_cluster_vac.trj.gz').as_posix() -TRC_TRICLINIC_SOLV = (_data_ref / 'gromos11/gromos11_triclinic_solv.trc.gz').as_posix() -TRC_TRUNCOCT_VAC = (_data_ref / 'gromos11/gromos11_truncOcta_vac.trc.gz').as_posix() -TRC_GENBOX_ORIGIN = (_data_ref / 'gromos11/gromos11_genbox_origin.trc.gz').as_posix() -TRC_GENBOX_EULER = (_data_ref / 'gromos11/gromos11_genbox_euler.trc.gz').as_posix() -TRC_EMPTY = (_data_ref / 'gromos11/gromos11_empty.trc').as_posix() - -DihedralArray = (_data_ref / 'adk_oplsaa_dihedral.npy').as_posix() -DihedralsArray = (_data_ref / 'adk_oplsaa_dihedral_list.npy').as_posix() -RamaArray = (_data_ref / 'adk_oplsaa_rama.npy').as_posix() -GLYRamaArray = (_data_ref / 'adk_oplsaa_GLY_rama.npy').as_posix() -JaninArray = (_data_ref / 'adk_oplsaa_janin.npy').as_posix() -LYSJaninArray = (_data_ref / 'adk_oplsaa_LYS_janin.npy').as_posix() -PDB_rama = (_data_ref / '19hc.pdb.gz').as_posix() -PDB_janin = (_data_ref / '1a28.pdb.gz').as_posix() - -BATArray = (_data_ref / 'mol2_comments_header_bat.npy').as_posix() - -ITP = (_data_ref / 'gromacs_ala10.itp').as_posix() -ITP_nomass = (_data_ref / 'itp_nomass.itp').as_posix() -ITP_atomtypes = (_data_ref / 'atomtypes.itp').as_posix() -ITP_charges = (_data_ref / 'atomtypes_charge.itp').as_posix() -ITP_edited = (_data_ref / 'edited_itp.itp').as_posix() +ALIGN_BOUND = (_data_ref / "analysis/align_bound.pdb.gz").as_posix() +ALIGN_UNBOUND = (_data_ref / "analysis/align_unbound.pdb.gz").as_posix() + +GSD = (_data_ref / "example.gsd").as_posix() +GSD_bonds = (_data_ref / "example_bonds.gsd").as_posix() +GSD_long = (_data_ref / "example_longer.gsd").as_posix() + +TRC_PDB_VAC = (_data_ref / "gromos11/gromos11_traj_vac.pdb.gz").as_posix() +TRC_TRAJ1_VAC = (_data_ref / "gromos11/gromos11_traj_vac_1.trc.gz").as_posix() +TRC_TRAJ2_VAC = (_data_ref / "gromos11/gromos11_traj_vac_2.trc.gz").as_posix() +TRC_PDB_SOLV = (_data_ref / "gromos11/gromos11_traj_solv.pdb.gz").as_posix() +TRC_TRAJ_SOLV = (_data_ref / "gromos11/gromos11_traj_solv.trc.gz").as_posix() +TRC_CLUSTER_VAC = ( + _data_ref / "gromos11/gromos11_cluster_vac.trj.gz" +).as_posix() +TRC_TRICLINIC_SOLV = ( + _data_ref / "gromos11/gromos11_triclinic_solv.trc.gz" +).as_posix() +TRC_TRUNCOCT_VAC = ( + _data_ref / "gromos11/gromos11_truncOcta_vac.trc.gz" +).as_posix() +TRC_GENBOX_ORIGIN = ( + _data_ref / "gromos11/gromos11_genbox_origin.trc.gz" +).as_posix() +TRC_GENBOX_EULER = ( + _data_ref / "gromos11/gromos11_genbox_euler.trc.gz" +).as_posix() +TRC_EMPTY = (_data_ref / "gromos11/gromos11_empty.trc").as_posix() + +DihedralArray = (_data_ref / "adk_oplsaa_dihedral.npy").as_posix() +DihedralsArray = (_data_ref / "adk_oplsaa_dihedral_list.npy").as_posix() +RamaArray = (_data_ref / "adk_oplsaa_rama.npy").as_posix() +GLYRamaArray = (_data_ref / "adk_oplsaa_GLY_rama.npy").as_posix() +JaninArray = (_data_ref / "adk_oplsaa_janin.npy").as_posix() +LYSJaninArray = (_data_ref / "adk_oplsaa_LYS_janin.npy").as_posix() +PDB_rama = (_data_ref / "19hc.pdb.gz").as_posix() +PDB_janin = (_data_ref / "1a28.pdb.gz").as_posix() + +BATArray = (_data_ref / "mol2_comments_header_bat.npy").as_posix() + +ITP = (_data_ref / "gromacs_ala10.itp").as_posix() +ITP_nomass = (_data_ref / "itp_nomass.itp").as_posix() +ITP_atomtypes = (_data_ref / "atomtypes.itp").as_posix() +ITP_charges = (_data_ref / "atomtypes_charge.itp").as_posix() +ITP_edited = (_data_ref / "edited_itp.itp").as_posix() ITP_tip5p = (_data_ref / "tip5p.itp").as_posix() -ITP_spce = (_data_ref / 'spce.itp').as_posix() +ITP_spce = (_data_ref / "spce.itp").as_posix() -GMX_TOP = (_data_ref / 'gromacs_ala10.top').as_posix() -GMX_DIR = (_data_ref / 'gromacs/').as_posix() -GMX_TOP_BAD = (_data_ref / 'bad_top.top').as_posix() -ITP_no_endif = (_data_ref / 'no_endif_spc.itp').as_posix() +GMX_TOP = (_data_ref / "gromacs_ala10.top").as_posix() +GMX_DIR = (_data_ref / "gromacs/").as_posix() +GMX_TOP_BAD = (_data_ref / "bad_top.top").as_posix() +ITP_no_endif = (_data_ref / "no_endif_spc.itp").as_posix() -NAMDBIN = (_data_ref / 'adk_open.coor').as_posix() +NAMDBIN = (_data_ref / "adk_open.coor").as_posix() -SDF_molecule = (_data_ref / 'molecule.sdf').as_posix() +SDF_molecule = (_data_ref / "molecule.sdf").as_posix() -PDB_elements = (_data_ref / 'elements.pdb').as_posix() -PDB_charges = (_data_ref / 'charges.pdb').as_posix() +PDB_elements = (_data_ref / "elements.pdb").as_posix() +PDB_charges = (_data_ref / "charges.pdb").as_posix() PDBX = (_data_ref / "4x8u.pdbx").as_posix() -SURFACE_PDB = (_data_ref / 'surface.pdb.bz2').as_posix() -SURFACE_TRR = (_data_ref / 'surface.trr').as_posix() +SURFACE_PDB = (_data_ref / "surface.pdb.bz2").as_posix() +SURFACE_TRR = (_data_ref / "surface.trr").as_posix() # DSSP testing: from https://github.com/ShintaroMinami/PyDSSP -DSSP = (_data_ref / 'dssp').as_posix() +DSSP = (_data_ref / "dssp").as_posix() # This should be the last line: clean up namespace del resources diff --git a/testsuite/MDAnalysisTests/dummy.py b/testsuite/MDAnalysisTests/dummy.py index fc4c77326b3..7bfa11fdb86 100644 --- a/testsuite/MDAnalysisTests/dummy.py +++ b/testsuite/MDAnalysisTests/dummy.py @@ -38,8 +38,9 @@ _RESIDUES_PER_SEG = _N_RESIDUES // _N_SEGMENTS -def make_Universe(extras=None, size=None, - trajectory=False, velocities=False, forces=False): +def make_Universe( + extras=None, size=None, trajectory=False, velocities=False, forces=False +): """Make a dummy reference Universe Allows the construction of arbitrary-sized Universes. Suitable for @@ -81,10 +82,10 @@ def make_Universe(extras=None, size=None, n_atoms=n_atoms, n_residues=n_residues, n_segments=n_segments, - atom_resindex=np.repeat( - np.arange(n_residues), n_atoms // n_residues), + atom_resindex=np.repeat(np.arange(n_residues), n_atoms // n_residues), residue_segindex=np.repeat( - np.arange(n_segments), n_residues // n_segments), + np.arange(n_segments), n_residues // n_segments + ), # trajectory things trajectory=trajectory, velocities=velocities, @@ -106,12 +107,12 @@ def make_Universe(extras=None, size=None, return u + def make_altLocs(size): """AltLocs cycling through A B C D E""" na, nr, ns = size - alts = itertools.cycle(('A', 'B', 'C', 'D', 'E')) - return np.array(['{}'.format(next(alts)) for _ in range(na)], - dtype=object) + alts = itertools.cycle(("A", "B", "C", "D", "E")) + return np.array(["{}".format(next(alts)) for _ in range(na)], dtype=object) def make_bfactors(size): @@ -130,57 +131,73 @@ def make_charges(size): charges = itertools.cycle([-1.5, -0.5, 0.0, 0.5, 1.5]) return np.array([next(charges) for _ in range(na)]) + def make_resnames(size): - """Creates residues named RsA RsB ... """ + """Creates residues named RsA RsB ...""" na, nr, ns = size - return np.array(['Rs{}'.format(string.ascii_uppercase[i]) - for i in range(nr)], dtype=object) + return np.array( + ["Rs{}".format(string.ascii_uppercase[i]) for i in range(nr)], + dtype=object, + ) + def make_segids(size): """Segids SegA -> SegY""" na, nr, ns = size - return np.array(['Seg{}'.format(string.ascii_uppercase[i]) - for i in range(ns)], dtype=object) + return np.array( + ["Seg{}".format(string.ascii_uppercase[i]) for i in range(ns)], + dtype=object, + ) + def make_types(size): """Atoms are given types TypeA -> TypeE on a loop""" na, nr, ns = size types = itertools.cycle(string.ascii_uppercase[:5]) - return np.array(['Type{}'.format(next(types)) for _ in range(na)], - dtype=object) + return np.array( + ["Type{}".format(next(types)) for _ in range(na)], dtype=object + ) + def make_names(size): """Atom names AAA -> ZZZ (all unique)""" na, nr, ns = size # produces, AAA, AAB, AAC, ABA etc names = itertools.product(*[string.ascii_uppercase] * 3) - return np.array(['{}'.format(''.join(next(names))) for _ in range(na)], - dtype=object) + return np.array( + ["{}".format("".join(next(names))) for _ in range(na)], dtype=object + ) + def make_occupancies(size): na, nr, ns = size return np.tile(np.array([1.0, 2, 3, 4, 5]), nr) + def make_radii(size): na, nr, ns = size return np.tile(np.array([1.0, 2, 3, 4, 5]), nr) + def make_serials(size): """Serials go from 10 to size+10""" na, nr, ns = size return np.arange(na) + 10 + def make_masses(size): """Atom masses (5.1, 4.2, 3.3, 1.5, 0.5) repeated""" na, nr, ns = size masses = itertools.cycle([5.1, 4.2, 3.3, 1.5, 0.5]) return np.array([next(masses) for _ in range(na)]) + def make_resnums(size): """Resnums 1 and upwards""" na, nr, ns = size return np.arange(nr, dtype=np.int64) + 1 + def make_resids(size): """Resids 1 and upwards""" na, nr, ns = size @@ -190,20 +207,20 @@ def make_resids(size): # Available extra TopologyAttrs to a dummy Universe _MENU = { # Atoms - 'altLocs': make_altLocs, - 'bfactors': make_bfactors, - 'charges': make_charges, - 'names': make_names, - 'occupancies': make_occupancies, - 'radii': make_radii, - 'serials': make_serials, - 'tempfactors': make_tempfactors, - 'types': make_types, - 'masses': make_masses, + "altLocs": make_altLocs, + "bfactors": make_bfactors, + "charges": make_charges, + "names": make_names, + "occupancies": make_occupancies, + "radii": make_radii, + "serials": make_serials, + "tempfactors": make_tempfactors, + "types": make_types, + "masses": make_masses, # Residues - 'resnames': make_resnames, - 'resnums': make_resnums, - 'resids': make_resids, + "resnames": make_resnames, + "resnums": make_resnums, + "resids": make_resids, # Segments - 'segids': make_segids, + "segids": make_segids, } diff --git a/testsuite/MDAnalysisTests/test_api.py b/testsuite/MDAnalysisTests/test_api.py index a3a476825cf..2819cd549a7 100644 --- a/testsuite/MDAnalysisTests/test_api.py +++ b/testsuite/MDAnalysisTests/test_api.py @@ -32,6 +32,7 @@ from numpy.testing import assert_equal import MDAnalysis as mda + mda_dirname = os.path.dirname(mda.__file__) @@ -61,19 +62,26 @@ def init_files(): for root, dirs, files in os.walk("."): if "__init__.py" in files: submodule = ".".join(PurePath(root).parts) - submodule = "."*(len(submodule) > 0) + submodule + submodule = "." * (len(submodule) > 0) + submodule yield submodule -@pytest.mark.parametrize('submodule', init_files()) +@pytest.mark.parametrize("submodule", init_files()) def test_all_import(submodule): module = importlib.import_module("MDAnalysis" + submodule) module_path = os.path.join(mda_dirname, *submodule.split(".")) if hasattr(module, "__all__"): - missing = [name for name in module.__all__ - if name not in module.__dict__.keys() - and name not in [os.path.splitext(f)[0] for - f in os.listdir(module_path)]] - assert_equal(missing, [], err_msg="{}".format(submodule) + - " has errors in __all__ list: " + - "missing = {}".format(missing)) + missing = [ + name + for name in module.__all__ + if name not in module.__dict__.keys() + and name + not in [os.path.splitext(f)[0] for f in os.listdir(module_path)] + ] + assert_equal( + missing, + [], + err_msg="{}".format(submodule) + + " has errors in __all__ list: " + + "missing = {}".format(missing), + ) diff --git a/testsuite/MDAnalysisTests/util.py b/testsuite/MDAnalysisTests/util.py index 88631bdcff7..afd8199e4f4 100644 --- a/testsuite/MDAnalysisTests/util.py +++ b/testsuite/MDAnalysisTests/util.py @@ -26,7 +26,8 @@ """ import builtins -builtins_name = 'builtins' + +builtins_name = "builtins" importer = builtins.__import__ from contextlib import contextmanager @@ -58,20 +59,26 @@ def try_and_do_something(): Shadows the builtin import method, sniffs import requests and blocks the designated package. """ + def blocker_wrapper(func): @wraps(func) def func_wrapper(*args, **kwargs): - with mock.patch('{}.__import__'.format(builtins_name), - wraps=importer) as mbi: + with mock.patch( + "{}.__import__".format(builtins_name), wraps=importer + ) as mbi: + def blocker(*args, **kwargs): if package in args[0]: raise ImportError("Blocked by block_import") else: # returning DEFAULT allows the real function to continue return mock.DEFAULT + mbi.side_effect = blocker func(*args, **kwargs) + return func_wrapper + return blocker_wrapper @@ -200,24 +207,28 @@ def assert_nowarns(warning_class, *args, **kwargs): return True else: # There was a warning even though we do not want to see one. - raise AssertionError("function {0} raises warning of class {1}".format( - func.__name__, warning_class.__name__)) + raise AssertionError( + "function {0} raises warning of class {1}".format( + func.__name__, warning_class.__name__ + ) + ) @contextmanager def no_warning(warning_class): """contextmanager to check that no warning was raised""" with warnings.catch_warnings(record=True) as record: - warnings.simplefilter('always') + warnings.simplefilter("always") yield if len(record) != 0: - raise AssertionError("Raised warning of class {}".format( - warning_class.__name__)) + raise AssertionError( + "Raised warning of class {}".format(warning_class.__name__) + ) class _NoDeprecatedCallContext(object): - # modified version of similar pytest class object that checks for - # raised DeprecationWarning + # modified version of similar pytest class object that checks for + # raised DeprecationWarning def __enter__(self): self._captured_categories = [] @@ -245,16 +256,24 @@ def __exit__(self, exc_type, exc_val, exc_tb): warnings.warn = self._old_warn if exc_type is None: - deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) - if any(issubclass(c, deprecation_categories) for c in self._captured_categories): + deprecation_categories = ( + DeprecationWarning, + PendingDeprecationWarning, + ) + if any( + issubclass(c, deprecation_categories) + for c in self._captured_categories + ): __tracebackhide__ = True - msg = "Produced DeprecationWarning or PendingDeprecationWarning" + msg = ( + "Produced DeprecationWarning or PendingDeprecationWarning" + ) raise AssertionError(msg) def no_deprecated_call(func=None, *args, **kwargs): - # modified version of similar pytest function - # check that DeprecationWarning is NOT raised + # modified version of similar pytest function + # check that DeprecationWarning is NOT raised if not func: return _NoDeprecatedCallContext() else: @@ -268,7 +287,7 @@ def get_userid(): Calls os.geteuid() where possible, or returns 1000 (usually on windows). """ # no such thing as euid on Windows, assuming normal user 1000 - if (os.name == 'nt' or not hasattr(os, "geteuid")): + if os.name == "nt" or not hasattr(os, "geteuid"): return 1000 else: return os.geteuid() diff --git a/testsuite/pyproject.toml b/testsuite/pyproject.toml index acac47ed0b9..c3529a7bd63 100644 --- a/testsuite/pyproject.toml +++ b/testsuite/pyproject.toml @@ -156,27 +156,6 @@ filterwarnings = [ [tool.black] line-length = 79 target-version = ['py310', 'py311', 'py312', 'py313'] -include = ''' -( -setup\.py -| MDAnalysisTests/auxiliary/.*\.py -| MDAnalysisTests/lib/.*\.py -| MDAnalysisTests/transformations/.*\.py -| MDAnalysisTests/topology/.*\.py -| MDAnalysisTests/analysis/.*\.py -| MDAnalysisTests/guesser/.*\.py -| MDAnalysisTests/converters/.*\.py -| MDAnalysisTests/coordinates/.*\.py -| MDAnalysisTests/data/.*\.py -| MDAnalysisTests/formats/.*\.py -| MDAnalysisTests/parallelism/.*\.py -| MDAnalysisTests/scripts/.*\.py -| MDAnalysisTests/core/.*\.py -| MDAnalysisTests/import/.*\.py -| MDAnalysisTests/utils/.*\.py -| MDAnalysisTests/visualization/.*\.py -) -''' extend-exclude = ''' ( __pycache__ diff --git a/testsuite/scripts/modeller_make_A6PA6_alpha.py b/testsuite/scripts/modeller_make_A6PA6_alpha.py index d67746826bc..9af0bff2189 100644 --- a/testsuite/scripts/modeller_make_A6PA6_alpha.py +++ b/testsuite/scripts/modeller_make_A6PA6_alpha.py @@ -14,17 +14,17 @@ # Set up environment e = environ() # use all-hydrogen topology: -e.libs.topology.read('${LIB}/top_allh.lib') -e.libs.parameters.read('${LIB}/par.lib') +e.libs.topology.read("${LIB}/top_allh.lib") +e.libs.parameters.read("${LIB}/par.lib") e.io.hydrogen = True # Build an extended chain model from primary sequence m = model(e) -m.build_sequence('AAAAAAPAAAAAA') +m.build_sequence("AAAAAAPAAAAAA") # Make stereochemical restraints on all atoms allatoms = selection(m) -m.restraints.make(allatoms, restraint_type='STEREO', spline_on_site=False) +m.restraints.make(allatoms, restraint_type="STEREO", spline_on_site=False) # Constrain all residues to be alpha-helical # (Could also use m.residue_range() rather than m.residues here.) @@ -33,4 +33,4 @@ # Get an optimized structure with CG, and write it out cg = conjugate_gradients() cg.optimize(allatoms, max_iterations=1000) -m.write(file='A6PA6_alpha.pdb') +m.write(file="A6PA6_alpha.pdb") From bdfb2c9f9a91e4769b0b8c76b7e8d382842f6714 Mon Sep 17 00:00:00 2001 From: Rocco Meli Date: Fri, 10 Jan 2025 08:02:43 +0100 Subject: [PATCH 58/58] Exclude automated formatting PRs from git blame (#4886) --- .git-blame-ignore-revs | 45 +++++++++++++++++++++++++-- maintainer/active_files_package.lst | 44 -------------------------- maintainer/active_files_testsuite.lst | 21 ------------- package/CHANGELOG | 5 +-- 4 files changed, 46 insertions(+), 69 deletions(-) delete mode 100644 maintainer/active_files_package.lst delete mode 100644 maintainer/active_files_testsuite.lst diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index d40a8a87bd9..5c1f33a305a 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,43 @@ -# black -83e5f99051d86ca354b537be8854c40f9b6ce172 +##### git-blame shows what revision and author last modified each line of a file +##### The ".git-blame-ignore-revs" file is used to ignore certain revisions from being +##### shown in the output of git blame (also on GitHub blame view). +# +# ~~~ Commits that only change the formatting of the code should be ignored. ~~~ +# +# --- Automated Formatting with `black` --- +# MDAnalysis was automatically formatted with `black` (end of 2024, beginning of 2025). +# Formatting PRs (should) contain "[fmt]" in the title (and so do the related commits). +# See https://github.com/MDAnalysis/mdanalysis/issues/2450 for more details. +# The following commits are squash commits that contain only^ formatting changes. +# ^ This is not strictly true. See PRs for the nitty gritty details. +#. PR numbers are listed just above the commit being ignored. +#4725 +571431a169e5881f7d09930c4d7e633df0f2cd40 +#4726 +46be788d84a6cb149d90e4493a726c3a30b3cca0 +#4802 +441e2c67abdb8a0a5ffac4a0bc5e88bc7c329aa8 +#4804 +25e755fd78e0a6fb71a91c9d3d989328f021f34b +#4809 +557f27d658ff0d4011bbe0efa03495f18aa2c1ce +#4848 +b710e57a64654bed5250eb771f0f27a2dddfeebf +#4850 +9110a6efe2765802856d028e650ebb0117d336bf +#4851 +a10e23e681023f383baac7582c39dce4180e2263 +#4856 +c08cb797fd1a2cb45bc0b9e4522cabb15d1f36bd +#4857 +29deccc9b43a09d2ec3b456bb9c70aae4c34c2cd +#4859 +9312fa67f163ec055a66f5756a182206fbea3130 +#4861 +55cce24003d4c0975b1bba387b9e6ac20638780e +#4874 +5eef34165b03281515e08a69159f6504e3a2ff8b +#4875 +b8fe34b73c9df9330c1608229b2f8cddc6e275b4 +#4885 +263bbe65047e535fbad981852ad9c7327826f93d diff --git a/maintainer/active_files_package.lst b/maintainer/active_files_package.lst deleted file mode 100644 index a1889549c1e..00000000000 --- a/maintainer/active_files_package.lst +++ /dev/null @@ -1,44 +0,0 @@ -package/MDAnalysis/core/selection\.py -| package/MDAnalysis/analysis/atomicdistances\.py -| package/MDAnalysis/topology/CMSParser\.py -| package/MDAnalysis/topology/__init__\.py -| package/MDAnalysis/coordinates/XDR\.py -| package/MDAnalysis/core/selection\.py -| package/MDAnalysis/analysis/diffusionmap\.py -| package/MDAnalysis/analysis/align\.py -| package/MDAnalysis/lib/_distopia\.py -| package/MDAnalysis/lib/distances\.py -| package/MDAnalysis/analysis/dasktimeseries\.py -| package/MDAnalysis/coordinates/H5MD\.py -| package/MDAnalysis/coordinates/MMCIF\.py -| package/MDAnalysis/coordinates/__init__\.py -| package/MDAnalysis/topology/MMCIFParser\.py -| package/MDAnalysis/topology/PDBParser\.py -| package/MDAnalysis/topology/__init__\.py -| package/MDAnalysis/topology/tpr/utils\.py -| package/MDAnalysis/coordinates/CIF\.py -| package/MDAnalysis/coordinates/PDBx\.py -| package/MDAnalysis/coordinates/__init__\.py -| package/MDAnalysis/topology/PDBxParser\.py -| package/MDAnalysis/topology/__init__\.py -| package/MDAnalysis/coordinates/base\.py -| package/MDAnalysis/core/universe\.py -| package/MDAnalysis/topology/base\.py -| package/MDAnalysis/analysis/hydrogenbonds/hbond_analysis\.py -| package/MDAnalysis/analysis/sasa\.py -| package/MDAnalysis/coordinates/DCD\.py -| package/MDAnalysis/coordinates/DLPoly\.py -| package/MDAnalysis/coordinates/GMS\.py -| package/MDAnalysis/coordinates/MOL2\.py -| package/MDAnalysis/coordinates/PDB\.py -| package/MDAnalysis/coordinates/TRJ\.py -| package/MDAnalysis/coordinates/TRR\.py -| package/MDAnalysis/coordinates/TRZ\.py -| package/MDAnalysis/coordinates/TXYZ\.py -| package/MDAnalysis/coordinates/XTC\.py -| package/MDAnalysis/coordinates/XYZ\.py -| package/MDAnalysis/coordinates/base\.py -| package/MDAnalysis/coordinates/chain\.py -| package/MDAnalysis/coordinates/chemfiles\.py -| package/MDAnalysis/coordinates/memory\.py -| package/MDAnalysis/core/universe\.py diff --git a/maintainer/active_files_testsuite.lst b/maintainer/active_files_testsuite.lst deleted file mode 100644 index 4898eafde48..00000000000 --- a/maintainer/active_files_testsuite.lst +++ /dev/null @@ -1,21 +0,0 @@ -testsuite/MDAnalysisTests/core/test_atomselections\.py -| testsuite/MDAnalysisTests/analysis/test_atomicdistances\.py -| testsuite/MDAnalysisTests/coordinates/test_xdr\.py -| testsuite/MDAnalysisTests/core/test_atomselections\.py -| testsuite/MDAnalysisTests/datafiles\.py -| testsuite/MDAnalysisTests/analysis/conftest\.py -| testsuite/MDAnalysisTests/analysis/test_diffusionmap\.py -| testsuite/MDAnalysisTests/analysis/conftest\.py -| testsuite/MDAnalysisTests/analysis/test_align\.py -| testsuite/MDAnalysisTests/lib/test_distances\.py -| testsuite/MDAnalysisTests/coordinates/test_mmcif\.py -| testsuite/MDAnalysisTests/datafiles\.py -| testsuite/MDAnalysisTests/topology/test_mmcif\.py -| testsuite/MDAnalysisTests/coordinates/test_cif\.py -| testsuite/MDAnalysisTests/analysis/test_encore\.py -| testsuite/MDAnalysisTests/parallelism/test_multiprocessing\.py -| testsuite/MDAnalysisTests/coordinates/base\.py -| testsuite/MDAnalysisTests/coordinates/test_gro\.py -| testsuite/MDAnalysisTests/topology/base\.py -| testsuite/MDAnalysisTests/analysis/test_hydrogenbonds_analysis\.py -| testsuite/MDAnalysisTests/analysis/test_sasa\.py diff --git a/package/CHANGELOG b/package/CHANGELOG index 5d8328f3313..32a7374ff6b 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,8 +14,8 @@ The rules for this file: ------------------------------------------------------------------------------- -??/??/?? IAlibay, ChiahsinChu, RMeli, tanishy7777, talagayev, - tylerjereddy +??/??/?? IAlibay, ChiahsinChu, RMeli, tanishy7777, talagayev, tylerjereddy, + marinegor * 2.9.0 @@ -36,6 +36,7 @@ Enhancements Changes + * Codebase is now formatted with black (version `24`) (PR #4886) Deprecations