Skip to content
Merged
1 change: 1 addition & 0 deletions package/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Chronological list of authors
- William Glass
- Marcello Sega
- Edis Jakupovic
- Nicholas Craven

External code
-------------
Expand Down
8 changes: 6 additions & 2 deletions package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ The rules for this file:

------------------------------------------------------------------------------
??/??/?? tylerjereddy, richardjgowers, IAlibay, hmacdope, orbeckst, cbouy,
lilyminium, daveminh, jbarnoud, yuxuanzhuang, VOD555, ianmkenney
lilyminium, daveminh, jbarnoud, yuxuanzhuang, VOD555, ianmkenney,
calcraven

* 2.0.0

Fixes
* Fixed reading in masses and charges from a hoomdxml file
(Issue #2888, PR #2889)
* Bond attribute is no longer set if PDB file contains no CONECT records
(Issue #2832)
* ResidueAttrs now have Atom as a target class by default; ICodes and
Expand All @@ -40,7 +43,8 @@ Fixes
was thrown when finding D-H pairs via the topology if `hydrogens` was an
empty AtomGroup (Issue #2848)
* Fixed the DMSParser, allowing the creation of multiple segids sharing
residues with identical resids (Issue #1387, PR #2872)
residues with identical resids (Issue #1387, PR #2872)
* H5MD files are now pickleable with H5PYPicklable (Issue #2890, PR #2894)

Enhancements
* Refactored analysis.helanal into analysis.helix_analysis
Expand Down
139 changes: 119 additions & 20 deletions package/MDAnalysis/coordinates/H5MD.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
interface of the HDF5 library to improve parallel reads and writes.

The HDF5 library and `h5py`_ must be installed; otherwise, H5MD files
cannot be read by MDAnalysis. If `h5py`_ is not installed, a
cannot be read by MDAnalysis. If `h5py`_ is not installed, a
:exc:`RuntimeError` is raised.

Units
Expand Down Expand Up @@ -94,10 +94,10 @@

Building parallel h5py and HDF5 on Linux
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Building a working parallel HDF5/h5py/mpi4py environment can be
Building a working parallel HDF5/h5py/mpi4py environment can be
challenging and is often specific to your local computing resources,
e.g., the supercomputer that you're running on typically already has
its preferred MPI installation. As a starting point we provide
its preferred MPI installation. As a starting point we provide
instructions that worked in a specific, fairly generic environment.

These instructions successfully built parallel HDF5/h5py
Expand Down Expand Up @@ -133,8 +133,8 @@
CC=mpicc HDF5_DIR=$HDF5_PATH python setup.py build
python setup.py install

If you have questions or want to share how you managed to build
parallel hdf5/h5py/mpi4py please let everyone know on the
If you have questions or want to share how you managed to build
parallel hdf5/h5py/mpi4py please let everyone know on the
`MDAnalysis forums`_.

.. _`H5MD`: https://nongnu.org/h5md/index.html
Expand Down Expand Up @@ -180,6 +180,9 @@

.. automethod:: H5MDReader._reopen

.. autoclass:: H5PYPicklable
:members:

"""

import numpy as np
Expand All @@ -190,6 +193,15 @@
import h5py
except ImportError:
HAS_H5PY = False

# Allow building documentation even if h5py is not installed
import types

class MockH5pyFile:
pass
h5py = types.ModuleType("h5py")
h5py.File = MockH5pyFile

else:
HAS_H5PY = True

Expand Down Expand Up @@ -228,23 +240,23 @@ class H5MDReader(base.ReaderBase):

See `h5md documentation <https://nongnu.org/h5md/h5md.html>`_
for a detailed overview of the H5MD file format.
The reader attempts to convert units in the trajectory file to
the standard MDAnalysis units (:mod:`MDAnalysis.units`) if

The reader attempts to convert units in the trajectory file to
the standard MDAnalysis units (:mod:`MDAnalysis.units`) if
`convert_units` is set to ``True``.

Additional data in the *observables* group of the H5MD file are
loaded into the :attr:`Timestep.data` dictionary.

Only 3D-periodic boxes or no periodicity are supported; for no
periodicity, :attr:`Timestep.dimensions` will return ``None``.

Although H5MD can store varying numbers of particles per time step
as produced by, e.g., GCMC simulations, MDAnalysis can currently
only process a fixed number of particles per step. If the number
of particles changes a :exc:`ValueError` is raised.

The :class:`H5MDReader` reads .h5md files with the following
The :class:`H5MDReader` reads .h5md files with the following
HDF5 hierarchy:

.. code-block:: text
Expand Down Expand Up @@ -277,21 +289,21 @@ class H5MDReader(base.ReaderBase):
\-- (position)
\-- [step] <int>, gives frame
\-- [time] <float>, gives time
+-- units <str>
+-- unit <str>
\-- [value] <float>, gives numpy arrary of positions
with shape (n_atoms, 3)
+-- unit <str>
\-- (velocity)
\-- [step] <int>, gives frame
\-- [time] <float>, gives time
+-- units <str>
+-- unit <str>
\-- [value] <float>, gives numpy arrary of velocities
with shape (n_atoms, 3)
+-- unit <str>
\-- (force)
\-- [step] <int>, gives frame
\-- [time] <float>, gives time
+-- units <str>
+-- unit <str>
\-- [value] <float>, gives numpy arrary of forces
with shape (n_atoms, 3)
+-- unit <str>
Expand Down Expand Up @@ -557,13 +569,15 @@ def open_trajectory(self):
if self._comm is not None:
# can only pass comm argument to h5py.File if driver='mpio'
assert self._driver == 'mpio'
self._file = h5py.File(self.filename, 'r', # pragma: no cover
driver=self._driver,
comm=self._comm)
self._file = H5PYPicklable(name=self.filename, # pragma: no cover
mode='r',
driver=self._driver,
comm=self._comm)
elif self._driver is not None:
self._file = h5py.File(self.filename, 'r', driver=self._driver)
self._file = H5PYPicklable(name=self.filename, mode='r',
driver=self._driver)
else:
self._file = h5py.File(self.filename, 'r')
self._file = H5PYPicklable(name=self.filename, mode='r')
# pulls first key out of 'particles'
# allows for arbitrary name of group1 in 'particles'
self._particle_group = self._file['particles'][
Expand Down Expand Up @@ -727,3 +741,88 @@ def has_forces(self):
@has_forces.setter
def has_forces(self, value: bool):
self._has['force'] = value

def __getstate__(self):
state = self.__dict__.copy()
del state['_particle_group']
return state

def __setstate__(self, state):
self.__dict__ = state
self._particle_group = self._file['particles'][
list(self._file['particles'])[0]]
self[self.ts.frame]


class H5PYPicklable(h5py.File):
"""H5PY file object (read-only) that can be pickled.

This class provides a file-like object (as returned by
:class:`h5py.File`) that,
unlike standard Python file objects,
can be pickled. Only read mode is supported.

When the file is pickled, filename, mode, driver, and comm of
:class:`h5py.File` in the file are saved. On unpickling, the file
is opened by filename, mode, driver. This means that for a successful
unpickle, the original file still has to be accessible with its filename.

Parameters
----------
filename : str or file-like
a filename given a text or byte string.
driver : str (optional)
H5PY file driver used to open H5MD file

Example
-------
::

f = H5PYPicklable('filename', 'r')
print(f['particles/trajectory/position/value'][0])
f.close()

can also be used as context manager::

with H5PYPicklable('filename', 'r'):
print(f['particles/trajectory/position/value'][0])

Note
----
Pickling of an `h5py.File` opened with `driver="mpio"` and an MPI
communicator is currently not supported

See Also
---------
:class:`MDAnalysis.lib.picklable_file_io.FileIOPicklable`
:class:`MDAnalysis.lib.picklable_file_io.BufferIOPicklable`
:class:`MDAnalysis.lib.picklable_file_io.TextIOPicklable`
:class:`MDAnalysis.lib.picklable_file_io.GzipPicklable`
:class:`MDAnalysis.lib.picklable_file_io.BZ2Picklable`


.. versionadded:: 2.0.0
"""

def __getstate__(self):
driver = self.driver
# Current issues: Need a way to retrieve MPI communicator object
# from self and pickle MPI.Comm object. Parallel driver is excluded
# from test because h5py calls for an MPI configuration when driver is
# 'mpio', so this will need to be patched in the test function.
if driver == 'mpio': # pragma: no cover
raise TypeError("Parallel pickling of `h5py.File` with" # pragma: no cover
" 'mpio' driver is currently not supported.")

return {'name': self.filename,
'mode': self.mode,
'driver': driver}

def __setstate__(self, state):
self.__init__(name=state['name'],
mode=state['mode'],
driver=state['driver'])

def __getnewargs__(self):
"""Override the h5py getnewargs to skip its error message"""
return ()
9 changes: 4 additions & 5 deletions package/MDAnalysis/topology/HoomdXMLParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ def parse(self, **kwargs):
pass
else:
attrs[attrname] = attr(np.array(vals, dtype=dtype))

for attrname, attr, in (
('bond', Bonds),
('angle', Angles),
Expand All @@ -142,10 +141,10 @@ def parse(self, **kwargs):
vals = []
attrs[attrname] = attr(vals)

if not 'masses' in attrs:
attrs['masses'] = Masses(np.zeros(natoms))
if not 'charges' in attrs:
attrs['charges'] = Charges(np.zeros(natoms, dtype=np.float32))
if 'mass' not in attrs:
attrs['mass'] = Masses(np.zeros(natoms))
if 'charge' not in attrs:
attrs['charge'] = Charges(np.zeros(natoms, dtype=np.float32))

attrs = list(attrs.values())

Expand Down
40 changes: 22 additions & 18 deletions package/MDAnalysis/transformations/rotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,35 +40,39 @@

def rotateby(angle, direction, point=None, ag=None, weights=None, wrap=False):
'''
Rotates the trajectory by a given angle on a given axis. The axis is defined by
Rotates the trajectory by a given angle on a given axis. The axis is defined by
the user, combining the direction vector and a point. This point can be the center
of geometry or the center of mass of a user defined AtomGroup, or an array defining
of geometry or the center of mass of a user defined AtomGroup, or an array defining
custom coordinates.

Examples
--------
e.g. rotate the coordinates by 90 degrees on a axis formed by the [0,0,1] vector and

e.g. rotate the coordinates by 90 degrees on a axis formed by the [0,0,1] vector and
the center of geometry of a given AtomGroup:

.. code-block:: python


from MDAnalysis import transformations

ts = u.trajectory.ts
angle = 90
ag = u.atoms()
ag = u.atoms
d = [0,0,1]
rotated = MDAnalysis.transformations.rotate(angle, direction=d, ag=ag)(ts)
rotated = transformations.rotate.rotateby(angle, direction=d, ag=ag)(ts)

e.g. rotate the coordinates by a custom axis:

.. code-block:: python

from MDAnalysis import transformations

ts = u.trajectory.ts
angle = 90
p = [1,2,3]
d = [0,0,1]
rotated = MDAnalysis.transformations.rotate(angle, direction=d, point=point)(ts)
rotated = transformations.rotate.rotateby(angle, direction=d, point=p)(ts)

Parameters
----------
angle: float
Expand Down Expand Up @@ -97,7 +101,7 @@ def rotateby(angle, direction, point=None, ag=None, weights=None, wrap=False):
Returns
-------
MDAnalysis.coordinates.base.Timestep

Warning
-------
Wrapping/unwrapping the trajectory or performing PBC corrections may not be possible
Expand Down Expand Up @@ -132,7 +136,7 @@ def rotateby(angle, direction, point=None, ag=None, weights=None, wrap=False):
center_method = partial(atoms.center, weights, pbc=wrap)
else:
raise ValueError('A point or an AtomGroup must be specified')

def wrapped(ts):
if point is None:
position = center_method()
Expand All @@ -143,8 +147,8 @@ def wrapped(ts):
translation = matrix[:3, 3]
ts.positions= np.dot(ts.positions, rotation)
ts.positions += translation

return ts

return wrapped

Loading