diff --git a/package/CHANGELOG b/package/CHANGELOG index b04e990ec39..7aaaeb1cadc 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -14,11 +14,13 @@ The rules for this file: ------------------------------------------------------------------------------ ??/??/18 tylerjereddy, richardjgowers, palnabarun, orbeckst, kain88-de, zemanj, - VOD555 + VOD555, davidercruz * 0.18.1 Enhancements + * Added a on-the-fly trajectory transformations API and a coordinate translation + function (Issue #786) * Added various duecredit stubs * Import time reduced by a factor two (PR #1881) * added compound kwarg to center, centroid, center_of_geometry, center_of_mass (PR #1903) @@ -26,6 +28,7 @@ Enhancements functions (PR #1815) Fixes + * rewind in the SingleFrameReader now reads the frame from the file (Issue #1929) * Fixed order of indices in Angle/Dihedral/Improper repr * coordinates.memory.MemoryReader now takes np.ndarray only (Issue #1685) * Silenced warning when duecredit is not installed (Issue #1872) diff --git a/package/MDAnalysis/coordinates/base.py b/package/MDAnalysis/coordinates/base.py index 1e20769c63d..859f7a29c80 100644 --- a/package/MDAnalysis/coordinates/base.py +++ b/package/MDAnalysis/coordinates/base.py @@ -278,6 +278,7 @@ def __init__(self, n_atoms, **kwargs): # set up aux namespace for adding auxiliary data self.aux = Namespace() + @classmethod def from_timestep(cls, other, **kwargs): @@ -1170,6 +1171,7 @@ def __init__(self): # initialise list to store added auxiliary readers in # subclasses should now call super self._auxs = {} + self._transformations=[] def __len__(self): return self.n_frames @@ -1201,6 +1203,9 @@ def next(self): else: for auxname in self.aux_list: ts = self._auxs[auxname].update_ts(ts) + + ts = self._apply_transformations(ts) + return ts def __next__(self): @@ -1356,10 +1361,13 @@ def _read_frame(self, frame): # return ts def _read_frame_with_aux(self, frame): - """Move to *frame*, updating ts with trajectory and auxiliary data.""" + """Move to *frame*, updating ts with trajectory, transformations and auxiliary data.""" ts = self._read_frame(frame) for aux in self.aux_list: ts = self._auxs[aux].update_ts(ts) + + ts = self._apply_transformations(ts) + return ts def _sliced_iter(self, start, stop, step): @@ -1741,7 +1749,67 @@ def get_aux_descriptions(self, auxnames=None): auxnames = self.aux_list descriptions = [self._auxs[aux].get_description() for aux in auxnames] return descriptions - + + @property + def transformations(self): + """ Returns the list of transformations""" + return self._transformations + + @transformations.setter + def transformations(self, transformations): + if not self._transformations: + self._transformations = transformations + else: + raise ValueError("Transformations are already set") + + def add_transformations(self, *transformations): + """ Add all transformations to be applied to the trajectory. + + This function take as list of transformations as an argument. These + transformations are functions that will be called by the Reader and given + a :class:`Timestep` object as argument, which will be transformed and returned + to the Reader. + The transformations can be part of the :mod:`~MDAnalysis.transformations` + module, or created by the user, and are stored as a list `transformations`. + This list can only be modified once, and further calls of this function will + raise an exception. + + .. code-block:: python + + u = MDAnalysis.Universe(topology, coordinates) + workflow = [some_transform, another_transform, this_transform] + u.trajectory.add_transformations(*workflow) + + Parameters + ---------- + transform_list : list + list of all the transformations that will be applied to the coordinates + + See Also + -------- + :mod:`MDAnalysis.transformations` + """ + + try: + self.transformations = transformations + except ValueError: + raise ValueError("Can't add transformations again. Please create new Universe object") + else: + self.ts = self._apply_transformations(self.ts) + + + # call reader here to apply the newly added transformation on the + # current loaded frame? + + def _apply_transformations(self, ts): + """Applies all the transformations given by the user """ + + for transform in self.transformations: + ts = transform(ts) + + return ts + + class ReaderBase(ProtoReader): """Base class for trajectory readers that extends :class:`ProtoReader` with a @@ -1802,6 +1870,8 @@ def copy(self): """ new = self.__class__(self.filename, n_atoms=self.n_atoms) + if self.transformations: + new.add_transformations(*self.transformations) # seek the new reader to the same frame we started with new[self.ts.frame] # then copy over the current Timestep in case it has @@ -1976,6 +2046,10 @@ def copy(self): new.ts = self.ts.copy() for auxname, auxread in self._auxs.items(): new.add_auxiliary(auxname, auxread.copy()) + # since the transformations have already been applied to the frame + # simply copy the property + new.transformations = self.transformations + return new def _read_first_frame(self): # pragma: no cover @@ -1983,7 +2057,10 @@ def _read_first_frame(self): # pragma: no cover pass def rewind(self): - pass + self._read_first_frame() + for auxname, auxread in self._auxs.items(): + self.ts = auxread.update_ts(self.ts) + super(SingleFrameReaderBase, self)._apply_transformations(self.ts) def _reopen(self): pass @@ -2006,3 +2083,47 @@ def close(self): # self.filename. Explicitly setting it to the null action in case # the IOBase.close method is ever changed from that. pass + + def add_transformations(self, *transformations): + """ Add all transformations to be applied to the trajectory. + + This function take as list of transformations as an argument. These + transformations are functions that will be called by the Reader and given + a :class:`Timestep` object as argument, which will be transformed and returned + to the Reader. + The transformations can be part of the :mod:`~MDAnalysis.transformations` + module, or created by the user, and are stored as a list `transformations`. + This list can only be modified once, and further calls of this function will + raise an exception. + + .. code-block:: python + + u = MDAnalysis.Universe(topology, coordinates) + workflow = [some_transform, another_transform, this_transform] + u.trajectory.add_transformations(*workflow) + + Parameters + ---------- + transform_list : list + list of all the transformations that will be applied to the coordinates + + See Also + -------- + :mod:`MDAnalysis.transformations` + """ + #Overrides :meth:`~MDAnalysis.coordinates.base.ProtoReader.add_transformations` + #to avoid unintended behaviour where the coordinates of each frame are transformed + #multiple times when iterating over the trajectory. + #In this method, the trajectory is modified all at once and once only. + + super(SingleFrameReaderBase, self).add_transformations(*transformations) + for transform in self.transformations: + self.ts = transform(self.ts) + + def _apply_transformations(self, ts): + """ Applies the transformations to the timestep.""" + # Overrides :meth:`~MDAnalysis.coordinates.base.ProtoReader.add_transformations` + # to avoid applying the same transformations multiple times on each frame + + return ts + diff --git a/package/MDAnalysis/coordinates/chain.py b/package/MDAnalysis/coordinates/chain.py index 9bbefa7ce36..612f3526b76 100644 --- a/package/MDAnalysis/coordinates/chain.py +++ b/package/MDAnalysis/coordinates/chain.py @@ -397,4 +397,45 @@ def __repr__(self): nframes=self.n_frames, natoms=self.n_atoms)) - + def add_transformations(self, *transformations): + """ Add all transformations to be applied to the trajectory. + + This function take as list of transformations as an argument. These + transformations are functions that will be called by the Reader and given + a :class:`Timestep` object as argument, which will be transformed and returned + to the Reader. + The transformations can be part of the :mod:`~MDAnalysis.transformations` + module, or created by the user, and are stored as a list `transformations`. + This list can only be modified once, and further calls of this function will + raise an exception. + + .. code-block:: python + + u = MDAnalysis.Universe(topology, coordinates) + workflow = [some_transform, another_transform, this_transform] + u.trajectory.add_transformations(*workflow) + + Parameters + ---------- + transform_list : list + list of all the transformations that will be applied to the coordinates + + See Also + -------- + :mod:`MDAnalysis.transformations` + """ + #Overrides :meth:`~MDAnalysis.coordinates.base.ProtoReader.add_transformations` + #to avoid unintended behaviour where the coordinates of each frame are transformed + #multiple times when iterating over the trajectory. + #In this method, the trajectory is modified all at once and once only. + + super(ChainReader, self).add_transformations(*transformations) + for r in self.readers: + r.add_transformations(*transformations) + + def _apply_transformations(self, ts): + """ Applies the transformations to the timestep.""" + # Overrides :meth:`~MDAnalysis.coordinates.base.ProtoReader.add_transformations` + # to avoid applying the same transformations multiple times on each frame + + return ts diff --git a/package/MDAnalysis/coordinates/memory.py b/package/MDAnalysis/coordinates/memory.py index 6f4fbcbaf88..ca02ee805c5 100644 --- a/package/MDAnalysis/coordinates/memory.py +++ b/package/MDAnalysis/coordinates/memory.py @@ -339,6 +339,9 @@ def copy(self): new.ts = self.ts.copy() for auxname, auxread in self._auxs.items(): new.add_auxiliary(auxname, auxread.copy()) + # since transformations are already applied to the whole trajectory + # simply copy the property + new.transformations = self.transformations return new @@ -480,3 +483,47 @@ def __repr__(self): nframes=self.n_frames, natoms=self.n_atoms )) + + def add_transformations(self, *transformations): + """ Add all transformations to be applied to the trajectory. + + This function take as list of transformations as an argument. These + transformations are functions that will be called by the Reader and given + a :class:`Timestep` object as argument, which will be transformed and returned + to the Reader. + The transformations can be part of the :mod:`~MDAnalysis.transformations` + module, or created by the user, and are stored as a list `transformations`. + This list can only be modified once, and further calls of this function will + raise an exception. + + .. code-block:: python + + u = MDAnalysis.Universe(topology, coordinates) + workflow = [some_transform, another_transform, this_transform] + u.trajectory.add_transformations(*workflow) + + Parameters + ---------- + transform_list : list + list of all the transformations that will be applied to the coordinates + + See Also + -------- + :mod:`MDAnalysis.transformations` + """ + #Overrides :meth:`~MDAnalysis.coordinates.base.ProtoReader.add_transformations` + #to avoid unintended behaviour where the coordinates of each frame are transformed + #multiple times when iterating over the trajectory. + #In this method, the trajectory is modified all at once and once only. + + super(MemoryReader, self).add_transformations(*transformations) + for i, ts in enumerate(self): + for transform in self.transformations: + ts = transform(ts) + + def _apply_transformations(self, ts): + """ Applies the transformations to the timestep.""" + # Overrides :meth:`~MDAnalysis.coordinates.base.ProtoReader.add_transformations` + # to avoid applying the same transformations multiple times on each frame + + return ts diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index a71ffae184f..f31bbd01d11 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -213,6 +213,10 @@ class Universe(object): Universe to only unpickle if a compatible Universe with matching *anchor_name* is found. Even if *anchor_name* is set *is_anchor* will still be honored when unpickling. + transformations: function or list, optional + Provide a list of transformations that you wish to apply to the + trajectory upon reading. Transformations can be found in + :mod:`MDAnalysis.transformations`, or can be user-created. in_memory After reading in the trajectory, transfer it to an in-memory representations, which allow for manipulation of coordinates. @@ -321,7 +325,13 @@ def __init__(self, *args, **kwargs): if not coordinatefile: coordinatefile = None + self.load_new(coordinatefile, **kwargs) + # parse transformations + trans_arg = kwargs.pop('transformations', None) + if trans_arg: + transforms =[trans_arg] if callable(trans_arg) else trans_arg + self.trajectory.add_transformations(*transforms) # Check for guess_bonds if kwargs.pop('guess_bonds', False): @@ -333,6 +343,7 @@ def __init__(self, *args, **kwargs): self._anchor_name = kwargs.get('anchor_name', None) # Universes are anchors by default self.is_anchor = kwargs.get('is_anchor', True) + def copy(self): """Return an independent copy of this Universe""" diff --git a/package/MDAnalysis/transformations/__init__.py b/package/MDAnalysis/transformations/__init__.py new file mode 100644 index 00000000000..eaf3d0a74eb --- /dev/null +++ b/package/MDAnalysis/transformations/__init__.py @@ -0,0 +1,87 @@ +# -*- 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. +# +# 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 +# + + +"""\ +Trajectory transformations --- :mod:`MDAnalysis.transformations` +================================================================= + +The transformations submodule contains a collection of functions to modify the +trajectory. Coordinate transformations, such as PBC corrections and molecule fitting +are often required for some analyses and visualization, and the functions in this +module allow transformations to be applied on-the-fly. +These transformation functions can be called by the user for any given +timestep of the trajectory, added as a workflow using :meth:`add_transformations` +of the :mod:`~MDAnalysis.coordinates.base` module, or upon Universe creation using +the keyword argument `transformations`. Note that in the two latter cases, the +workflow cannot be changed after being defined. + +In addition to the specific arguments that each transformation can take, they also +contain a wrapped function that takes a `Timestep` object as argument. +So, a transformation can be roughly defined as follows: + + def transformations(*args,**kwargs): + # do some things + def wrapped(ts): + # apply changes to the Timestep object + return ts + + return wrapped + + +See `MDAnalysis.transformations.translate` for a simple example. + +Currently implemented transformations are: + + - translate: translate the coordinates of a given trajectory frame by a given vector. + + + +Examples +-------- + +e.g. translate the coordinates of a frame: + + u = MDAnalysis.Universe(topology, trajectory) + new_ts = MDAnalysis.transformations.translate([1,1,1])(u.trajectory.ts) + +e.g. create a workflow and adding it to the trajectory: + + u = MDAnalysis.Universe(topology, trajectory) + workflow = [MDAnalysis.transformations.translate([1,1,1]), + MDAnalysis.transformations.translate([1,2,3])] + u.trajetory.add_transformations(*workflow) + +e.g. giving a workflow as a keyword argument when defining the universe: + + workflow = [MDAnalysis.transformations.translate([1,1,1]), + MDAnalysis.transformations.translate([1,2,3])] + u = MDAnalysis.Universe(topology, trajectory, transformations = *workflow) + + + +""" +from __future__ import absolute_import + +from .translate import translate + + diff --git a/package/MDAnalysis/transformations/translate.py b/package/MDAnalysis/transformations/translate.py new file mode 100644 index 00000000000..7f2466c7297 --- /dev/null +++ b/package/MDAnalysis/transformations/translate.py @@ -0,0 +1,67 @@ +# -*- 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. +# +# 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 +# + +"""\ +Translate trajectory --- :mod:`MDAnalysis.transformations.translate` +==================================================================== + +Translate the coordinates of a given trajectory by a given vector. +This is a wrapped function so usage is as the following example: + + ts = MDAnalysis.transformations.translate(vector)(timestep) + +""" +from __future__ import absolute_import + +import numpy as np + +def translate(vector): + """ + Translates the coordinates of a given :class:`~MDAnalysis.coordinates.base.Timestep` + instance by a given vector. + + Example + ------- + ts = MDAnalysis.transformations.translate([1,2,3])(ts) + + Parameters + ---------- + vector: list + coordinates of the vector to which the coordinates will be translated + ts: Timestep + frame that will be transformed + + Returns + ------- + :class:`~MDAnalysis.coordinates.base.Timestep` object + + """ + def wrapped(ts): + if len(vector)>2: + v = np.float32(vector) + ts.positions += v + else: + raise ValueError("{} vector in too short".format(vector)) + + return ts + + return wrapped diff --git a/testsuite/MDAnalysisTests/coordinates/base.py b/testsuite/MDAnalysisTests/coordinates/base.py index 3ceb2070c82..976caca3476 100644 --- a/testsuite/MDAnalysisTests/coordinates/base.py +++ b/testsuite/MDAnalysisTests/coordinates/base.py @@ -30,6 +30,7 @@ import MDAnalysis as mda from MDAnalysis.coordinates.base import Timestep +from MDAnalysis.transformations import translate from MDAnalysis import NoDataError from MDAnalysis.lib.mdamath import triclinic_vectors @@ -208,7 +209,14 @@ def reader(ref): 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])) + return transformed + def test_n_atoms(self, ref, reader): assert_equal(reader.n_atoms, ref.n_atoms) @@ -338,7 +346,83 @@ def test_stop_iter(self, reader): for ts in reader[:-1]: pass assert_equal(reader.frame, 0) - + + 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)) + for i, ts in enumerate(transformed): + idealcoords = ref.iter_ts(i).positions + v1 + v2 + 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=[] + 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) + + 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]): + idealcoords = ref.iter_ts(ts.frame).positions + v1 + v2 + 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)) + 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) + second_ideal = ref.iter_ts(1).positions + v1 + v2 + 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) + + # How about we switch the frame to itself? + 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) + + 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)) + ideal_coords = ref.iter_ts(0).positions + v1 + v2 + transformed.rewind() + assert_array_almost_equal(transformed[0].positions, ideal_coords, decimal = ref.prec) + + 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)) + new = transformed.copy() + 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) + + + 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])) class MultiframeReaderTest(BaseReaderTest): def test_last_frame(self, ref, reader): diff --git a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py index 7cccf375053..39d4da9457b 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_chainreader.py +++ b/testsuite/MDAnalysisTests/coordinates/test_chainreader.py @@ -30,6 +30,7 @@ from numpy.testing import (assert_equal, assert_almost_equal, assert_array_almost_equal) import MDAnalysis as mda +from MDAnalysis.transformations import translate from MDAnalysisTests.datafiles import (PDB, PSF, CRD, DCD, GRO, XTC, TRR, PDB_small, PDB_closed) @@ -41,6 +42,12 @@ class TestChainReader(object): def universe(self): 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])]) def test_next_trajectory(self, universe): universe.trajectory.rewind() @@ -115,6 +122,46 @@ def test_write_dcd(self, universe, tmpdir): self.prec, err_msg="Coordinates disagree at frame {0:d}".format( ts_orig.frame)) + + def test_transform_iteration(self, universe, transformed): + 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) + # iterate again: + for ts in transformed.trajectory: + 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? + 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) + + def test_transform_switch(self, universe, transformed): + 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) + # now switch to another frame + newref = universe.trajectory[10].positions + vector + 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 + transformed.trajectory.rewind() + assert_almost_equal(transformed.trajectory.ts.positions, ref, decimal = 6) class TestChainReaderCommonDt(object): common_dt = 100.0 diff --git a/testsuite/MDAnalysisTests/coordinates/test_gro.py b/testsuite/MDAnalysisTests/coordinates/test_gro.py index 14e7199fab6..f0a7e5ef2fa 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_gro.py +++ b/testsuite/MDAnalysisTests/coordinates/test_gro.py @@ -23,6 +23,7 @@ import MDAnalysis as mda import numpy as np from MDAnalysis.coordinates.GRO import GROReader, GROWriter +from MDAnalysis.transformations import translate from MDAnalysisTests import make_Universe, tempdir from MDAnalysisTests.coordinates.base import ( BaseReference, BaseReaderTest, BaseWriterTest, BaseTimestepTest, @@ -290,6 +291,13 @@ def reader(ref): dt=ref.aux_highf_dt, initial_time=0, time_selector=None) return reader + + @staticmethod + @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])) + return transformed class TestGROWriterNoConversion(BaseWriterTest): diff --git a/testsuite/MDAnalysisTests/coordinates/test_reader_api.py b/testsuite/MDAnalysisTests/coordinates/test_reader_api.py index 840665d72d9..6f236b3e2ac 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_reader_api.py +++ b/testsuite/MDAnalysisTests/coordinates/test_reader_api.py @@ -22,6 +22,7 @@ from __future__ import absolute_import import numpy as np +from collections import OrderedDict from MDAnalysis.coordinates.base import ( Timestep, SingleFrameReaderBase, @@ -43,6 +44,7 @@ def __init__(self, filename, **kwargs): self.n_frames = 10 self.n_atoms = 10 self._auxs = {} + self._transformations = [] # ts isn't a real timestep, but just an integer # whose value represents the frame number (0 based) self.ts = Timestep(self.n_atoms) diff --git a/testsuite/MDAnalysisTests/core/test_universe.py b/testsuite/MDAnalysisTests/core/test_universe.py index d002c56e5a8..d237d03f12d 100644 --- a/testsuite/MDAnalysisTests/core/test_universe.py +++ b/testsuite/MDAnalysisTests/core/test_universe.py @@ -55,6 +55,7 @@ import MDAnalysis as mda import MDAnalysis.coordinates from MDAnalysis.topology.base import TopologyReaderBase +from MDAnalysis.transformations import translate from MDAnalysisTests import assert_nowarns @@ -303,6 +304,21 @@ def test_chainid_quick_select(): assert len(u.C.atoms) == 5 assert len(u.D.atoms) == 7 +class TestTransformations(object): + """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) + 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) + assert_almost_equal(u.trajectory.ts.positions, ref, decimal=6) class TestGuessMasses(object): """Tests the Mass Guesser in topology.guessers