Skip to content

feature: IMDReader Integration #4923

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

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions .github/actions/setup-deps/action.yaml
Original file line number Diff line number Diff line change
@@ -82,6 +82,8 @@ inputs:
default: 'seaborn>=0.7.0'
tidynamics:
default: 'tidynamics>=1.0.0'
imdclient:
default: 'imdclient'
Comment on lines +85 to +86
Copy link
Member

Choose a reason for hiding this comment

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

It's going to take time to get imdclient to a stage where the imdclient package does not actually affect MDAnalysis.

Is there a way that we could temporarily (for initial CI testing) install imdclient from a branch or tarball, e.g., in a pip section? Then we could fairly rapidly create a preliminary (unpublished) imdclient package without IMDReader.

Copy link
Member

Choose a reason for hiding this comment

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

By initial CI testing, do you mean "in this PR"?

There's a pip section just below, which should work if you put in the git location for pip install, but also you can just temporarily modify the CI script to do an additional pip install if it's for "testing within the PR itself".

If it's "after merge", this would require more discussion.

Copy link
Member

Choose a reason for hiding this comment

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

Just for right now to bootstrap the PR.

I don't want to merge without a solid conda-forge imdclient package in place.

# pip-installed min dependencies
coverage:
default: 'coverage'
@@ -137,6 +139,7 @@ runs:
${{ inputs.distopia }}
${{ inputs.gsd }}
${{ inputs.h5py }}
${{ inputs.imdclient }}
${{ inputs.hole2 }}
${{ inputs.joblib }}
${{ inputs.netcdf4 }}
1 change: 1 addition & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -77,6 +77,7 @@ jobs:
displayName: 'Install tools'
- script: >-
python -m pip install --only-binary=scipy,h5py
imdclient
cython
hypothesis
h5py>=2.10
1 change: 1 addition & 0 deletions maintainer/conda/environment.yml
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ dependencies:
- sphinxcontrib-bibtex
- mdaencore
- waterdynamics
- imdclient
- pip:
- mdahole2
- pathsimanalysis
215 changes: 215 additions & 0 deletions package/MDAnalysis/coordinates/IMD.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
"""
IMDReader --- :mod:`MDAnalysis.coordinates.IMD`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Read and analyze simulation data interactively using `IMDClient`_.
Copy link
Member

Choose a reason for hiding this comment

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

A bit more detail here for the documentation would be good.

Copy link
Member

Choose a reason for hiding this comment

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

Need a complete example people can follow

.. _IMDClient: https://github.com/Becksteinlab/imdclient
Units
-----
The units in IMDv3 are fixed.
.. list-table::
:widths: 10 10
:header-rows: 1
* - Measurement
- Unit
* - Length
- angstrom
* - Velocity
- angstrom/picosecond
* - Force
- kilojoules/(mol*angstrom)
* - Time
- picosecond
* - Energy
- kilojoules/mol
Classes
-------
.. autoclass:: IMDReader
:members:
:inherited-members:
"""

import numpy as np
import logging
import warnings

from MDAnalysis.coordinates import core
from MDAnalysis.lib.util import store_init_arguments
from MDAnalysis.coordinates.base import StreamReaderBase


from packaging.version import Version

MIN_IMDCLIENT_VERSION = Version("0.1.4")

try:
import imdclient
from imdclient.IMDClient import IMDClient
except ImportError:
HAS_IMDCLIENT = False
imdclient_version = Version("0.0.0")

# Allow building documentation without imdclient
import types

class MockIMDClient:
pass
imdclient = types.ModuleType("imdclient")
imdclient.IMDClient = MockIMDClient
imdclient.__version__ = "0.0.0"

else:
HAS_IMDCLIENT = True
imdclient_version = Version(imdclient.__version__)

# Check for compatibility: currently needs to be >=0.1.4
if imdclient_version < MIN_IMDCLIENT_VERSION:
warnings.warn(
f"imdclient version {imdclient_version} is too old; "
f"need at least {imdclient_version}, Your installed version of "
"imdclient will NOT be used.",
category=RuntimeWarning,
)
HAS_IMDCLIENT = False

logger = logging.getLogger("MDAnalysis.coordinates.IMDReader")


class IMDReader(StreamReaderBase):
Copy link
Member

@IAlibay IAlibay Feb 19, 2025

Choose a reason for hiding this comment

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

I think I'm getting confused about which PR is for which thing. @orbeckst given our discussion earlier this week, and your comment above which I take to be "IMDClient is still in flux", does it not make sense for the IMDReader to exist upstream and then just import it here? (edit: here my intent is "well then you could make releases and it wouldn't be limited to MDA release frequency").

Copy link
Member

Choose a reason for hiding this comment

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

We have to split IMDReader from imdclient and make a version of imdclient without IMDReader (which is in the works Becksteinlab/imdclient#54 ). At the same time we are moving what was split off into coordinates.IMD.

Amru is working on both at the moment.

The way IMDReader depends on imdclient is not the problem, and imdclient itself is also pretty stable, it's just that the tests for imdclient have made use of a lot of MDAnalysis/IMDReader for convenience, and we now have to rewrite some of these tests to use bare-bones python.

"""
Reader for IMD protocol packets.
Parameters
----------
filename : a string of the form "imd://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.
kwargs : dict (optional)
keyword arguments passed to the constructed :class:`IMDClient`
"""

format = "IMD"
one_pass = True

@store_init_arguments
def __init__(
self,
filename,
convert_units=True,
n_atoms=None,
**kwargs,
):
if not HAS_IMDCLIENT:
raise ImportError(
"IMDReader requires the imdclient package. "
"Please install it with 'pip install imdclient'."
)

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

self._imdclient = None
logger.debug("IMDReader initializing")

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

host, port = parse_host_port(filename)

# This starts the simulation
self._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 as e:
raise RuntimeError("IMDReader: No data found in stream") from e

def _read_frame(self, frame):

try:
imdf = self._imdclient.get_imdframe()
except EOFError as e:
raise e

self._frame = frame
self._load_imdframe_into_ts(imdf)

logger.debug("IMDReader: Loaded frame %d", 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
self.ts.data["step"] = imdf.step
if imdf.energies is not None:
self.ts.data.update(
{k: v for k, v in imdf.energies.items() if k != "step"}
)
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
np.copyto(self.ts.positions, imdf.positions)
if imdf.velocities is not None:
np.copyto(self.ts.velocities, imdf.velocities)
if imdf.forces is not None:
np.copyto(self.ts.forces, imdf.forces)

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

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

# NOTE: think of other edge cases as well- should be robust
def parse_host_port(filename):
if not filename.startswith("imd://"):
raise ValueError("IMDReader: URL must be in the format 'imd://host:port'")
# Check if the format is correct
parts = filename.split("imd://")[1].split(":")
if len(parts) == 2:
host = parts[0]
try:
port = int(parts[1])
return (host, port)
except ValueError as e:
raise ValueError("IMDReader: Port must be an integer") from e
else:
raise ValueError("IMDReader: URL must be in the format 'imd://host:port'")
6 changes: 6 additions & 0 deletions package/MDAnalysis/coordinates/__init__.py
Original file line number Diff line number Diff line change
@@ -274,6 +274,11 @@ class can choose an appropriate reader automatically.
| library | | | file formats`_ and |
| | | | :mod:`MDAnalysis.coordinates.chemfiles` |
+---------------+-----------+-------+------------------------------------------------------+
| IMD | IP address| r/w | Receive simulation trajectory data using interactive |
| | and port | | molecular dynamics version 3 (IMDv3) by configuring |
| | number | | a socket address to a NAMD, GROMACS, or LAMMPS |
| | | | simulation. |
+---------------+-----------+-------+------------------------------------------------------+
.. [#a] This format can also be used to provide basic *topology*
information (i.e. the list of atoms); it is possible to create a
@@ -770,6 +775,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
204 changes: 204 additions & 0 deletions package/MDAnalysis/coordinates/base.py
Original file line number Diff line number Diff line change
@@ -1844,3 +1844,207 @@ def __repr__(self):

def convert(self, obj):
raise NotImplementedError

class StreamReaderBase(ReaderBase):
"""Base class for readers that read a continuous stream of data.
This class is used for readers that read a continuous stream of data,
such as a live feed from a simulation. This places some constraints on the
reader, such as not being able to rewind or iterate more than once.
.. versionadded:: 2.9.0
"""

def __init__(self, filename, convert_units=True, **kwargs):
super(StreamReaderBase, self).__init__(
filename, convert_units=convert_units, **kwargs
)
self._init_scope = True
self._reopen_called = False
self._first_ts = None

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

ts = self._read_frame(self._frame + 1)

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

return ts

@property
def n_frames(self):
"""Changes as stream is processed unlike other readers"""
raise RuntimeError(
"{}: n_frames is unknown".format(self.__class__.__name__)
)

def __len__(self):
raise RuntimeError(
"{} has unknown length".format(self.__class__.__name__)
)

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
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(
"{}: Stream-based readers can't be rewound".format(
self.__class__.__name__
)
)

# Incompatible methods
def copy(self):
raise RuntimeError(
"{} does not support copying".format(self.__class__.__name__)
)

def _reopen(self):
if self._reopen_called:
raise RuntimeError(
"{}: Cannot reopen stream".format(self.__class__.__name__)
)
self._frame = -1
self._reopen_called = True

def timeseries(self, **kwargs):
raise RuntimeError(
"{}: cannot access timeseries for streamed trajectories".format(self.__class__.__name__)
)

def __getitem__(self, frame):
"""Return the Timestep corresponding to *frame*.
If `frame` is a integer then the corresponding frame is
returned. Negative numbers are counted from the end.
If frame is a :class:`slice` then an iterator is returned that
allows iteration over that part of the trajectory.
Note
----
*frame* is a 0-based frame index.
"""
if isinstance(frame, slice):
_, _, step = self.check_slice_indices(
frame.start, frame.stop, frame.step
)
if step is None:
return FrameIteratorAll(self)
else:
return StreamFrameIteratorSliced(self, step)
else:
raise TypeError(
"Streamed trajectories must be an indexed using a slice"
)

def check_slice_indices(self, start, stop, step):
if start is not None:
raise ValueError(
"{}: Cannot expect a start index from a stream, 'start' must be None".format(
self.__class__.__name__
)
)
if stop is not None:
raise ValueError(
"{}: Cannot expect a stop index from a stream, 'stop' must be None".format(
self.__class__.__name__
)
)
if step is not None:
if isinstance(step, numbers.Integral):
if step < 1:
raise ValueError(
"{}: Cannot go backwards in a stream, 'step' must be > 0".format(
self.__class__.__name__
)
)
else:
raise ValueError(
"{}: 'step' must be an integer".format(
self.__class__.__name__
)
)

return start, stop, step

def __getstate__(self):
raise NotImplementedError(
"{} does not support pickling".format(self.__class__.__name__)
)

def __setstate__(self, state: object):
raise NotImplementedError(
"{} does not support pickling".format(self.__class__.__name__)
)

def __repr__(self):
return (
"<{cls} {fname} with continuous stream of {natoms} atoms>"
"".format(
cls=self.__class__.__name__,
fname=self.filename,
natoms=self.n_atoms,
)
)


class StreamFrameIteratorSliced(FrameIteratorBase):

def __init__(self, trajectory, step):
super().__init__(trajectory)
self._step = step

def __iter__(self):
# Calling reopen tells reader
# it can't be reopened again
self.trajectory._reopen()
return self

def __next__(self):
try:
# Burn the timesteps until we reach the desired step
# Don't use next() to avoid unnecessary transformations
while self.trajectory._frame + 1 % self.step != 0:
self.trajectory._read_next_timestep()
except (EOFError, IOError):
# Don't rewind here like we normally would
raise StopIteration from None

return self.trajectory.next()

def __len__(self):
raise RuntimeError(
"{} has unknown length".format(self.__class__.__name__)
)

def __getitem__(self, frame):
raise RuntimeError("Sliced iterator does not support indexing")

@property
def step(self):
return self._step
1 change: 1 addition & 0 deletions package/doc/sphinx/source/conf.py
Original file line number Diff line number Diff line change
@@ -349,4 +349,5 @@ class KeyStyle(UnsrtStyle):
"pathsimanalysis": ("https://www.mdanalysis.org/PathSimAnalysis/", None),
"mdahole2": ("https://www.mdanalysis.org/mdahole2/", None),
"dask": ("https://docs.dask.org/en/stable/", None),
"imdclient": ("https://imdclient.readthedocs.io/en/stable/", None),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. automodule:: MDAnalysis.coordinates.IMD
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ provide the format in the keyword argument *format* to
coordinates/GSD
coordinates/GRO
coordinates/H5MD
coordinates/IMD
coordinates/INPCRD
coordinates/LAMMPS
coordinates/MMTF
7 changes: 7 additions & 0 deletions package/doc/sphinx/source/documentation_pages/references.rst
Original file line number Diff line number Diff line change
@@ -229,6 +229,13 @@ If you use H5MD files using
pp. 18 – 26, 2021. doi:`10.25080/majora-1b6fd038-005.
<https://www.doi.org/10.25080/majora-1b6fd038-005>`_
If you use IMD capability with :mod:`MDAnalysis.coordinates.IMD.py`, please cite
[IMDv3paper]_.

.. [IMDv3paper] Authors (YEAR).
IMDv3 Manuscript Title.
*Journal*, 185. doi:`insert-doi-here
<https://doi.org/>`_
.. _citations-using-duecredit:

1 change: 1 addition & 0 deletions package/pyproject.toml
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ extra_formats = [
"pytng>=0.2.3",
"gsd>3.0.0",
"rdkit>=2020.03.1",
"imdclient",
Copy link
Member

Choose a reason for hiding this comment

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

We almost certainly need to add a minimal version.

]
analysis = [
"biopython>=1.80",
390 changes: 390 additions & 0 deletions testsuite/MDAnalysisTests/coordinates/test_imd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
"""Test for MDAnalysis trajectory reader expectations
"""
import numpy as np
import logging
import pytest
from MDAnalysis.transformations import translate
import pickle
import MDAnalysis as mda
from numpy.testing import (
assert_almost_equal,
assert_array_almost_equal,
assert_equal,
assert_allclose,
)
import sys
from types import ModuleType
from MDAnalysis.coordinates.IMD import HAS_IMDCLIENT

if HAS_IMDCLIENT:
import imdclient
from imdclient.tests.utils import (
get_free_port,
create_default_imdsinfo_v3,
)
from imdclient.tests.server import InThreadIMDServer

from MDAnalysis.coordinates.IMD import IMDReader

from MDAnalysisTests.datafiles import (
COORDINATES_TOPOLOGY,
COORDINATES_TRR,
COORDINATES_H5MD,
)

from MDAnalysisTests.coordinates.base import (
MultiframeReaderTest,
BaseReference,
assert_timestep_almost_equal,
)


def test_HAS_IMDCLIENT_too_old():
# mock a version of imdclient that is too old
module_name = "imdclient"

sys.modules.pop(module_name, None)
sys.modules.pop("MDAnalysis.coordinates.IMD", None)

mocked_module = ModuleType(module_name)
# too old version
mocked_module.__version__ = "0.1.0"
sys.modules[module_name] = mocked_module

from MDAnalysis.coordinates.IMD import HAS_IMDCLIENT
assert not HAS_IMDCLIENT



def test_HAS_IMDCLIENT_new_enough():
module_name = "imdclient"
sys.modules.pop(module_name, None)
sys.modules.pop("MDAnalysis.coordinates.IMD", None)

mocked_module = ModuleType(module_name)
# new enough version
mocked_module.__version__ = "0.1.4"
sys.modules[module_name] = mocked_module

from MDAnalysis.coordinates.IMD import HAS_IMDCLIENT
assert HAS_IMDCLIENT




@pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed")
class IMDReference(BaseReference):
def __init__(self):
super(IMDReference, self).__init__()
self.port = get_free_port()
# Serve TRR traj data via the server
traj = mda.coordinates.TRR.TRRReader(COORDINATES_TRR)
self.server = InThreadIMDServer(traj)
self.server.set_imdsessioninfo(create_default_imdsinfo_v3())

self.n_atoms = traj.n_atoms
self.prec = 3

self.trajectory = f"imd://localhost:{self.port}"
self.topology = COORDINATES_TOPOLOGY
self.changing_dimensions = True
self.reader = IMDReader

self.first_frame.velocities = self.first_frame.positions / 10
self.first_frame.forces = self.first_frame.positions / 100

self.second_frame.velocities = self.second_frame.positions / 10
self.second_frame.forces = self.second_frame.positions / 100

self.last_frame.velocities = self.last_frame.positions / 10
self.last_frame.forces = self.last_frame.positions / 100

self.jump_to_frame.velocities = self.jump_to_frame.positions / 10
self.jump_to_frame.forces = self.jump_to_frame.positions / 100

def iter_ts(self, i):
ts = self.first_frame.copy()
ts.positions = 2**i * self.first_frame.positions
ts.velocities = ts.positions / 10
ts.forces = ts.positions / 100
ts.time = i
ts.frame = i
return ts


@pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed")
class TestIMDReaderBaseAPI(MultiframeReaderTest):

@pytest.fixture(scope='function')
def ref(self):
"""Not a static method like in base class- need new server for each test"""
return IMDReference()

@pytest.fixture()
def reader(self, ref):
# This will start the test IMD Server, waiting for a connection
# to then send handshake & first frame
ref.server.handshake_sequence("localhost", ref.port)
# This will connect to the test IMD Server and read the first frame
reader = ref.reader(ref.trajectory, n_atoms=ref.n_atoms)
# Send the rest of the frames- small enough to all fit in socket itself
ref.server.send_frames(1, 5)

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

@pytest.fixture()
def transformed(self, ref):
# This will start the test IMD Server, waiting for a connection
# to then send handshake & first frame
ref.server.handshake_sequence("localhost", ref.port)
# This will connect to the test IMD Server and read the first frame
transformed = ref.reader(ref.trajectory, n_atoms=ref.n_atoms)
# Send the rest of the frames- small enough to all fit in socket itself
ref.server.send_frames(1, 5)
transformed.add_transformations(
translate([1, 1, 1]), translate([0, 0, 0.33])
)
return transformed

def test_n_frames(self, reader, ref):
Copy link
Member

Choose a reason for hiding this comment

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

Could you add for these skips a short comment of why this test is skipped i.e it is not part of the API and why

pass

def test_first_frame(self, ref, reader):
# don't rewind here as in inherited base test
assert_timestep_almost_equal(
reader.ts, ref.first_frame, decimal=ref.prec
)

def test_get_writer_1(self, ref, reader, tmpdir):
pass

def test_get_writer_2(self, ref, reader, tmpdir):
pass

def test_total_time(self, reader, ref):
pass

def test_changing_dimensions(self, ref, reader):
pass

def test_iter(self, ref, reader):
for i, ts in enumerate(reader):
assert_timestep_almost_equal(ts, ref.iter_ts(i), decimal=ref.prec)

def test_first_dimensions(self, ref, reader):
# don't rewind here as in inherited base test
if ref.dimensions is None:
assert reader.ts.dimensions is None
else:
assert_allclose(
reader.ts.dimensions,
ref.dimensions,
rtol=0,
atol=1.5 * 10**(-ref.prec)
)


def test_volume(self, ref, reader):
# don't rewind here as in inherited base test
vol = reader.ts.volume
# Here we can only be sure about the numbers upto the decimal point due
# to floating point impressions.
assert_allclose(vol, ref.volume, rtol=0, atol=1.5e0)

def test_reload_auxiliaries_from_description(self, ref, reader):
pass

def test_stop_iter(self, reader):
pass

def test_iter_rewinds(self, reader):
pass

def test_timeseries_shape(self, reader,):
pass

def test_timeseries_asel_shape(self, reader):
pass

def test_timeseries_values(self, reader):
pass

def test_transformations_2iter(self, ref, transformed):
pass

def test_transformations_slice(self, ref, transformed):
pass

def test_transformations_switch_frame(self, ref, transformed):
pass

def test_transformation_rewind(self, ref, transformed):
pass

def test_copy(self, ref, transformed):
pass

def test_pickle_reader(self, reader):
pass

def test_pickle_next_ts_reader(self, reader):
pass

def test_pickle_last_ts_reader(self, reader):
pass

def test_transformations_copy(self, ref, transformed):
pass

def test_timeseries_empty_asel(self, reader):
pass

def test_timeseries_empty_atomgroup(self, reader):
pass

def test_timeseries_asel_warns_deprecation(self, reader):
pass

def test_timeseries_atomgroup(self, reader):
pass

def test_timeseries_atomgroup_asel_mutex(self, reader):
pass

def test_last_frame(self, ref, reader):
pass

def test_go_over_last_frame(self, ref, reader):
pass

def test_frame_jump(self, ref, reader):
pass

def test_frame_jump_issue1942(self, ref, reader):
pass

def test_next_gives_second_frame(self, ref, reader):
# don't recreate reader here as in inherited base test
ts = reader.next()
assert_timestep_almost_equal(ts, ref.second_frame, decimal=ref.prec)

def test_frame_collect_all_same(self, reader):
pass


@pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not isntalled")
class TestStreamIteration:

@pytest.fixture
def port(self):
return get_free_port()

@pytest.fixture
def universe(self):
return mda.Universe(COORDINATES_TOPOLOGY, COORDINATES_H5MD)

@pytest.fixture
def imdsinfo(self):
return create_default_imdsinfo_v3()

@pytest.fixture
def reader(self, universe, imdsinfo, port):
server = InThreadIMDServer(universe.trajectory)
server.set_imdsessioninfo(imdsinfo)
server.handshake_sequence("localhost", port, first_frame=True)
reader = IMDReader(
f"imd://localhost:{port}",
n_atoms=universe.trajectory.n_atoms,
)
server.send_frames(1, 5)

yield reader
server.cleanup()

def test_iterate_step(self, reader, universe):
i = 0
for ts in reader[::2]:
assert ts.frame == i
i += 2

def test_iterate_twice_sliced_raises_error(self, reader):
for ts in reader[::2]:
pass
with pytest.raises(RuntimeError, match="Cannot reopen stream"):
for ts in reader[::2]:
pass

def test_iterate_twice_all_raises_error(self, reader):
for ts in reader:
pass
with pytest.raises(RuntimeError, match="Cannot reopen stream"):
for ts in reader:
pass

def test_iterate_twice_fi_all_raises_error(self, reader):
for ts in reader[:]:
pass
with pytest.raises(RuntimeError, match="Cannot reopen stream"):
for ts in reader[:]:
pass

def test_index_stream_raises_error(self, reader):
with pytest.raises(TypeError, match="Streamed trajectories must be"):
reader[0]

def test_iterate_backwards_raises_error(self, reader):
with pytest.raises(ValueError, match="Cannot go backwards"):
for ts in reader[::-1]:
pass


def test_iterate_start_stop_raises_error(self, reader):
with pytest.raises(ValueError, match="Cannot expect a start index"):
for ts in reader[1:3]:
pass

def test_subslice_fi_all_after_iteration_raises_error(self, reader):
sliced_reader = reader[:]
for ts in sliced_reader:
pass
sub_sliced_reader = sliced_reader[::1]
with pytest.raises(RuntimeError):
for ts in sub_sliced_reader:
pass


def test_timeseries_raises(self, reader):
with pytest.raises(
RuntimeError, match="cannot access timeseries for streamed trajectories"
):
reader.timeseries()

@pytest.mark.skipif(not HAS_IMDCLIENT, reason="IMDClient not installed")
def test_n_atoms_mismatch():
universe = mda.Universe(COORDINATES_TOPOLOGY, COORDINATES_H5MD)
port = get_free_port()
server = InThreadIMDServer(universe.trajectory)
server.set_imdsessioninfo(create_default_imdsinfo_v3())
server.handshake_sequence("localhost", port, first_frame=True)
with pytest.raises(
EOFError,
match="IMDProducer: Expected n_atoms value 6, got 5. Ensure you are using the correct topology file.",
):
IMDReader(
f"imd://localhost:{port}",
n_atoms=universe.trajectory.n_atoms + 1,
)