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

Open
wants to merge 62 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
a6f2bb2
IMDReader integration
amruthesht Feb 19, 2025
a67cbfb
small cleanup
amruthesht Feb 19, 2025
8c00bc1
Chore: Cleanup and small fixes
amruthesht Apr 10, 2025
bc88b7b
bump CI
hmacdope May 5, 2025
cf15cf9
Add minimum docs
jaclark5 May 24, 2025
7aed3b4
Merge remote-tracking branch 'upstream/develop' into imdv3-dev
hmacdope Jun 7, 2025
b5ff03d
try add mock for import versions
hmacdope Jun 7, 2025
6263151
fix for wrong assert
hmacdope Jun 7, 2025
efbb903
fix fixture issues
hmacdope Jun 7, 2025
2ff3935
add short class description
hmacdope Jun 7, 2025
073430b
add stricter matching to tests
hmacdope Jun 7, 2025
45ad921
forbid use of .timeseries() method for streamed trajectories
hmacdope Jun 7, 2025
eb825c6
Update test_HAS_IMDCLIENT_new_enough to pass
jaclark5 Jun 19, 2025
5f30313
Merge remote-tracking branch 'upstream/develop' into imdv3-dev
hmacdope Jun 23, 2025
a8b4157
add imd docs
yuxuanzhuang Jun 23, 2025
0dfb194
add doc for streamreader
yuxuanzhuang Jun 23, 2025
83d9443
black imd
yuxuanzhuang Jun 23, 2025
a169cb6
add reason for skipped test
yuxuanzhuang Jun 23, 2025
d48fec1
use staticmethod for readers
yuxuanzhuang Jun 23, 2025
c23fa3e
black imd test
yuxuanzhuang Jun 23, 2025
809d592
try 0.2.0b0 imdclient
hmacdope Jun 28, 2025
a25ff7a
check imd file input is a string
yuxuanzhuang Jun 28, 2025
7ae4b21
add some port parsing test
hmacdope Jun 29, 2025
577f785
add test for transformation
hmacdope Jun 29, 2025
93cdb22
try cut buffer by 10
hmacdope Jun 29, 2025
7e5bcb0
add buffer sizes
hmacdope Jun 29, 2025
103278b
fail out on non-v3
ljwoods2 Jun 30, 2025
5501df6
Change to `r` only
hmacdope Jul 1, 2025
a9eab43
remove parse port
yuxuanzhuang Jul 7, 2025
97d0636
remove parse port in test
yuxuanzhuang Jul 7, 2025
b2239bc
only import parse port when imdclient exists
yuxuanzhuang Jul 7, 2025
5932f66
use monkeypatch to avoid mess sys module
yuxuanzhuang Jul 9, 2025
808b998
black imd test
yuxuanzhuang Jul 9, 2025
2d95ac7
add coverage test
yuxuanzhuang Jul 9, 2025
57948c6
add tests fix
yuxuanzhuang Jul 9, 2025
c40a829
disable test on imd version
yuxuanzhuang Jul 9, 2025
05f58e5
imd type hint
yuxuanzhuang Jul 10, 2025
b181cb9
test error on mda not imdclient
yuxuanzhuang Jul 10, 2025
2199882
close ref port properly
yuxuanzhuang Jul 10, 2025
10d260d
test imd import in different file
yuxuanzhuang Jul 13, 2025
f89a75c
add test for stream base
yuxuanzhuang Jul 13, 2025
2167620
move _frame to baseclass
yuxuanzhuang Jul 13, 2025
831b46e
remove onepass
yuxuanzhuang Jul 13, 2025
38f96a6
Update installed optional deps with new imdclient 0.2.2 package
hmacdope Jul 21, 2025
e2c0913
Update azure-pipelines.yml for 0.2.2 imdclient version
hmacdope Jul 21, 2025
07756f8
Update environment.yml
hmacdope Jul 21, 2025
fd61753
Update IMD.py version pins for imdclient package 0.2.2
hmacdope Jul 21, 2025
db59525
Update pyproject.toml for imdclient package 0.2.2
hmacdope Jul 21, 2025
bfc7e94
Merge branch 'develop' into imdv3-dev
hmacdope Jul 21, 2025
bbcb14e
Update version pin to be dynamic in test_imd_import.py
hmacdope Jul 21, 2025
60c434c
Update test_imd_import.py
hmacdope Jul 21, 2025
3c04d37
make changes for new testserver API
hmacdope Jul 24, 2025
6a9115a
remove n_atoms_mismatch test
hmacdope Jul 24, 2025
27597e7
fix str concat
hmacdope Jul 24, 2025
d04ff96
Fix host port format
hmacdope Jul 24, 2025
10dfe27
doc tweaks
ljwoods2 Jul 24, 2025
c6a9f39
typo
ljwoods2 Jul 24, 2025
65a1bf8
clarify need for gromacs configuration before example command
ljwoods2 Jul 24, 2025
b1502ff
buffer docs tweak and typo
amruthesht Jul 25, 2025
74050f4
buffer default value change
amruthesht Jul 25, 2025
9338d96
Update: AUTHORS
amruthesht Jul 25, 2025
8f154f2
Update: CHANGELOG
amruthesht Jul 25, 2025
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
Expand Up @@ -82,6 +82,8 @@ inputs:
default: 'seaborn>=0.7.0'
tidynamics:
default: 'tidynamics>=1.0.0'
imdclient:
default: 'imdclient>=0.2.2'
# pip-installed min dependencies
coverage:
default: 'coverage'
Expand Down Expand Up @@ -147,6 +149,7 @@ runs:
${{ inputs.scikit-learn }}
${{ inputs.seaborn }}
${{ inputs.tidynamics }}
${{ inputs.imdclient }}

run: |
# setup full variable
Expand Down
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ jobs:
pytng>=0.2.3
rdkit>=2024.03.4
tidynamics>=1.0.0
imdclient>=0.2.2

# remove from azure to avoid test hanging #4707
# "gsd>3.0.0"
displayName: 'Install additional dependencies for 64-bit tests'
Expand Down
1 change: 1 addition & 0 deletions maintainer/conda/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies:
- sphinxcontrib-bibtex
- mdaencore
- waterdynamics
- imdclient>=0.2.2
- pip:
- mdahole2
- pathsimanalysis
Expand Down
2 changes: 1 addition & 1 deletion package/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ Chronological list of authors
- Tulga-Erdene Sodjargal
- Gareth Elliott
- Marc Schuh

- Amruthesh Thirumalaiswamy

External code
-------------
Expand Down
3 changes: 3 additions & 0 deletions package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ Enhancements
(PR #5038)
* Moved distopia checking function to common import location in
MDAnalysisTest.util (PR #5038)
* New coordinate reader: Added `IMDReader` for reading real-time streamed
molecular dynamics simulation data using the IMDv3 protocol - requires
`imdclient` package (Issue #4827, PR #4923)

Changes
* Refactored the RDKit converter code to move the inferring code in a separate
Expand Down
254 changes: 254 additions & 0 deletions package/MDAnalysis/coordinates/IMD.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
"""
IMDReader --- :mod:`MDAnalysis.coordinates.IMD`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:class:`MDAnalysis.coordinates.IMD.IMDReader` is a class that implements the
`Interactive Molecular Dynamics (IMD) protocol <https://imdclient.readthedocs.io/en/latest/protocol_v3.html>`_ for reading simulation
data using the IMDClient (see `imdclient <https://github.com/Becksteinlab/imdclient>`_).
The protocol allows two-way communicating molecular simulation data through a socket.
Via IMD, a simulation engine sends data to a receiver (in this case, the IMDClient) and the receiver can send forces and specific control
requests (such as pausing, resuming, or terminating the simulation) back to the simulation engine.

IMDv3, the newest version of the protocol, is the one supported by this reader class and is implemented in GROMACS, LAMMPS, and NAMD at varying
stages of development. See the `imdclient simulation engine docs <https://imdclient.readthedocs.io/en/latest/usage.html>`_ for more.

IMDv2, the first version to be broadly adopted, is currently available as a part of official releases of GROMACS, LAMMPS, and NAMD. However,
this reader class does not currently provide support for it since it was designed for visualization and gaps are allowed in the stream
(i.e., an inconsistent number of integrator time steps between transmitted coordinate arrays is allowed)

As an example of reading a stream, after configuring GROMACS to run a simulation with IMDv3 enabled
(see the `imdclient simulation engine docs <https://imdclient.readthedocs.io/en/latest/usage.html>`_ for
up-to-date resources on configuring each simulation engine), use the following commands:

.. code-block:: bash

gmx grompp -f run-NPT_imd-v3.mdp -c conf.gro -p topol.top -o topol.tpr
gmx mdrun -v -nt 4 -imdwait -imdport 8889

The :class:`MDAnalysis.coordinates.IMD.IMDReader` can then connect to the running simulation and stream data in real time:

.. code-block:: python

import MDAnalysis as mda
u = mda.Universe("topol.tpr", "imd://localhost:8889", buffer_size=10*1024*1024)

print(" time [ position ] [ velocity ] [ force ] [ box ]")
sel = u.select_atoms("all") # Select all atoms; adjust selection as needed
for ts in u.trajectory:
print(f'{ts.time:8.3f} {sel[0].position} {sel[0].velocity} {sel[0].force} {u.dimensions[0:3]}')

Details about the IMD protocol and usage examples can be found in the
`imdclient <https://github.com/Becksteinlab/imdclient>`_ repository.


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.2.2")

try:
import imdclient
from imdclient.IMDClient import IMDClient
from imdclient.utils import parse_host_port
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.2.2
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):
"""
Reader that supports the Interactive Molecular Dynamics (IMD) protocol for reading simulation
data using the IMDClient.

By using the keyword `buffer_size`, you can change the amount of memory the :class:`IMDClient`
allocates to its internal buffer. The buffer size determines how many frames can be stored
in memory as data is received from the socket and awaits reading by the client. For analyses
that periodically perform heavier computation at fixed intervals, say for example once every
200 received frames, increasing this value will decrease the amount of time the simulation
engine spends in a paused state and potentially decrease total analysis time, but will require
more RAM.

Parameters
----------
filename : a string of the form "imd://host:port" where host is the hostname
Copy link
Contributor

Choose a reason for hiding this comment

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

Add documentation for buffer_size which gets passed to imdclient. Buffer size is a cap on the number of bytes allocated in memory for the IMDFrameBuffer. Increasing it will likely decrease the amount of time the simulation engine spends in a paused state, but requires more RAM

Copy link
Member

Choose a reason for hiding this comment

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

@ljwoods2 could you add this documentation.

or IP address of the listening simulation engine's IMD 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.
buffer_size: int (optional) default=10*(1024**2)
number of bytes of memory to allocate to the :class:`IMDClient`'s
internal buffer. Defaults to 10 megabytes.
kwargs : dict (optional)
keyword arguments passed to the constructed :class:`IMDClient`
"""

format = "IMD"

@store_init_arguments
def __init__(
self,
filename,
n_atoms=None,
buffer_size=10*(1024**2),
**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

try:
host, port = parse_host_port(filename)
except ValueError as e:
raise ValueError(f"IMDReader: Invalid IMD URL '{filename}': {e}")

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

imdsinfo = self._imdclient.get_imdsessioninfo()
if imdsinfo.version != 3:
raise NotImplementedError(
f"IMDReader: Detected IMD version v{imdsinfo.version}, "
+ "but IMDReader is only compatible with v3"
)

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

try:
self._read_next_timestep()
except EOFError as e:
raise RuntimeError(f"IMDReader: Read error: {e}") 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):
if not isinstance(thing, str):
return False
# a weaker check for type hint
if thing.startswith("imd://"):
return True
else:
return False

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.")
6 changes: 6 additions & 0 deletions package/MDAnalysis/coordinates/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ class can choose an appropriate reader automatically.
| library | | | file formats`_ and |
| | | | :mod:`MDAnalysis.coordinates.chemfiles` |
+---------------+-----------+-------+------------------------------------------------------+
| IMD | IP address| r | 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
Expand Down Expand Up @@ -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
Expand Down
Loading