From bed3523d611da995c9f552e3f7f2ba00ee93d63e Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Tue, 18 Feb 2020 19:23:57 -0800 Subject: [PATCH 01/10] fixed reST in AnalysisBase (need two lines before ..versionchanged) --- package/MDAnalysis/analysis/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index eec11b8e5f6..09a85949c47 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -100,6 +100,7 @@ def __init__(self, trajectory, verbose=False, **kwargs): verbose : bool, optional Turn on more logging and debugging, default ``False`` + .. versionchanged:: 1.0.0 Support for setting ``start``, ``stop``, and ``step`` has been removed. These should now be directly passed to From c04d2118c9ec0df53aad1192b1c81994419db0c4 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Mon, 17 Feb 2020 17:29:08 -0800 Subject: [PATCH 02/10] deprecated density.density_from_Universe() - deprecate in 1.0.0 - remove in 2.0.0 - adjusted tests that test for warnings: only test for UserWarning and match the message with regex in pytest.warns() instead manually looking through the WarningReport object: the problem is that the deprecation warning changes the number of entries --- package/CHANGELOG | 3 +- package/MDAnalysis/analysis/density.py | 57 +++++++++++-------- .../MDAnalysisTests/analysis/test_density.py | 28 ++++----- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index d40a3b41a21..33a44f7cc7c 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -126,8 +126,9 @@ Changes Deprecations * analysis.hbonds.HydrogenBondAnalysis is deprecated in 1.0 (remove in 2.0) + * analysis.density.density_from_Universe() (remove in 2.0) - + 09/05/19 IAlibay, richardjgowers * 0.20.1 diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index f4ee108bf80..c1c1a489614 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -63,23 +63,27 @@ trajectory is already appropriately treated for periodic boundary artifacts and is suitably superimposed to provide a fixed reference frame) [#testraj]_ :: - from MDAnalysis.analysis.density import density_from_Universe + from MDAnalysis.analysis.density import DensityAnalysis u = Universe(TPR, XTC) - D = density_from_Universe(u, delta=1.0, select="name OW") + ow = u.select_atoms("name OW") + D = DensityAnalysis(ow, delta=1.0) + D.run() D.convert_density('TIP4P') - D.export("water.dx", type="double") + D.density.export("water.dx", type="double") -The positions of all water oxygens are histogrammed on a grid with spacing -*delta* = 1 Å. Initially the density is measured in :math:`\text{Å}^{-3}`. With -the :meth:`Density.convert_density` method, the units of measurement are -changed. In the example we are now measuring the density relative to the -literature value of the TIP4P water model at ambient conditions (see the values -in :data:`MDAnalysis.units.water` for details). Finally, the density is written -as an OpenDX_ compatible file that can be read in VMD_, Chimera_, or PyMOL_. +The positions of all water oxygens (the :class:`AtomGroup` `ow`) are +histogrammed on a grid with spacing *delta* = 1 Å. Initially the density is +measured in :math:`\text{Å}^{-3}`. With the :meth:`Density.convert_density` +method, the units of measurement are changed. In the example we are now +measuring the density relative to the literature value of the TIP4P water model +at ambient conditions (see the values in :data:`MDAnalysis.units.water` for +details). Finally, the density is written as an OpenDX_ compatible file that +can be read in VMD_, Chimera_, or PyMOL_. -See :class:`Density` for details. In particular, the density is stored -as a NumPy array in :attr:`Density.grid`, which can be processed in -any manner. +The :class:`Density` object is accessible as the +:attr:`DensityAnalysis.density` attribute. In particular, the data for the +density is stored as a NumPy array in :attr:`Density.grid`, which can be +processed in any manner. Creating densities @@ -88,6 +92,7 @@ The following functions take trajectory or coordinate data and generate a :class:`Density` object. +.. autoclass:; DensityAnalysis .. autofunction:: density_from_Universe .. autofunction:: density_from_PDB .. autofunction:: Bfactor2RMSF @@ -180,11 +185,13 @@ from .. import units from ..lib import distances from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.util import deprecate import logging logger = logging.getLogger("MDAnalysis.analysis.density") + class Density(Grid): r"""Class representing a density on a regular cartesian grid. @@ -272,7 +279,7 @@ class Density(Grid): See Also -------- - gridData.core.Grid : the base class of :class:`Density`. + gridData.core.Grid is the base class of :class:`Density`. Examples -------- @@ -311,6 +318,7 @@ class Density(Grid): subtracted, multiplied, ...) but there are *no sanity checks* in place to make sure that units, metadata, etc are compatible! + .. Note:: It is suggested to construct the Grid object from a histogram, @@ -551,6 +559,8 @@ def _set_user_grid(gridcenter, xdim, ydim, zdim, smin, smax): return umin, umax +@deprecate(release="1.0.0", remove="2.0.0", + message="Use ``DensityAnalysis(u, ...).run().density`` instead.") def density_from_Universe(universe, delta=1.0, select='name OH2', start=None, stop=None, step=None, metadata=None, padding=2.0, cutoff=0, soluteselection=None, @@ -705,20 +715,21 @@ def density_from_Universe(universe, delta=1.0, select='name OH2', atom_count_histogram = physical_density * volume - .. versionchanged:: 0.21.0 - Warns users that `padding` value is not used in user defined grids - .. versionchanged:: 0.20.0 - ProgressMeter now iterates over the number of frames analysed. - .. versionchanged:: 0.19.0 - *gridcenter*, *xdim*, *ydim* and *zdim* keywords added to allow for user - defined boxes .. versionchanged:: 0.13.0 *update_selection* and *quiet* keywords added .. deprecated:: 0.16 The keyword argument *quiet* is deprecated in favor of *verbose*. - .. versionchanged:: 0.21.0 + .. versionchanged:: 0.19.0 + *gridcenter*, *xdim*, *ydim* and *zdim* keywords added to allow for user + defined boxes + .. versionchanged:: 0.20.0 + ProgressMeter now iterates over the number of frames analysed. + .. versionchanged:: 1.0.0 time_unit and length_unit default to ps and Angstrom now flags have - been removed (same as previous flag defaults) + been removed (same as previous flag defaults); + warns users that `padding` value is not used in user defined grids + .. deprecated:: 1.0.0 + `density_from_Universe` will removed in 2.0.0; use `DensityAnalysis` instead """ u = universe diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index abc74d3089a..c0e85bb6e58 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -241,30 +241,24 @@ def test_density_from_Universe_userdefn_boxshape(self, universe): def test_density_from_Universe_userdefn_padding(self, universe): import MDAnalysis.analysis.density - wmsg = ("Box padding (currently set at 1.0) is not used in user " - "defined grids.") - with pytest.warns(UserWarning) as record: + regex = ("Box padding \(currently set at 1\.0\) is not used " + "in user defined grids\.") + with pytest.warns(UserWarning, match=regex): D = MDAnalysis.analysis.density.density_from_Universe( universe, select=self.selections['static'], - delta=1.0, xdim=1.0, ydim=2.0, zdim=2.0, padding=1.0, + delta=self.delta, xdim=100.0, ydim=100.0, zdim=100.0, padding=1.0, gridcenter=self.gridcenters['static_defined']) - - assert len(record) == 3 - assert str(record[1].message.args[0]) == wmsg def test_density_from_Universe_userdefn_selwarning(self, universe): import MDAnalysis.analysis.density - wmsg = ("Atom selection does not fit grid --- " + regex = ("Atom selection does not fit grid --- " "you may want to define a larger box") - with pytest.warns(UserWarning) as record: + with pytest.warns(UserWarning, match=regex): D = MDAnalysis.analysis.density.density_from_Universe( universe, select=self.selections['static'], - delta=1.0, xdim=1.0, ydim=2.0, zdim=2.0, padding=0.0, + delta=self.delta, xdim=1.0, ydim=2.0, zdim=2.0, padding=0.0, gridcenter=self.gridcenters['static_defined']) - assert len(record) == 2 - assert str(record[1].message.args[0]) == wmsg - def test_density_from_Universe_userdefn_ValueErrors(self, universe): import MDAnalysis.analysis.density # Test len(gridcenter) != 3 @@ -286,6 +280,14 @@ def test_density_from_Universe_userdefn_ValueErrors(self, universe): delta=self.delta, xdim="MDAnalysis", ydim=10.0, zdim=10.0, gridcenter=self.gridcenters['static_defined']) + def test_density_from_Universe_Deprecation_warning(self, universe): + import MDAnalysis.analysis.density + with pytest.warns(DeprecationWarning, + match="will be removed in release 2.0.0"): + D = MDAnalysis.analysis.density.density_from_Universe( + universe, select=self.selections['static'], + delta=self.delta) + class TestGridImport(object): From 72654fa3c3a880de5de3f9efd4d8371f7a4350b6 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Mon, 17 Feb 2020 22:03:01 -0800 Subject: [PATCH 03/10] removed tests for missing gridDataFormats These tests are not necessary because gridDataFormats is a dependency. --- .../MDAnalysisTests/analysis/test_density.py | 78 ++++++------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index c0e85bb6e58..c40a91c786f 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -37,8 +37,6 @@ from MDAnalysis.analysis import density from MDAnalysisTests.datafiles import TPR, XTC, GRO -from mock import Mock, patch -from MDAnalysisTests.util import block_import class TestDensity(object): @@ -156,10 +154,8 @@ def universe(self): def check_density_from_Universe(self, atomselection, ref_meandensity, universe, tmpdir, **kwargs): - import MDAnalysis.analysis.density - with tmpdir.as_cwd(): - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=atomselection, delta=self.delta, **kwargs) assert_almost_equal(D.grid.mean(), ref_meandensity, @@ -167,7 +163,7 @@ def check_density_from_Universe(self, atomselection, ref_meandensity, D.export(self.outfile) - D2 = MDAnalysis.analysis.density.Density(self.outfile) + D2 = density.Density(self.outfile) assert_almost_equal(D.grid, D2.grid, decimal=self.precision, err_msg="DX export failed: different grid sizes") @@ -232,89 +228,58 @@ def test_density_from_Universe_userdefn_neqbox(self, universe, tmpdir): ) def test_density_from_Universe_userdefn_boxshape(self, universe): - import MDAnalysis.analysis.density - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=self.selections['static'], delta=1.0, xdim=8.0, ydim=12.0, zdim=17.0, gridcenter=self.gridcenters['static_defined']) assert D.grid.shape == (8, 12, 17) def test_density_from_Universe_userdefn_padding(self, universe): - import MDAnalysis.analysis.density regex = ("Box padding \(currently set at 1\.0\) is not used " "in user defined grids\.") with pytest.warns(UserWarning, match=regex): - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=self.selections['static'], delta=self.delta, xdim=100.0, ydim=100.0, zdim=100.0, padding=1.0, gridcenter=self.gridcenters['static_defined']) def test_density_from_Universe_userdefn_selwarning(self, universe): - import MDAnalysis.analysis.density regex = ("Atom selection does not fit grid --- " "you may want to define a larger box") with pytest.warns(UserWarning, match=regex): - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=self.selections['static'], delta=self.delta, xdim=1.0, ydim=2.0, zdim=2.0, padding=0.0, gridcenter=self.gridcenters['static_defined']) def test_density_from_Universe_userdefn_ValueErrors(self, universe): - import MDAnalysis.analysis.density # Test len(gridcenter) != 3 with pytest.raises(ValueError): - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=self.selections['static'], delta=self.delta, xdim=10.0, ydim=10.0, zdim=10.0, gridcenter=self.gridcenters['error1']) # Test gridcenter includes non-numeric strings with pytest.raises(ValueError): - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=self.selections['static'], delta=self.delta, xdim=10.0, ydim=10.0, zdim=10.0, gridcenter=self.gridcenters['error2']) # Test xdim != int or float with pytest.raises(ValueError): - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=self.selections['static'], delta=self.delta, xdim="MDAnalysis", ydim=10.0, zdim=10.0, gridcenter=self.gridcenters['static_defined']) def test_density_from_Universe_Deprecation_warning(self, universe): - import MDAnalysis.analysis.density with pytest.warns(DeprecationWarning, match="will be removed in release 2.0.0"): - D = MDAnalysis.analysis.density.density_from_Universe( + D = density.density_from_Universe( universe, select=self.selections['static'], delta=self.delta) -class TestGridImport(object): - - @block_import('gridData') - def test_absence_griddata(self): - 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) - # 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}): - try: - import MDAnalysis.analysis.density - except ImportError: - pytest.fail(msg='''MDAnalysis.analysis.density should not raise - an ImportError if gridData is available.''') - - class TestNotWithin(object): # tests notwithin_coordinates_factory # only checks that KDTree and distance_array give same results @@ -325,17 +290,24 @@ def u(): return mda.Universe(GRO) def test_within(self, u): - from MDAnalysis.analysis.density import notwithin_coordinates_factory as ncf - - vers1 = ncf(u, 'resname SOL', 'protein', 2, not_within=False, use_kdtree=True)() - vers2 = ncf(u, 'resname SOL', 'protein', 2, not_within=False, use_kdtree=False)() + vers1 = density.notwithin_coordinates_factory(u, 'resname SOL', + 'protein', 2, + not_within=False, + use_kdtree=True)() + vers2 = density.notwithin_coordinates_factory(u, 'resname SOL', + 'protein', 2, + not_within=False, + use_kdtree=False)() assert_equal(vers1, vers2) def test_not_within(self, u): - from MDAnalysis.analysis.density import notwithin_coordinates_factory as ncf - - vers1 = ncf(u, 'resname SOL', 'protein', 2, not_within=True, use_kdtree=True)() - vers2 = ncf(u, 'resname SOL', 'protein', 2, not_within=True, use_kdtree=False)() - + vers1 = density.notwithin_coordinates_factory(u, 'resname SOL', + 'protein', 2, + not_within=True, + use_kdtree=True)() + vers2 = density.notwithin_coordinates_factory(u, 'resname SOL', + 'protein', 2, + not_within=True, + use_kdtree=False)() assert_equal(vers1, vers2) From 2714988acb5dee5924a814968354f9eb1aa1998c Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Mon, 17 Feb 2020 23:35:12 -0800 Subject: [PATCH 04/10] add new density.DensityAnalysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - close #2502 - DensityAnalysis is based on @nawtrey's pmda.density.DensityAnalysis but replace _reduce with accumulation in _single_frame - added tests (based on the Test_density_from_universe): both give the same answer as they pass the same tests; test specific exceptions in DensityAnalysis - update docs (also write ångström consistently throughout) - update CHANGELOG --- package/CHANGELOG | 3 +- package/MDAnalysis/analysis/density.py | 370 ++++++++++++++++-- .../MDAnalysisTests/analysis/test_density.py | 112 +++++- 3 files changed, 439 insertions(+), 46 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 33a44f7cc7c..feb3eb8cfb7 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -15,7 +15,7 @@ The rules for this file: ------------------------------------------------------------------------------ mm/dd/yy richardjgowers, kain88-de, lilyminium, p-j-smith, bdice, joaomcteixeira, PicoCentauri, davidercruz, jbarnoud, RMeli, IAlibay, mtiberti, CCook96, - Yuan-Yu, xiki-tempula + Yuan-Yu, xiki-tempula, orbeckst * 0.21.0 @@ -56,6 +56,7 @@ Fixes * Added parmed to setup.py Enhancements + * Added new density.DensityAnalysis (Issue #2502) * Added coordinate reader and writer for NAMD binary coordinate format (PR #2485) * Improved ClusterCollection and Cluster string representations (Issue #2464) * XYZ parser store elements attribute (#2420) and XYZ write uses the elements diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index c1c1a489614..3d6a1863632 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -68,7 +68,7 @@ ow = u.select_atoms("name OW") D = DensityAnalysis(ow, delta=1.0) D.run() - D.convert_density('TIP4P') + D.density.convert_density('TIP4P') D.density.export("water.dx", type="double") The positions of all water oxygens (the :class:`AtomGroup` `ow`) are @@ -89,68 +89,75 @@ Creating densities ------------------ -The following functions take trajectory or coordinate data and generate a -:class:`Density` object. +The :class:`DensityAnalysis` class generates a :class:`Density` from an +atomgroup. Its function equivalent :func:`density_from_Universe` is deprecated. -.. autoclass:; DensityAnalysis -.. autofunction:: density_from_Universe -.. autofunction:: density_from_PDB -.. autofunction:: Bfactor2RMSF +:func:`density_from_PDB` generates a pseudo-density from a PDB file by +replacing each atom with a Gaussian density with a width that is computed from +the crystallographic temperature factors (B-factor) with :func:`Bfactor2RMSF`. + +.. autoclass:: DensityAnalysis + :members: + :inherited-members: run + + .. attribute:: density + + After the analysis (see the :meth:`~DensityAnalysis.run` method), the resulting density is + stored in the :attr:`density` attribut as a :class:`Density` instance. -Supporting classes and functions --------------------------------- +Density object +-------------- + +The main output of the density creation functions is a :class:`Density` +instance, which is derived from a :class:`gridData.core.Grid`. A +:class:`Density` is essentially a 3D array with origin and lengths. + +.. See Also:: :mod:`gridData` -The main output of the density creation functions is a -:class:`Density` instance, which is derived from a -:class:`gridData.core.Grid`. A :class:`Density` is essentially, a 3D -array with origin and lengths together with associated metadata (which -can be used in downstream processing). .. autoclass:: Density :members: :inherited-members: :show-inheritance: -.. autoclass:: BfactorDensityCreator - :members: -.. autofunction:: notwithin_coordinates_factory +Deprecated functionality +------------------------ +Use :class:`DensityAnalysis` instead of :func:`density_from_Universe`. The +:func:`density_from_PDB` function is no longer supported and will also be +removed in 2.0.0. -.. rubric:: Footnotes - -.. [#pbc] Making molecules whole can be accomplished with the - :meth:`MDAnalysis.core.groups.AtomGroup.wrap` of - :attr:`Universe.atoms` (use ``compound="fragments"``). +.. autofunction:: density_from_Universe - When using, for instance, the Gromacs_ command `gmx trjconv`_ +.. autofunction:: density_from_PDB - .. code-block:: bash +.. autofunction:: Bfactor2RMSF - gmx trjconv -pbc mol -center -ur compact +.. autoclass:: BfactorDensityCreator + :members: - one can make the molecules whole ``-pbc whole``, center it on a group - (``-center``), and also pack all molecules in a compact unitcell - representation, which can be useful for density generation. +.. autofunction:: notwithin_coordinates_factory -.. [#fit] Superposition can be performed with - :class:`MDAnalysis.analysis.align.AlignTraj`. - The Gromacs_ command `gmx trjconv`_ - .. code-block:: bash +.. rubric:: Footnotes - gmx trjconv -fit rot+trans +.. [#pbc] Making molecules whole can be accomplished with the + :meth:`MDAnalysis.core.groups.AtomGroup.wrap` of + :attr:`Universe.atoms` (use ``compound="fragments"``). or the + PBC-wrapping transformations in + :mod:`MDAnalysis.transformations.wrap`. - will also accomplish such a superposition. Note that the fitting has - to be done in a *separate* step from the treatment of the periodic - boundaries [#pbc]_. +.. [#fit] Superposition can be performed with + :class:`MDAnalysis.analysis.align.AlignTraj` or the fitting + transformations in :mod:`MDAnalysis.transformations.fit`. -.. [#testraj] Note that the trajectory in the example (`XTC`) is *not* properly - made whole and fitted to a reference structure; these steps were - omitted to clearly show the steps necessary for the actual density - calculation. +.. [#testraj] Note that the trajectory in the example (`XTC`) is *not* + properly made whole and fitted to a reference structure; + these steps were omitted to clearly show the steps necessary + for the actual density calculation. .. Links .. ----- @@ -187,10 +194,284 @@ from MDAnalysis.lib.log import ProgressMeter from MDAnalysis.lib.util import deprecate +from .base import AnalysisBase + import logging logger = logging.getLogger("MDAnalysis.analysis.density") +class DensityAnalysis(AnalysisBase): + r"""Volumetric density analysis. + + The trajectory is read, frame by frame, and the atoms in `atomgroup` are + histogrammed on a 3D grid with spacing `delta`. + + Parameters + ---------- + atomgroup : AtomGroup or UpdatingAtomGroup + Group of atoms (such as all the water oxygen atoms) being analyzed. + This can be an :class:`~MDAnalysis.core.groups.UpdatingAtomGroup` for + selections that change every time step. + delta : float (optional) + Bin size for the density grid in ångström (same in x,y,z). + padding : float (optional) + Increase histogram dimensions by padding (on top of initial box + size) in ångström. Padding is ignored when setting a user defined + grid. + gridcenter : numpy ndarray, float32 (optional) + 3 element numpy array detailing the x, y and z coordinates of the + center of a user defined grid box in ångström. + xdim : float (optional) + User defined x dimension box edge in ångström; ignored if + gridcenter is "None". + ydim : float (optional) + User defined y dimension box edge in ångström; ignored if + gridcenter is "None". + zdim : float (optional) + User defined z dimension box edge in ångström; ignored if + gridcenter is "None". + + See Also + -------- + pmda.density.DensityAnalysis for a parallel version + + Notes + ----- + Normal :class:`AtomGroup` instances represent a static selection of + atoms. If you want *dynamically changing selections* (such as "name OW and + around 4.0 (protein and not name H*)", i.e., the water oxygen atoms that + are within 4 Å of the protein heavy atoms) then create an + :class:`~MDAnalysis.core.groups.UpdatingAtomGroup` (see Examples). + + + Examples + -------- + A common use case is to analyze the solvent density around a protein of + interest. The density is calculated with :class:`DensityAnalysis` in the + fixed coordinate system of the simulation unit cell. It is therefore + necessary to orient and fix the protein with respect to the box coordinate + system. In practice this means centering and superimposing the protein, + frame by frame, on a reference structure and translating and rotating all + other components of the simulation with the protein. In this way, the + solvent will appear in the reference frame of the protein. + + An input trajectory must + + 1. have been centered on the protein of interest; + 2. have all molecules made whole that have been broken across periodic + boundaries [#pbc]_; + 3. have the solvent molecules remapped so that they are closest to the + solute (this is important when using triclinic unit cells such as + a dodecahedron or a truncated octahedron) [#pbc]_; + 4. have a fixed frame of reference; for instance, by superimposing a + protein on a reference structure so that one can study the solvent + density around it [#fit]_. + + .. rubric:: Generate the density + + To generate the density of water molecules around a protein (assuming that + the trajectory is already appropriately treated for periodic boundary + artifacts and is suitably superimposed to provide a fixed reference frame) + [#testraj]_, first create the :class:`DensityAnalysis` object by + supplying an AtomGroup, then use the :meth:`run` method:: + + from MDAnalysis.analysis import density + u = Universe(TPR, XTC) + ow = u.select_atoms("name OW") + D = density.DensityAnalysis(ow, delta=1.0) + D.run() + D.density.convert_density('TIP4P') + + The positions of all water oxygens are histogrammed on a grid with spacing + *delta* = 1 Å and stored as a :class:`Density` object in the attribute + :attr:`DensityAnalysis.density`. + + .. rubric:: Working with a density + + A :class:`Density` contains a large number of methods and attributes that + are listed in the documentation. Here we use the + :meth:`Density.convert_density` to convert the density from inverse cubic + ångström to a density relative to the bulk density of TIP4P water at + standard conditions. (MDAnalysis stores a number of literature values in + :data:`MDAnalysis.units.water`.) + + One can directly access the density as a 3D NumPy array through + :attr:`Density.grid`. + + By default, the :class:`Density` object returned contains a physical + density in units of Å\ :sup:`-3`. If you are interested in recovering the + underlying **probability density**, simply divide by the sum:: + + probability_density = D.density.grid / D.density.grid.sum() + + Similarly, if you would like to recover a grid containing a **histogram of + atom counts**, simply multiply by the volume `dV` of each bin (or voxel); + in this case you need to ensure that the physical density is measured in + Å\ :sup:`-3` by converting it:: + + import numpy as np + + # ensure that the density is A^{-3} + D.density.convert_density("A^{-3}") + + dV = np.prod(D.density.delta) + atom_count_histogram = D.density.grid * dV + + + .. rubric:: Writing the density to a file + + A density can be `exported to different formats + `_ with + :meth:`Density.export` (thanks to the fact that :class:`Density` is a + subclass :class:`gridData.core.Grid`, which provides the functionality). + For example, to `write a DX file + `_ + ``water.dx`` that can be read with VMD, PyMOL, or Chimera:: + + D.density.export("water.dx", type="double") + + + .. rubric:: Example: Water density in the whole simulation + + Basic use for creating a water density (just using the water oxygen + atoms "OW"):: + + D = DensityAnalysis(universe.select_atoms('name OW')).run() + + + .. rubric:: Example: Water in a binding site (updating selection) + + If you are only interested in water within a certain region, e.g., within + a vicinity around a binding site, you can use a selection that updates + every step by using an :class:`~MDAnalysis.core.groups.UpdatingAtomGroup`:: + + near_waters = universe.select_atoms('name OW and around 5 (resid 156 157 305)', + updating=True) + D_site = DensityAnalysis(near_waters).run() + + + .. rubric:: Example: Small region around a ligand (manual box selection) + + If you are interested in explicitly setting a grid box of a given edge size + and origin, you can use the `gridcenter` and `xdim`/`ydim`/`zdim` + arguments. For example to plot the density of waters within 5 Å of a + ligand (in this case the ligand has been assigned the residue name "LIG") + in a cubic grid with 20 Å edges which is centered on the center of mass + (COM) of the ligand:: + + # Create a selection based on the ligand + ligand_selection = universe.select_atoms("resname LIG") + + # Extract the COM of the ligand + ligand_COM = ligand_selection.center_of_mass() + + # Create a density of waters on a cubic grid centered on the ligand COM + # In this case, we update the atom selection as shown above. + ligand_waters = universe.select_atoms('name OW and around 5 resname LIG', + updating=True) + D_water = DensityAnalysis(ligand_waters, + delta=1.0, + gridcenter=ligand_COM, + xdim=20, ydim=20, zdim=20) + + (It should be noted that the `padding` keyword is not used when a user + defined grid is assigned). + + + + .. versionadded:: 1.0.0 + + """ + 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 + self._delta = delta + self._padding = padding + self._gridcenter = gridcenter + self._xdim = xdim + self._ydim = ydim + self._zdim = zdim + + def _prepare(self): + coord = self._atomgroup.positions + if self._gridcenter is not None: + # Issue 2372: padding is ignored, defaults to 2.0 therefore warn + if self._padding > 0: + msg = ("Box padding (currently set at {0}) " + "is not used in user defined grids.".format(self._padding)) + warnings.warn(msg) + logger.warning(msg) + # Generate a copy of smin/smax from coords to later check if the + # defined box might be too small for the selection + smin = np.min(coord, axis=0) + smax = np.max(coord, axis=0) + # Overwrite smin/smax with user defined values + smin, smax = _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 + # case the bulk regions don't have to be close to 1 * n0 but can + # be less. It's much more difficult to deal with outliers. The + # ideal solution would use images: implement 'looking across the + # periodic boundaries' but that gets complicated when the box + # rotates due to RMS fitting. + smin = np.min(coord, axis=0) - self._padding + smax = np.max(coord, axis=0) + self._padding + BINS = fixedwidth_bins(self._delta, smin, smax) + 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, normed=False) + grid *= 0.0 + self._grid = grid + self._edges = edges + self._arange = arange + self._bins = bins + self.density = None + + def _single_frame(self): + h, _ = np.histogramdd(self._atomgroup.positions, + bins=self._bins, range=self._arange, + normed=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 + + def _conclude(self): + # average: + self._grid /= float(self.n_frames) + density = Density(grid=self._grid, edges=self._edges, + units={'length': "Angstrom"}, + parameters={'isDensity': False}) + density.make_density() + self.density = density + + # _reduce is not strictly necessary for the serial version but is necessary for + # pmda-style parallelism (see #2542) + # @staticmethod + # def _reduce(res, result_single_frame): + # """'accumulate' action for a time series + # + # If `res` is a numpy array, the `result_single_frame` is added to it + # element-wise. If `res` and `result_single_frame` are lists then + # `result_single_frame` is appended to `res`. + # """ + # if isinstance(res, list) and len(res) == 0: + # res = result_single_frame + # else: + # res += result_single_frame + # return res + class Density(Grid): r"""Class representing a density on a regular cartesian grid. @@ -311,7 +592,7 @@ class Density(Grid): D.convert_density('water') After the final step, ``D`` will contain a density on a grid measured in - Ångstrom, with the density values itself measured relative to the + ångström, with the density values itself measured relative to the density of water. :class:`Density` objects can be algebraically manipulated (added, @@ -957,6 +1238,9 @@ def notwithin_coordinates(cutoff=cutoff): return notwithin_coordinates +# This is useful in principle but it is not needed here anymore. Maybe move +# to the rms module?? +@deprecate(release="1.0.0", remove="2.0.0") def Bfactor2RMSF(B): r"""Atomic root mean square fluctuation (in Angstrom) from the crystallographic B-factor @@ -972,7 +1256,8 @@ def Bfactor2RMSF(B): \rm{RMSF} = \sqrt{\frac{3 B}{8\pi^2}} - .. rubric:: References + References + ---------- .. [Willis1975] BTM Willis and AW Pryor. *Thermal vibrations in crystallography*. Cambridge Univ. Press, 1975 """ @@ -1050,6 +1335,7 @@ class BfactorDensityCreator(object): """ + @deprecate(release="1.0.0", remove="2.0.0") def __init__(self, pdb, delta=1.0, select='resname HOH and name O', metadata=None, padding=1.0, sigma=None): """Construct the density from psf and pdb and the select. diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index c40a91c786f..ed023b9ffd9 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -118,8 +118,7 @@ def test_export_types(self, D, dxtype, tmpdir, outfile="density.dx"): data = dx.components['data'] assert data.type == dxtype - -class Test_density_from_Universe(object): +class DensityParameters(object): topology = TPR trajectory = XTC delta = 2.0 @@ -152,6 +151,114 @@ class Test_density_from_Universe(object): def universe(self): return mda.Universe(self.topology, self.trajectory) +class TestDensityAnalysis(DensityParameters): + def check_DensityAnalysis(self, ag, ref_meandensity, + tmpdir, runargs=None, **kwargs): + runargs = runargs if runargs else {} + with tmpdir.as_cwd(): + D = density.DensityAnalysis(ag, delta=self.delta, **kwargs).run(**runargs) + assert_almost_equal(D.density.grid.mean(), ref_meandensity, + err_msg="mean density does not match") + D.density.export(self.outfile) + + D2 = density.Density(self.outfile) + assert_almost_equal(D.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): + updating = (mode == "dynamic") + self.check_DensityAnalysis( + universe.select_atoms(self.selections[mode], updating=updating), + self.references[mode]['meandensity'], + tmpdir=tmpdir + ) + + def test_sliced(self, universe, tmpdir): + self.check_DensityAnalysis( + universe.select_atoms(self.selections['static']), + self.references['static_sliced']['meandensity'], + tmpdir=tmpdir, + runargs=dict(start=1, stop=-1, step=2), + ) + + def test_userdefn_eqbox(self, universe, tmpdir): + with warnings.catch_warnings(): + # 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'], + tmpdir=tmpdir, + gridcenter=self.gridcenters['static_defined'], + xdim=10.0, + ydim=10.0, + zdim=10.0, + ) + + def test_userdefn_neqbox(self, universe, tmpdir): + self.check_DensityAnalysis( + universe.select_atoms(self.selections['static']), + self.references['static_defined_unequal']['meandensity'], + tmpdir=tmpdir, + gridcenter=self.gridcenters['static_defined'], + xdim=10.0, + ydim=15.0, + zdim=20.0, + ) + + def test_userdefn_boxshape(self, universe): + 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() + assert D.density.grid.shape == (8, 12, 17) + + def test_warn_userdefn_padding(self, universe): + regex = ("Box padding \(currently set at 1\.0\) is not used " + "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): + 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): + # 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) + + def test_ValueError_userdefn_gridcenter_type(self, universe): + # Test gridcenter includes non-numeric strings + with pytest.raises(ValueError, match="Non-number values assigned to gridcenter"): + 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) + + def test_ValueError_userdefn_xdim_type(self, universe): + # 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) + + +# remove in 2.0.0 +class Test_density_from_Universe(DensityParameters): def check_density_from_Universe(self, atomselection, ref_meandensity, universe, tmpdir, **kwargs): with tmpdir.as_cwd(): @@ -279,7 +386,6 @@ def test_density_from_Universe_Deprecation_warning(self, universe): universe, select=self.selections['static'], delta=self.delta) - class TestNotWithin(object): # tests notwithin_coordinates_factory # only checks that KDTree and distance_array give same results From 548c43f0ddd16288e6fc3c94e1749274e70a5e55 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Tue, 18 Feb 2020 00:29:42 -0800 Subject: [PATCH 05/10] deprecate density.notwithin_coordinates_factory When density_from_universe() is gone we will not need the function factory either. --- package/CHANGELOG | 1 + package/MDAnalysis/analysis/density.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/package/CHANGELOG b/package/CHANGELOG index feb3eb8cfb7..0a1dfb7727e 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -128,6 +128,7 @@ Changes Deprecations * analysis.hbonds.HydrogenBondAnalysis is deprecated in 1.0 (remove in 2.0) * analysis.density.density_from_Universe() (remove in 2.0) + * analysis.density.notwithin_coordinates_factory() (remove in 2.0) 09/05/19 IAlibay, richardjgowers diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index 3d6a1863632..90dad48094f 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -1129,6 +1129,7 @@ def current_coordinates(): return g +@deprecate(release="1.0.0", remove="2.0.0") def notwithin_coordinates_factory(universe, sel1, sel2, cutoff, not_within=True, use_kdtree=True, updating_selection=False): """Generate optimized selection for '*sel1* not within *cutoff* of *sel2*' @@ -1190,6 +1191,9 @@ def notwithin_coordinates_factory(universe, sel1, sel2, cutoff, (Readability is enhanced by properly naming the generated function ``within_coordinates()``.) + + + .. deprecated:: 1.0.0 """ # Benchmark of FABP system (solvent 3400 OH2, protein 2100 atoms) on G4 powerbook, 500 frames # cpu/s relative speedup use_kdtree From 9764455cb61a80bb751bb794ce16fb28d76814b2 Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Tue, 18 Feb 2020 18:30:00 -0800 Subject: [PATCH 06/10] Bfactor2RMSF:simple test --- testsuite/MDAnalysisTests/analysis/test_density.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index ed023b9ffd9..38e3e5f2706 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -417,3 +417,11 @@ def test_not_within(self, u): not_within=True, use_kdtree=False)() assert_equal(vers1, vers2) + + +@pytest.mark.parametrize('B', [20, 20.0, 20.123, + np.array([45.678, 67.1, 0.0, 100.2])]) +def test_Bfactor2RMSF(B): + values = density.Bfactor2RMSF(B) + ref = np.sqrt(3. * B / 8.) / np.pi # original equation + assert_almost_equal(values, ref) From 115453270f08087a834922de10e4785b4faa639a Mon Sep 17 00:00:00 2001 From: Oliver Beckstein Date: Tue, 18 Feb 2020 18:47:45 -0800 Subject: [PATCH 07/10] density_from_PDB: deprecated, test, and bugfix - density_from_PDB is terrible code (I wrote it) and not used: deprecated and to be removed for 2.0.0 - added test so that it is covered - fixed bug that was uncovered by test (us ag.tempfactors) - added test for Bfactors2RMSF - test for deprecation warnings - organized in classes - shortened test names for density_from_universe() --- package/CHANGELOG | 3 +- package/MDAnalysis/analysis/density.py | 18 +++-- .../MDAnalysisTests/analysis/test_density.py | 67 ++++++++++++++----- 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 0a1dfb7727e..abe1d9801bd 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -129,8 +129,9 @@ Deprecations * analysis.hbonds.HydrogenBondAnalysis is deprecated in 1.0 (remove in 2.0) * analysis.density.density_from_Universe() (remove in 2.0) * analysis.density.notwithin_coordinates_factory() (remove in 2.0) + * analysis.density.density_from_PDB and BfactorDensityCreator (remove in 2.0) + - 09/05/19 IAlibay, richardjgowers * 0.20.1 diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index 90dad48094f..d777783609c 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -1268,6 +1268,7 @@ def Bfactor2RMSF(B): return np.sqrt(3. * B / 8.) / np.pi +@deprecate(release="1.0.0", remove="2.0.0") def density_from_PDB(pdb, **kwargs): """Create a density from a single frame PDB. @@ -1313,10 +1314,15 @@ def density_from_PDB(pdb, **kwargs): -------- :func:`Bfactor2RMSF` and :class:`BfactorDensityCreator` + + .. deprecated: 1.0.0 + This function is not well tested or optimized and will be removed + in 2.0.0. """ return BfactorDensityCreator(pdb, **kwargs).Density() +# REMOVE in 2.0.0 class BfactorDensityCreator(object): """Create a density grid from a pdb file using MDAnalysis. @@ -1337,6 +1343,10 @@ class BfactorDensityCreator(object): .. * Using a temporary Creator class with the .. :meth:`BfactorDensityCreator.Density` helper method is clumsy. + + .. deprecated: 1.0.0 + This function is not well tested or optimized and will be removed + in 2.0.0. """ @deprecate(release="1.0.0", remove="2.0.0") @@ -1405,11 +1415,11 @@ def __init__(self, pdb, delta=1.0, select='resname HOH and name O', if sigma is None: # histogram individually, and smear out at the same time # with the appropriate B-factor - if np.any(group.bfactors == 0.0): + if np.any(group.tempfactors == 0.0): wmsg = "Some B-factors are Zero (will be skipped)." logger.warning(wmsg) warnings.warn(wmsg, category=MissingDataWarning) - rmsf = Bfactor2RMSF(group.bfactors) + rmsf = Bfactor2RMSF(group.tempfactors) grid *= 0.0 # reset grid self.g = self._smear_rmsf(coord, grid, self.edges, rmsf) else: @@ -1450,7 +1460,7 @@ def _smear_sigma(self, grid, sigma): for iwat in range(len(pos[0])): # super-ugly loop p = tuple([wp[iwat] for wp in pos]) g += grid[p] * np.fromfunction(self._gaussian, grid.shape, dtype=np.int, p=p, sigma=sigma) - print("Smearing out atom position {0:4d}/{1:5d} with RMSF {2:4.2f} A\r".format(iwat + 1, len(pos[0]), sigma),) + # print("Smearing out atom position {0:4d}/{1:5d} with RMSF {2:4.2f} A\r".format(iwat + 1, len(pos[0]), sigma),) return g def _smear_rmsf(self, coordinates, grid, edges, rmsf): @@ -1463,7 +1473,7 @@ def _smear_rmsf(self, coordinates, grid, edges, rmsf): continue g += np.fromfunction(self._gaussian_cartesian, grid.shape, dtype=np.int, c=coord, sigma=rmsf[iwat]) - print("Smearing out atom position {0:4d}/{1:5d} with RMSF {2:4.2f} A\r".format(iwat + 1, N, rmsf[iwat]),) + # print("Smearing out atom position {0:4d}/{1:5d} with RMSF {2:4.2f} A\r".format(iwat + 1, N, rmsf[iwat]),) return g def _gaussian(self, i, j, k, p, sigma): diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index 38e3e5f2706..5e326ed04f9 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -36,7 +36,7 @@ import MDAnalysis as mda from MDAnalysis.analysis import density -from MDAnalysisTests.datafiles import TPR, XTC, GRO +from MDAnalysisTests.datafiles import TPR, XTC, GRO, PDB_full class TestDensity(object): @@ -282,7 +282,7 @@ def test_density_from_Universe(self, universe, tmpdir): tmpdir=tmpdir ) - def test_density_from_Universe_sliced(self, universe, tmpdir): + def test_sliced(self, universe, tmpdir): self.check_density_from_Universe( self.selections['static'], self.references['static_sliced']['meandensity'], @@ -291,7 +291,7 @@ def test_density_from_Universe_sliced(self, universe, tmpdir): tmpdir=tmpdir ) - def test_density_from_Universe_update_selection(self, universe, tmpdir): + def test_update_selection(self, universe, tmpdir): self.check_density_from_Universe( self.selections['dynamic'], self.references['dynamic']['meandensity'], @@ -300,7 +300,7 @@ def test_density_from_Universe_update_selection(self, universe, tmpdir): tmpdir=tmpdir ) - def test_density_from_Universe_notwithin(self, universe, tmpdir): + def test_notwithin(self, universe, tmpdir): self.check_density_from_Universe( self.selections['static'], self.references['notwithin']['meandensity'], @@ -310,7 +310,7 @@ def test_density_from_Universe_notwithin(self, universe, tmpdir): tmpdir=tmpdir ) - def test_density_from_Universe_userdefn_eqbox(self, universe, tmpdir): + def test_userdefn_eqbox(self, universe, tmpdir): self.check_density_from_Universe( self.selections['static'], self.references['static_defined']['meandensity'], @@ -322,7 +322,7 @@ def test_density_from_Universe_userdefn_eqbox(self, universe, tmpdir): tmpdir=tmpdir ) - def test_density_from_Universe_userdefn_neqbox(self, universe, tmpdir): + def test_userdefn_neqbox(self, universe, tmpdir): self.check_density_from_Universe( self.selections['static'], self.references['static_defined_unequal']['meandensity'], @@ -334,14 +334,14 @@ def test_density_from_Universe_userdefn_neqbox(self, universe, tmpdir): tmpdir=tmpdir ) - def test_density_from_Universe_userdefn_boxshape(self, universe): + def test_userdefn_boxshape(self, universe): D = density.density_from_Universe( universe, select=self.selections['static'], delta=1.0, xdim=8.0, ydim=12.0, zdim=17.0, gridcenter=self.gridcenters['static_defined']) assert D.grid.shape == (8, 12, 17) - def test_density_from_Universe_userdefn_padding(self, universe): + def test_userdefn_padding(self, universe): regex = ("Box padding \(currently set at 1\.0\) is not used " "in user defined grids\.") with pytest.warns(UserWarning, match=regex): @@ -350,7 +350,7 @@ def test_density_from_Universe_userdefn_padding(self, universe): delta=self.delta, xdim=100.0, ydim=100.0, zdim=100.0, padding=1.0, gridcenter=self.gridcenters['static_defined']) - def test_density_from_Universe_userdefn_selwarning(self, universe): + def test_userdefn_selwarning(self, universe): regex = ("Atom selection does not fit grid --- " "you may want to define a larger box") with pytest.warns(UserWarning, match=regex): @@ -359,7 +359,7 @@ def test_density_from_Universe_userdefn_selwarning(self, universe): delta=self.delta, xdim=1.0, ydim=2.0, zdim=2.0, padding=0.0, gridcenter=self.gridcenters['static_defined']) - def test_density_from_Universe_userdefn_ValueErrors(self, universe): + def test_userdefn_ValueErrors(self, universe): # Test len(gridcenter) != 3 with pytest.raises(ValueError): D = density.density_from_Universe( @@ -379,13 +379,14 @@ def test_density_from_Universe_userdefn_ValueErrors(self, universe): delta=self.delta, xdim="MDAnalysis", ydim=10.0, zdim=10.0, gridcenter=self.gridcenters['static_defined']) - def test_density_from_Universe_Deprecation_warning(self, universe): + def test_has_DeprecationWarning(self, universe): with pytest.warns(DeprecationWarning, match="will be removed in release 2.0.0"): D = density.density_from_Universe( universe, select=self.selections['static'], delta=self.delta) + class TestNotWithin(object): # tests notwithin_coordinates_factory # only checks that KDTree and distance_array give same results @@ -418,10 +419,42 @@ def test_not_within(self, u): use_kdtree=False)() assert_equal(vers1, vers2) + def test_has_DeprecationWarning(self, u): + with pytest.warns(DeprecationWarning, + match="will be removed in release 2.0.0"): + density.notwithin_coordinates_factory(u, 'resname SOL', + 'protein', 2, + not_within=False, + use_kdtree=True)() + + +class TestBfactor2RMSF(object): + @pytest.mark.parametrize('B', [20, 20.0, 20.123, + np.array([45.678, 67.1, 0.0, 100.2])]) + def test_Bfactor2RMSF(self, B): + values = density.Bfactor2RMSF(B) + ref = np.sqrt(3. * B / 8.) / np.pi # original equation + assert_almost_equal(values, ref) -@pytest.mark.parametrize('B', [20, 20.0, 20.123, - np.array([45.678, 67.1, 0.0, 100.2])]) -def test_Bfactor2RMSF(B): - values = density.Bfactor2RMSF(B) - ref = np.sqrt(3. * B / 8.) / np.pi # original equation - assert_almost_equal(values, ref) + def test_has_DeprecationWarning(self): + with pytest.warns(DeprecationWarning, + match="will be removed in release 2.0.0"): + density.Bfactor2RMSF(100.0) + +class Test_density_from_PDB(object): + + @pytest.mark.parametrize('sigma,ref_shape,ref_gridsum', + [(None, (21, 26, 29), 108.710149), + (1.5, (21, 26, 29), 160.050167)]) + def test_density_from_PDB(self, sigma, ref_shape, ref_gridsum): + # simple test that the code runs: use B-factors from PDB or fixed sigma + D = density.density_from_PDB(PDB_full, delta=2.0, sigma=sigma) + + assert isinstance(D, density.Density) + assert_equal(D.grid.shape, ref_shape) + assert_almost_equal(D.grid.sum(), ref_gridsum, decimal=6) + + def test_has_DeprecationWarning(self): + with pytest.warns(DeprecationWarning, + match="will be removed in release 2.0.0"): + density.density_from_PDB(PDB_full, delta=5.0, sigma=2) From 837c2dea0ca8e0365b2be39574c3133910bb8d99 Mon Sep 17 00:00:00 2001 From: wvandertoorn <31660235+wvandertoorn@users.noreply.github.com> Date: Sun, 15 Mar 2020 15:15:44 +0100 Subject: [PATCH 08/10] Finalizing #2537 (#2616) Partially fixes #2502 (completes PR #2537) ## PR contents: This PR by @wvandertoorn finalises the work done as part of #2537 Key commits: - Fixes docstring (adds .. deprecated information). - Adds back :class:`TestGridImport`. - Reduces precision matching of `test_density_from_PDB`. - CHANGELOG and AUTHORS updated accordingly. --- package/AUTHORS | 1 + package/CHANGELOG | 12 ++++---- package/MDAnalysis/analysis/density.py | 7 +++++ .../MDAnalysisTests/analysis/test_density.py | 29 ++++++++++++++++++- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/package/AUTHORS b/package/AUTHORS index 70c44845279..e429eb939f0 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -128,6 +128,7 @@ Chronological list of authors 2020 - Charlie Cook - Yuanyu Chang + - Wiep van der Toorn External code ------------- diff --git a/package/CHANGELOG b/package/CHANGELOG index abe1d9801bd..1884f925866 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -15,7 +15,7 @@ The rules for this file: ------------------------------------------------------------------------------ mm/dd/yy richardjgowers, kain88-de, lilyminium, p-j-smith, bdice, joaomcteixeira, PicoCentauri, davidercruz, jbarnoud, RMeli, IAlibay, mtiberti, CCook96, - Yuan-Yu, xiki-tempula, orbeckst + Yuan-Yu, xiki-tempula, orbeckst, wvandertoorn * 0.21.0 @@ -25,7 +25,7 @@ Fixes * Handle exception when PDBWriter is trying to remove an invalid StringIO (Issue #2512) * Clarifies density_from_Universe docs and adds user warning (Issue #2372) - * Fix upstream deprecation of `matplotlib.axis.Tick` attributes in + * Fix upstream deprecation of `matplotlib.axis.Tick` attributes in `MDAnalysis.analysis.psa` * PDBWriter now uses first character of segid as ChainID (Issue #2224) * Adds a more detailed warning when attempting to read chamber-style parm7 @@ -33,7 +33,7 @@ Fixes * ClusterCollection.get_ids now returns correctly (Issue #2464) * Removes files for stubs mainly introduced in 0.11.0 (Issue #2443) * Removes support for reading AMBER NetCDF files with `cell_angle` units set - to `degrees` instead of the convention agreed `degree` (Issue #2327). + to `degrees` instead of the convention agreed `degree` (Issue #2327). * Removes the MassWeight option from gnm (PR #2479). * AtomGroup.guess_bonds now uses periodic boundary information when available (Issue #2350) @@ -91,15 +91,15 @@ Changes Segment.r1, Universe.s4AKE). Use select_atoms instead (Issue #1377) * Calling Universe() now raises a TypeError advising you to use Universe.empty. Universe.empty() now no longer has n_atoms=0 as default. (Issue #2527) - * deprecated `start`, `stop`, and `step` keywords have been removed from `__init__` - in :class:`AnalysisBase`. These should now be called in :meth:`AnalysisBase.run` + * deprecated `start`, `stop`, and `step` keywords have been removed from `__init__` + in :class:`AnalysisBase`. These should now be called in :meth:`AnalysisBase.run` (Issue #1463) * Standardize `select` keyword by removing `selection`, `atomselection` and `ref_select` (Issue #2461, Issue #2530) * Removes support for setting `start`/`stop`/`step` trajecotry slicing keywords on :class:`HOLEtraj` construction. Also removes undocumented support for passing :class:`HOLE` parameters to :meth:`HOLEtraj.run` - (Issue #2513). + (Issue #2513). * Removes the `nproc` keyword from :class:`Waterdynamics.HydrogenBondLifetimes` as the multiprocessing functionality did not work in some cases(Issues #2511). diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index d777783609c..47876147eb2 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -1194,6 +1194,8 @@ def notwithin_coordinates_factory(universe, sel1, sel2, cutoff, .. deprecated:: 1.0.0 + :func:`notwithin_coordinates_factory` is no longer supported and will be + removed in 2.0.0. """ # Benchmark of FABP system (solvent 3400 OH2, protein 2100 atoms) on G4 powerbook, 500 frames # cpu/s relative speedup use_kdtree @@ -1264,6 +1266,11 @@ def Bfactor2RMSF(B): ---------- .. [Willis1975] BTM Willis and AW Pryor. *Thermal vibrations in crystallography*. Cambridge Univ. Press, 1975 + + + .. deprecated:: 1.0.0 + :func:`Bfactor2RMSF` is no longer supported and will be removed in 2.0.0. + as part of the removal of the :func:`density_from_PDB` function. """ return np.sqrt(3. * B / 8.) / np.pi diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index 5e326ed04f9..f4d1381cffa 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -37,6 +37,8 @@ from MDAnalysis.analysis import density from MDAnalysisTests.datafiles import TPR, XTC, GRO, PDB_full +from unittest.mock import Mock, patch +from MDAnalysisTests.util import block_import class TestDensity(object): @@ -452,9 +454,34 @@ def test_density_from_PDB(self, sigma, ref_shape, ref_gridsum): assert isinstance(D, density.Density) assert_equal(D.grid.shape, ref_shape) - assert_almost_equal(D.grid.sum(), ref_gridsum, decimal=6) + assert_almost_equal(D.grid.sum(), ref_gridsum, decimal=5) def test_has_DeprecationWarning(self): with pytest.warns(DeprecationWarning, match="will be removed in release 2.0.0"): density.density_from_PDB(PDB_full, delta=5.0, sigma=2) + +class TestGridImport(object): + + @block_import('gridData') + def test_absence_griddata(self): + 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) + # 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}): + try: + import MDAnalysis.analysis.density + except ImportError: + pytest.fail(msg='''MDAnalysis.analysis.density should not raise + an ImportError if gridData is available.''') From 4cfdec6d76751364c7794b585639b254553ee7e4 Mon Sep 17 00:00:00 2001 From: wvandertoorn <31660235+wvandertoorn@users.noreply.github.com> Date: Mon, 16 Mar 2020 11:11:50 +0100 Subject: [PATCH 09/10] Indentation and import fix (#2629) --- package/MDAnalysis/analysis/density.py | 8 ++++---- testsuite/MDAnalysisTests/analysis/test_density.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index 47876147eb2..18906c1b110 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -1194,8 +1194,8 @@ def notwithin_coordinates_factory(universe, sel1, sel2, cutoff, .. deprecated:: 1.0.0 - :func:`notwithin_coordinates_factory` is no longer supported and will be - removed in 2.0.0. + :func:`notwithin_coordinates_factory` is no longer supported and will be + removed in 2.0.0. """ # Benchmark of FABP system (solvent 3400 OH2, protein 2100 atoms) on G4 powerbook, 500 frames # cpu/s relative speedup use_kdtree @@ -1269,8 +1269,8 @@ def Bfactor2RMSF(B): .. deprecated:: 1.0.0 - :func:`Bfactor2RMSF` is no longer supported and will be removed in 2.0.0. - as part of the removal of the :func:`density_from_PDB` function. + :func:`Bfactor2RMSF` is no longer supported and will be removed in 2.0.0. + as part of the removal of the :func:`density_from_PDB` function. """ return np.sqrt(3. * B / 8.) / np.pi diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index f4d1381cffa..5b4b191c71e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -37,7 +37,7 @@ from MDAnalysis.analysis import density from MDAnalysisTests.datafiles import TPR, XTC, GRO, PDB_full -from unittest.mock import Mock, patch +from mock import Mock, patch from MDAnalysisTests.util import block_import From 1bfb9a8228b9fd8efdbf492d95ad0fbd164e0bae Mon Sep 17 00:00:00 2001 From: wvandertoorn <31660235+wvandertoorn@users.noreply.github.com> Date: Tue, 17 Mar 2020 19:14:28 +0100 Subject: [PATCH 10/10] Wrap up loose ends from PR review (#2633) --- package/MDAnalysis/analysis/density.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index 18906c1b110..cd15e989478 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -103,7 +103,7 @@ .. attribute:: density After the analysis (see the :meth:`~DensityAnalysis.run` method), the resulting density is - stored in the :attr:`density` attribut as a :class:`Density` instance. + stored in the :attr:`density` attribute as a :class:`Density` instance. Density object @@ -386,8 +386,6 @@ 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