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

IMDv3 Integration #4694

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
289 changes: 289 additions & 0 deletions package/MDAnalysis/coordinates/IMD.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
"""

Example: Streaming an IMD v3 trajectory
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

To stream a trajectory from GROMACS or another simulation engine that supports
IMD v3, ensure that the simulation engine is running and waiting for an IMD connection.

For example, in GROMACS, you can use ``gmx mdrun`` with the ``-imdwait`` flag
to ensure that GROMACS will wait for a client before starting the simulation.
In GROMACS, you will know that the simulation is ready and waiting for the
MDAnalysis IMDReader client when this line is printed to the terminal:

.. code-block:: none

IMD: Will wait until I have a connection and IMD_GO orders.

Once the simulation is ready for a client connection, setup your :class:`Universe`
like this: ::

import MDAnalysis as mda
# Pass host and port of the listening GROMACS simulation
# server as the trajectory argument
u = mda.Universe("topology.tpr", "localhost:8888")

Classes
^^^^^^^

.. autoclass:: IMDReader
:members:
:inherited-members:

"""

import queue
from MDAnalysis.coordinates.base import (
ReaderBase,
FrameIteratorIndices,
FrameIteratorAll,
FrameIteratorSliced,
)
from MDAnalysis.coordinates import core
from MDAnalysis.lib.util import store_init_arguments

import imdclient
from imdclient.utils import parse_host_port
import numpy as np
import logging
import warnings
from typing import Optional
import numbers

logger = logging.getLogger("imdclient.IMDClient")


class IMDReader(ReaderBase):
"""
Reader for IMD protocol packets.
"""

format = "IMD"

@store_init_arguments
def __init__(
self,
filename,
convert_units=True,
n_atoms=None,
**kwargs,
):
"""
Parameters
----------
filename : a string of the form "host:port" where host is the hostname
or IP address of the listening GROMACS server and port
is the port number.
n_atoms : int (optional)
number of atoms in the system. defaults to number of atoms
in the topology. don't set this unless you know what you're doing.
"""
self._init_scope = True
self._reopen_called = False

super(IMDReader, self).__init__(filename, **kwargs)

logger.debug("IMDReader initializing")

if n_atoms is None:
raise ValueError("IMDReader: n_atoms must be specified")

Check warning on line 89 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L89

Added line #L89 was not covered by tests
self.n_atoms = n_atoms

host, port = parse_host_port(filename)

# This starts the simulation
self._imdclient = imdclient.IMDClient(host, port, n_atoms, **kwargs)

imdsinfo = self._imdclient.get_imdsessioninfo()
# NOTE: after testing phase, fail out on IMDv2

self.ts = self._Timestep(
self.n_atoms,
positions=imdsinfo.positions,
velocities=imdsinfo.velocities,
forces=imdsinfo.forces,
**self._ts_kwargs,
)

self._frame = -1

try:
self._read_next_timestep()
except StopIteration:
raise RuntimeError("IMDReader: No data found in stream")

Check warning on line 113 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L112-L113

Added lines #L112 - L113 were not covered by tests

def _read_next_timestep(self):
# No rewinding- to both load the first frame after __init__
# and access it again during iteration, we need to store first ts in mem
if not self._init_scope and self._frame == -1:
self._frame += 1
# can't simply return the same ts again- transformations would be applied twice
# instead, return the pre-transformed copy
return self._first_ts

return self._read_frame(self._frame + 1)

def _read_frame(self, frame):

try:
imdf = self._imdclient.get_imdframe()
except EOFError:
# Not strictly necessary, but for clarity
raise StopIteration

self._frame = frame
self._load_imdframe_into_ts(imdf)

if self._init_scope:
self._first_ts = self.ts.copy()
self._init_scope = False

logger.debug(f"IMDReader: Loaded frame {self._frame}")
return self.ts

def _load_imdframe_into_ts(self, imdf):
self.ts.frame = self._frame
if imdf.time is not None:
self.ts.time = imdf.time
# NOTE: timestep.pyx "dt" method is suspicious bc it uses "new" keyword for a float
self.ts.data["dt"] = imdf.dt
if imdf.energies is not None:
self.ts.data.update(imdf.energies)
if imdf.box is not None:
self.ts.dimensions = core.triclinic_box(*imdf.box)
if imdf.positions is not None:
# must call copy because reference is expected to reset
# see 'test_frame_collect_all_same' in MDAnalysisTests.coordinates.base
self.ts.positions = imdf.positions
if imdf.velocities is not None:
self.ts.velocities = imdf.velocities
if imdf.forces is not None:
self.ts.forces = imdf.forces

@property
def n_frames(self):
"""Changes as stream is processed unlike other readers"""
raise RuntimeError("IMDReader: n_frames is unknown")

Check warning on line 166 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L166

Added line #L166 was not covered by tests

def next(self):
"""Don't rewind after iteration. When _reopen() is called,
an error will be raised
"""
try:
ts = self._read_next_timestep()
except (EOFError, IOError):
# Don't rewind here like we normally would
raise StopIteration from None

Check warning on line 176 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L176

Added line #L176 was not covered by tests
else:
for auxname, reader in self._auxs.items():
ts = self._auxs[auxname].update_ts(ts)

ts = self._apply_transformations(ts)

return ts

def rewind(self):
"""Raise error on rewind"""
raise RuntimeError("IMDReader: Stream-based readers can't be rewound")

Check warning on line 187 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L187

Added line #L187 was not covered by tests

@staticmethod
def _format_hint(thing):
try:
parse_host_port(thing)
except:
return False
return True

Check warning on line 195 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L195

Added line #L195 was not covered by tests

def close(self):
"""Gracefully shut down the reader. Stops the producer thread."""
logger.debug("IMDReader close() called")
self._imdclient.stop()
# NOTE: removeme after testing
logger.debug("IMDReader shut down gracefully.")

# Incompatible methods
def copy(self):
raise NotImplementedError("IMDReader does not support copying")

def _reopen(self):
if self._reopen_called:
raise RuntimeError("IMDReader: Cannot reopen IMD stream")

Check warning on line 210 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L210

Added line #L210 was not covered by tests
self._frame = -1
self._reopen_called = True

def __getitem__(self, frame):
"""This method from ProtoReader must be overridden
to prevent slicing that doesn't make sense in a stream.
"""
raise RuntimeError("IMDReader: Trajectory can only be read in for loop")

Check warning on line 218 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L218

Added line #L218 was not covered by tests

def check_slice_indices(self, start, stop, step):
"""Check frame indices are valid and clip to fit trajectory.

The usage follows standard Python conventions for :func:`range` but see
the warning below.

Parameters
----------
start : int or None
Starting frame index (inclusive). ``None`` corresponds to the default
of 0, i.e., the initial frame.
stop : int or None
Last frame index (exclusive). ``None`` corresponds to the default
of n_frames, i.e., it includes the last frame of the trajectory.
step : int or None
step size of the slice, ``None`` corresponds to the default of 1, i.e,
include every frame in the range `start`, `stop`.

Returns
-------
start, stop, step : tuple (int, int, int)
Integers representing the slice

Warning
-------
The returned values `start`, `stop` and `step` give the expected result
when passed in :func:`range` but gives unexpected behavior when passed
in a :class:`slice` when ``stop=None`` and ``step=-1``

This can be a problem for downstream processing of the output from this
method. For example, slicing of trajectories is implemented by passing
the values returned by :meth:`check_slice_indices` to :func:`range` ::

range(start, stop, step)

and using them as the indices to randomly seek to. On the other hand,
in :class:`MDAnalysis.analysis.base.AnalysisBase` the values returned
by :meth:`check_slice_indices` are used to splice the trajectory by
creating a :class:`slice` instance ::

slice(start, stop, step)

This creates a discrepancy because these two lines are not equivalent::

range(10, -1, -1) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
range(10)[slice(10, -1, -1)] # []

"""
if start is not None:
raise ValueError(

Check warning on line 269 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L269

Added line #L269 was not covered by tests
"IMDReader: Cannot slice a stream, 'start' must be None"
)
if stop is not None:
raise ValueError(

Check warning on line 273 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L273

Added line #L273 was not covered by tests
"IMDReader: Cannot slice a stream, 'stop' must be None"
)
if step is not None:
if isinstance(step, numbers.Integral):
if step != 1:
raise ValueError(

Check warning on line 279 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L279

Added line #L279 was not covered by tests
"IMDReader: Cannot slice a stream, 'step' must be None or 1"
)

return start, stop, step

Check warning on line 283 in package/MDAnalysis/coordinates/IMD.py

View check run for this annotation

Codecov / codecov/patch

package/MDAnalysis/coordinates/IMD.py#L283

Added line #L283 was not covered by tests

def __getstate__(self):
raise NotImplementedError("IMDReader does not support pickling")

def __setstate__(self, state: object):
raise NotImplementedError("IMDReader does not support pickling")
3 changes: 2 additions & 1 deletion package/MDAnalysis/coordinates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ class can choose an appropriate reader automatically.
raw :class:`~MDAnalysis.coordinates.Timestep` objects.

"""
__all__ = ['reader', 'writer', 'timestep']
__all__ = ["reader", "writer", "timestep"]

from . import base
from . import timestep
Expand All @@ -770,6 +770,7 @@ class can choose an appropriate reader automatically.
from . import DMS
from . import GMS
from . import GRO
from . import IMD
from . import INPCRD
from . import LAMMPS
from . import MOL2
Expand Down
1 change: 1 addition & 0 deletions package/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
'waterdynamics',
'pathsimanalysis',
'mdahole2',
"imdclient @ git+https://github.com/ljwoods2/imdclient.git@main",
]
keywords = [
"python", "science", "chemistry", "biophysics", "molecular-dynamics",
Expand Down
Loading
Loading