Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure NoJump handles jumps that occur on the second frame in NPT trajectories #4258

Merged
merged 42 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
cf412d9
Ensure atoms are unwrapped correctly when they jump across boundaries…
p-j-smith Aug 26, 2023
0a835ad
Add testfiles for nojump transformation
p-j-smith Aug 26, 2023
4efdf20
Add tests for nojump transformation where the jump occurs at the seco…
p-j-smith Aug 26, 2023
4a2b8a5
Remove unncessary second assignment of L in NoJump initialisation
p-j-smith Aug 26, 2023
7245bd1
Temporarily put test datafiles in the test directory
p-j-smith Aug 26, 2023
1dae25a
Add transformation to set variable box dimensions
p-j-smith Aug 26, 2023
0ed530e
Use set_variable_dimensions transformation in test for NoJump
p-j-smith Aug 26, 2023
66ccb4a
Remove the test datafiles for the npt NoJump
p-j-smith Aug 26, 2023
c3e8366
Import set_variable_dimensions in the transformation sub-package
p-j-smith Aug 26, 2023
58a7567
Fix typos in docsttring of set_variable_dimensions transformation
p-j-smith Aug 26, 2023
6f02285
remove unncessary comments in docstring of npt tests for nojump
p-j-smith Aug 26, 2023
7d90706
Add tests for set_variable_dimensions transformation
p-j-smith Aug 27, 2023
1b94f84
Add test to check that set_variable_dimensions raises an error when d…
p-j-smith Aug 27, 2023
970663a
Tidy formatting of set_variable_dimensions test functions
p-j-smith Aug 27, 2023
7ffaa2e
Add docs for set_variable_dimensions
p-j-smith Aug 27, 2023
21fdf0a
Add versionchanged note to NoJump about adding 'ag' argument
p-j-smith Aug 27, 2023
aa86f88
Add test to check if the nojump transform always returns the correct …
p-j-smith Aug 27, 2023
55d3c34
ensure coordinates can be unwrapped correctly when iterating
p-j-smith Aug 27, 2023
18d2bfc
Use tmp_path_factory to write tmp trajectory for testing NoJump repea…
p-j-smith Aug 27, 2023
a786599
Fix typo in boxdimensions module docstring
p-j-smith Aug 28, 2023
8e06cf3
Fix typo in docstring of NoJump test
p-j-smith Aug 28, 2023
ae8cef2
remove unnecessary second calculation of Linverse from NoJump
p-j-smith Aug 28, 2023
7ffc77a
Add a versionadded note to the set_variable_dimensions docstring
p-j-smith Aug 28, 2023
243c9f0
Update CHANGELOG
p-j-smith Aug 28, 2023
d673021
Remove added set_variable_dimensions class
p-j-smith Aug 28, 2023
e2e0052
Update tests to reflect removal of set_variable_dimensions transforma…
p-j-smith Aug 28, 2023
206a51f
Update CHANGELOG
p-j-smith Aug 28, 2023
296cd6a
Update NoJump tests to reflect removal of set_variable_dimensions tra…
p-j-smith Aug 28, 2023
90c037a
Remove unnecessary whitespace from NoJump tests
p-j-smith Aug 28, 2023
21ba7c3
Use numpy array in docstring example of set_dimensions transformation
p-j-smith Aug 28, 2023
8b3dfaf
Update changelog
p-j-smith Aug 28, 2023
44e473a
Use ValueError rather than NoDataError in set_dimensions
p-j-smith Aug 28, 2023
0d51b3d
Set NoJump.older_frame to -1 when ts.frame == 0.
p-j-smith Aug 28, 2023
2a2d100
Add comment to explain how we ensure jumps on the second frame are ha…
p-j-smith Aug 28, 2023
d735d71
Make flake8 happy
p-j-smith Aug 28, 2023
7f87344
Remove unnecessary if-block 'if self.prev is None:' from NoJump
p-j-smith Aug 29, 2023
35dc77c
Add test to check warning is emitted by NoJump when iterating traject…
p-j-smith Aug 29, 2023
cd339b6
Merge branch 'develop' into fix/no-jump-first-frame
IAlibay Aug 30, 2023
b1c32dc
Fix formatting of set_dimensions docstring
p-j-smith Aug 31, 2023
9a24312
Merge branch 'develop' into fix/no-jump-first-frame
p-j-smith Sep 1, 2023
8926e4a
Merge branch 'develop' into fix/no-jump-first-frame
IAlibay Sep 2, 2023
de33028
Merge remote-tracking branch 'upstream/develop' into fix/no-jump-firs…
hmacdope Sep 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ The rules for this file:
* release numbers follow "Semantic Versioning" http://semver.org

------------------------------------------------------------------------------
??/??/?? IAlibay, hmacdope, pillose, jaclark5, tylerjereddy
??/??/?? IAlibay, hmacdope, pillose, jaclark5, tylerjereddy, p-j-smith

* 2.7.0

Fixes
* Fix `NoJump`` unwrapping for jumps on the second frame in a
IAlibay marked this conversation as resolved.
Show resolved Hide resolved
trajectory (PR #4258, Issue #4257)
* Fix Atom type guessing error (PR #4168, Issue #4167)

Enhancements
* Allow `set_dimensions` transformation to handle NPT trajectories (PR #4258)

Changes
* NumPy `in1d` replaced with `isin` in preparation for NumPy
Expand Down
48 changes: 36 additions & 12 deletions package/MDAnalysis/transformations/boxdimensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,63 @@
Set box dimensions --- :mod:`MDAnalysis.transformations.boxdimensions`
=======================================================================

Set dimensions of the simulation box to a constant vector across all timesteps.

Set dimensions of the simulation box, either to a constant vector across
all timesteps or to a specified vector at each frame.

.. autoclass:: set_dimensions
"""
import numpy as np

from .base import TransformationBase


class set_dimensions(TransformationBase):
"""
Set simulation box dimensions.

Timestep dimensions are modified in place.

Example
-------
Examples
--------

e.g. set simulation box dimensions to a vector containing unit cell
dimensions [*a*, *b*, *c*, *alpha*, *beta*, *gamma*], lengths *a*,
*b*, *c* are in the MDAnalysis length unit (Å), and angles are in degrees.
The same dimensions will be used for every frame in the trajectory.

.. code-block:: python

dim = [2, 2, 2, 90, 90, 90]
dim = np.array([2, 2, 2, 90, 90, 90])
transform = mda.transformations.boxdimensions.set_dimensions(dim)
u.trajectory.add_transformations(transform)

Or e.g. set simulation box dimensions to a vector containing unit cell
dimensions [*a*, *b*, *c*, *alpha*, *beta*, *gamma*] at the first frame,
and [*2a*, *2b*, *2c*, *alpha*, *beta*, *gamma*] at the second frame.

.. code-block:: python

dim = np.array([
[2, 2, 2, 90, 90, 90],
[4, 4, 4, 90, 90, 90],
])
transform = mda.transformations.boxdimensions.set_dimensions(dim)
u.trajectory.add_transformations(transform)

Parameters
----------
dimensions: iterable of floats
dimensions: iterable of floats or two-dimensional np.typing.NDArrayLike
vector that contains unit cell lengths and angles.
Expected shapes are (6, 0) or (1, 6)
Expected shapes are (6, 0) or (1, 6) or (N, 6), where
N is the number of frames in the trajectory. If shape is (6, 0) or
(1, 6), the same dimensions will be used at every frame in the
trajectory.

Returns
-------
:class:`~MDAnalysis.coordinates.timestep.Timestep` object

.. versionchanged:: 2.7.0
p-j-smith marked this conversation as resolved.
Show resolved Hide resolved
Added the option to set varying box dimensions (i.e. an NPT trajectory).
"""

def __init__(self,
Expand All @@ -80,15 +99,20 @@ def __init__(self,
'np.float32 numpy.ndarray')
raise ValueError(errmsg)
try:
self.dimensions = self.dimensions.reshape(6, )
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, ), '
' containing 3 lengths and 3 angles: '
'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)

def _transform(self, ts):
ts.dimensions = self.dimensions
try:
ts.dimensions = self.dimensions[0] if self.dimensions.shape[0] == 1 else self.dimensions[ts.frame]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is both a long line and also a bit hard to read (at least to me), splitting this over a few lines probably would be worth it?

except IndexError as e:
raise ValueError(f"Dimensions array has no data for frame {ts.frame}") from e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise ValueError(f"Dimensions array has no data for frame {ts.frame}") from e
errmsg = f"Dimensions array has no data for frame {ts.frame}"
raise ValueError(errmsg) from e

I know it's annoying but pep8 - note we don't care about the black linter stuff, but the flake8 errors from here probably should be addressed: https://github.com/MDAnalysis/mdanalysis/actions/runs/5997923144/job/16265248319

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry I forgot to run flake8 locally, fixed now! Perhaps adding a pre-commit config for people to optionally use would be useful?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-commit config for people to optionally use would be useful?

It's.. complicated, I'll leave it as that for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha! okay :)

return ts
11 changes: 11 additions & 0 deletions package/MDAnalysis/transformations/nojump.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ def _transform(self, ts):
except np.linalg.LinAlgError:
msg = f"Periodic box dimensions are not invertible at step {ts.frame}"
raise NoDataError(msg)
hmacdope marked this conversation as resolved.
Show resolved Hide resolved
if ts.frame == 0:
IAlibay marked this conversation as resolved.
Show resolved Hide resolved
# We don't need to apply the transformation here. However, we need to ensure we
# have the 0th frame coordinates in reduced form. We also need to set an
# an appropriate value for self.older frame. This is so that on the following frame
# we don't return early when we check `self.older_frame != "A"`. If we return early,
# then the transformation is not applied, and any jumps across boundaries that occur
# at that frame will not be accounted for.
self.prev = ts.positions @ Linverse
self.old_frame = 0
self.older_frame = -1
return ts
if self.prev is None:
self.prev = ts.positions @ Linverse
self.old_frame = ts.frame
Expand Down
1 change: 0 additions & 1 deletion testsuite/MDAnalysisTests/datafiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,5 @@
SURFACE_PDB = (_data_ref / 'surface.pdb.bz2').as_posix()
SURFACE_TRR = (_data_ref / 'surface.trr').as_posix()


# This should be the last line: clean up namespace
del resources
48 changes: 47 additions & 1 deletion testsuite/MDAnalysisTests/transformations/test_boxdimensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ def boxdimensions_universe():
return new_u


@pytest.fixture()
def variable_boxdimensions_universe():
# create Universe objects for tests
n_atoms = 1
n_frames = 3
u = mdanalysis.Universe.empty(n_atoms, trajectory=True)
coordinates = np.zeros((n_frames, u.atoms.n_atoms, 3))
u.load_new(coordinates, order="fac")
return u


def test_boxdimensions_dims(boxdimensions_universe):
new_dims = np.float32([2, 2, 2, 90, 90, 90])
set_dimensions(new_dims)(boxdimensions_universe.trajectory.ts)
Expand Down Expand Up @@ -72,7 +83,6 @@ def test_dimensions_vector_asarray(boxdimensions_universe,
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])
Expand All @@ -81,3 +91,39 @@ def test_dimensions_transformations_api(boxdimensions_universe):
for ts in boxdimensions_universe.trajectory:
assert_array_almost_equal(boxdimensions_universe.dimensions,
new_dims, decimal=6)


def test_varying_dimensions_transformations_api(
variable_boxdimensions_universe,
):
"""
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],
])
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)

p-j-smith marked this conversation as resolved.
Show resolved Hide resolved

def test_varying_dimensions_no_data(
variable_boxdimensions_universe,
):
"""
Check error is raised when dimensions are missing for a frame
in a trajectory.
"""
# trjactory has three frames
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)
164 changes: 164 additions & 0 deletions testsuite/MDAnalysisTests/transformations/test_nojump.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,90 @@ def nojump_constantvel_universe():
return reference

p-j-smith marked this conversation as resolved.
Show resolved Hide resolved

@pytest.fixture
def nojump_universe_npt_2nd_frame():
"""
Create a Universe in which a single atom jumps across the periodic boundary in the
x-dimensions at the second frame.

Unwrapped coordinates should all be 97.5.
"""
n_atoms = 1
n_frames = 4
u = mda.Universe.empty(n_atoms, trajectory=True)
coordinates = np.empty((n_frames, u.atoms.n_atoms, 3))
coordinates[0] = [97.5, 50.0, 50.0]
coordinates[1] = [2.5, 50.0, 50.0]
coordinates[2] = [2.5, 50.0, 50.0]
coordinates[3] = [2.5, 50.0, 50.0]
u.load_new(coordinates, order="fac")
return u

p-j-smith marked this conversation as resolved.
Show resolved Hide resolved

@pytest.fixture
def nojump_universe_npt_3rd_frame():
"""
Create a Universe in which a single atom jumps across the periodic boundary in the
x-dimensions at the third frame.

Unwrapped coordinates should all be 97.5.
"""
n_atoms = 1
n_frames = 4
u = mda.Universe.empty(n_atoms, trajectory=True)
coordinates = np.empty((n_frames, u.atoms.n_atoms, 3))
coordinates[0] = [97.5, 50.0, 50.0]
coordinates[1] = [97.5, 50.0, 50.0]
coordinates[2] = [2.5, 50.0, 50.0]
coordinates[3] = [2.5, 50.0, 50.0]
u.load_new(coordinates, order="fac")
return u


@pytest.fixture(scope="module")
def nojump_universe_npt_2nd_frame_from_file(tmp_path_factory):
"""
Write the `nojump_universe_npt_2nd_frame` fixture to file, read it in and
return the Universe.

Used for testing that coordinates can be unwrapped correctly when iterating
over the trajectory multiple times.

We can't use an in-memory trajectory to test this because the
transformation would only be applied once.

Note, we use `tmp_path_factory` because this fixture requies `module` scope
so we can read the file after the fixture has been created, and
`tmp_path` has function-level scope.
"""
n_atoms = 1
n_frames = 4
u = mda.Universe.empty(n_atoms, trajectory=True)
coordinates = np.empty((n_frames, u.atoms.n_atoms, 3))
coordinates[0] = [97.5, 50.0, 50.0]
coordinates[1] = [2.5, 50.0, 50.0]
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 at the second frame
[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()
u.atoms.write(tmp_pdb)
with mda.Writer(tmp_xtc) as f:
for ts in u.trajectory:
f.write(u.atoms)
return mda.Universe(tmp_pdb, tmp_xtc)


def test_nojump_orthogonal_fwd(nojump_universe):
"""
Test if the nojump transform is returning the correct
Expand Down Expand Up @@ -122,6 +206,86 @@ def test_nojump_constantvel(nojump_constantvel_universe):
)

p-j-smith marked this conversation as resolved.
Show resolved Hide resolved

def test_nojump_2nd_frame(nojump_universe_npt_2nd_frame):
"""
Test if the nojump transform returns the correct values
at all frames when iterating over an npt trajectory
and an atom crosses the x-boundary at the second frame

Wrapped coordinates are:
coordinates = [
[97.5, 50.0, 50.0],
[2.5, 50.0, 50.0],
[2.5, 50.0, 50.0],
[2.5, 50.0, 50.0],
]
where each row is a different timestep.

Unwrapped coordinates are the same at each 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 at the second frame
[95, 100, 100, 90, 90, 90],
[95, 100, 100, 90, 90, 90],
])
workflow = [
mda.transformations.boxdimensions.set_dimensions(dim),
NoJump(),
]
u.trajectory.add_transformations(*workflow)
x_position = 97.5
np.testing.assert_allclose(u.trajectory.timeseries()[:, 0, 0], x_position)

p-j-smith marked this conversation as resolved.
Show resolved Hide resolved

def test_nojump_3rd_frame(nojump_universe_npt_3rd_frame):
"""
Test if the nojump transform returns the correct values
at all frames when iterating over an npt trajectory
and an atom crosses the x-boundary at the third frame.

Wrapped coordinates are:
coordinates = [
[97.5, 50.0, 50.0],
[97.5, 50.0, 50.0],
[2.5, 50.0, 50.0],
[2.5, 50.0, 50.0],
]
where each row is a different timestep.

Unwarpped coordinates are the same at each 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 at the third frame
[95, 100, 100, 90, 90, 90],
])
workflow = [
mda.transformations.boxdimensions.set_dimensions(dim),
NoJump(),
]
u.trajectory.add_transformations(*workflow)
x_position = 97.5
np.testing.assert_allclose(u.trajectory.timeseries()[:, 0, 0], x_position)


def test_nojump_iterate_twice(nojump_universe_npt_2nd_frame_from_file):
"""
Test if the nojump transform always returns the correct values
at all frames when iterating over multiple times.
"""
u = 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)


def test_nojump_constantvel_skip(nojump_universes_fromfile):
"""
Test if the nojump transform warning is emitted.
Expand Down