-
Notifications
You must be signed in to change notification settings - Fork 773
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
Changes from 110 commits
a6f2bb2
a67cbfb
8c00bc1
bc88b7b
cf15cf9
7aed3b4
b5ff03d
6263151
efbb903
2ff3935
073430b
45ad921
eb825c6
5f30313
a8b4157
0dfb194
83d9443
a169cb6
d48fec1
c23fa3e
809d592
a25ff7a
7ae4b21
577f785
93cdb22
7e5bcb0
103278b
5501df6
a9eab43
97d0636
b2239bc
5932f66
808b998
2d95ac7
57948c6
c40a829
05f58e5
b181cb9
2199882
10d260d
f89a75c
2167620
831b46e
38f96a6
e2c0913
07756f8
fd61753
db59525
bfc7e94
bbcb14e
60c434c
3c04d37
6a9115a
27597e7
d04ff96
10dfe27
c6a9f39
65a1bf8
b1502ff
74050f4
9338d96
8f154f2
c9ac065
a2b2136
481163b
b2e4bd9
aad39f1
17f2de7
b1b0fa7
dcb29d9
844a371
806a0ff
f69d741
3e81d96
230f1a6
c89c0b7
0ba7d0c
2e149aa
4aec1fe
a481b56
d50d307
6a739ea
4818a30
a66122a
6ffbe37
b404b04
0583c2e
73d7852
f04c434
a383185
474981d
e32df0d
e4211a1
7aa10d6
ab38548
b63df7e
d663d89
87311d0
6aee915
350af5f
0deecc0
54f17fd
849b8db
487753e
a9d4fd4
8e7a89c
aa395dc
6634706
04424a8
d68de42
16454a6
19a2526
b465bf1
5138b54
cd21d6b
feb6c9d
35fcc8e
f2a5050
ea85bb5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,290 @@ | ||
| """ | ||
| IMDReader --- :mod:`MDAnalysis.coordinates.IMD` | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| This module provides support for reading molecular dynamics simulation data via the | ||
| `Interactive Molecular Dynamics (IMD) protocol v3 <https://imdclient.readthedocs.io/en/latest/protocol_v3.html>`_. | ||
| The IMD 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. | ||
| .. note:: | ||
| This reader only supports IMDv3, which is implemented in GROMACS, LAMMPS, and NAMD at varying | ||
| stages of development. See the `imdclient simulation engine docs`_ for more. | ||
| While IMDv2 is widely available in simulation engines, it was designed primarily for visualization | ||
| and gaps are allowed in the stream (i.e., an inconsistent number of integrator time steps between transmitted coordinate arrays is allowed) | ||
| The :class:`IMDReader` connects to a simulation via a socket and receives coordinate, | ||
| velocity, force, and energy data as the simulation progresses. This allows for real-time | ||
| monitoring and analysis of ongoing simulations. It uses the `imdclient package <https://github.com/Becksteinlab/imdclient>`_ | ||
| (dependency) to implement the IMDv3 protocol and manage the socket connection and data parsing. | ||
| Usage Example | ||
| ------------- | ||
| As an example of reading a stream, after configuring GROMACS to run a simulation with IMDv3 enabled | ||
| (see the `imdclient simulation engine docs`_ for | ||
| up-to-date resources on configuring each simulation engine), use the following commands: | ||
| .. code-block:: bash | ||
| 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) | ||
amruthesht marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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]}') | ||
amruthesht marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Important Limitations | ||
| --------------------- | ||
amruthesht marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Since IMD streams data in real-time from a running simulation, it has fundamental | ||
| constraints that differ from traditional trajectory readers: | ||
| * **No random access**: Cannot jump to arbitrary frame numbers or seek backwards | ||
| * **Forward-only access**: You can only move forward through frames as they arrive | ||
| * **No trajectory length**: The total number of frames is unknown until the simulation ends | ||
| * **Single-use iteration**: Cannot restart or rewind once the stream has been consumed | ||
| * **No independent copies**: Cannot create separate reader instances for the same stream | ||
| * **No stream restart**: Cannot reconnect or reopen once the connection is closed | ||
| * **No bulk operations**: Cannot extract all data at once using timeseries methods | ||
| * **Limited multiprocessing**: Cannot split reader across processes for parallel analysis | ||
| * **Single client connection**: Only one reader can connect to an IMD stream at a time | ||
| * **No trajectory Writing**: Complimentary IMD Writer class is not available for streaming data | ||
| .. warning:: | ||
| The IMDReader has some important limitations that are inherent in streaming data. | ||
| See :class:`~MDAnalysis.coordinates.base.StreamReaderBase` for technical details. | ||
| .. seealso:: | ||
| :class:`IMDReader` | ||
| Technical details and parameter options for the reader class | ||
| `imdclient documentation <https://imdclient.readthedocs.io/>`_ | ||
| Complete documentation for the IMDClient package | ||
| `IMDClient GitHub repository <https://github.com/Becksteinlab/imdclient>`_ | ||
| Source code and development resources | ||
| .. _`imdclient simulation engine docs`: https://imdclient.readthedocs.io/en/latest/usage.html | ||
| 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 | ||
hmacdope marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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): | ||
hmacdope marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| Coordinate reader implementing the IMDv3 protocol for streaming simulation data. | ||
| This class handles the technical aspects of connecting to IMD-enabled simulation | ||
| engines and processing the incoming data stream. For usage examples and protocol | ||
| overview, see the module documentation above. | ||
| The reader manages socket connections, data buffering, and frame parsing according | ||
| to the IMDv3 specification. It automatically handles different data packet types | ||
| (coordinates, velocities, forces, energies, timing) and populates MDAnalysis | ||
| timestep objects accordingly. | ||
| Parameters | ||
| ---------- | ||
| filename : a string of the form "imd://host:port" where host is the hostname | ||
amruthesht marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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.IMDClient.IMDClient`'s | ||
| internal buffer. Defaults to 10 megabytes. Larger buffers can improve | ||
| performance for analyses with periodic heavy computation. | ||
| **kwargs : dict (optional) | ||
| keyword arguments passed to the constructed :class:`~imdclient.IMDClient.IMDClient` | ||
| Notes | ||
| ----- | ||
| The IMDReader provides access to additional simulation data through the timestep's | ||
| `data` attribute (`ts.data`). The following keys may be available depending on | ||
| what the simulation engine transmits: | ||
| * `dt` : float | ||
| Time step size in picoseconds (from the `IMD_TIME`_ packet of the IMDv3 protocol) | ||
| * `step` : int | ||
| Current simulation step number (from the `IMD_TIME`_ packet of the IMDv3 protocol) | ||
| * Energy terms : float | ||
| Various energy components (e.g., 'potential', 'kinetic', 'total', etc.) | ||
| from the `IMD_ENERGIES`_ packet of the IMDv3 protocol. | ||
| .. _IMD_TIME: https://imdclient.readthedocs.io/en/latest/protocol_v3.html#time | ||
| .. _IMD_ENERGIES: https://imdclient.readthedocs.io/en/latest/protocol_v3.html#energies | ||
| .. note:: | ||
| For important limitations inherent to streaming data, see the module documentation above | ||
| and :class:`~MDAnalysis.coordinates.base.StreamReaderBase` for more technical details. | ||
| .. versionadded:: 2.10.0 | ||
| """ | ||
amruthesht marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 ValueError( | ||
orbeckst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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): | ||
|
|
||
| imdf = self._imdclient.get_imdframe() | ||
|
|
||
| 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 | ||
amruthesht marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self.ts.data["dt"] = imdf.dt | ||
orbeckst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.ts.data["step"] = imdf.step | ||
orbeckst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if imdf.energies is not None: | ||
| self.ts.data.update( | ||
orbeckst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| {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) | ||
orbeckst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Above it says: \
Does that mean that if the IMDReader is not closed gracefully with this function that it will stay active and prevent another connection to the stream? I ask because this can be an issue with Jupyter notebooks where people are in the habit of restarting the kernel which will not close the IMDReader gracefully. This is an active issue I have with python interfacing with postgres
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the socket connection would saty active unless terminated properly. The Side Note: But technically it is possible to form multiple conenctions to the same port address. This is however limited by the host. Currently only NAMD supports this. But each connection has its seperate socket and stream of data which would need to be processed sperately by each client object seperately. The distinction is that these data streams may not always necessarily have the same data, depending on how the host has been configured. So, one can't make an independent copy on the same stream of data but can open a new connection while an existing connection is present (at least in NAMD).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a note to the docs on best practices for using IMDReader in notebooks. Regarding connecting multiple streams to one MD engine port, mention that the behavior is MD engine implementation dependent (some may allow it, others may fail).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am moving the details on MD engine capability to the imdclient docs, so that there's only a single place Becksteinlab/imdclient#114 . I replaced the details with a link. (I also fixed the section level and rearranged where the warning is shown.) @jaclark5 please have a look an resolve if you're happy. |
||
| """Gracefully shut down the reader. Stops the producer thread.""" | ||
| logger.debug("IMDReader close() called") | ||
| if self._imdclient is not None: | ||
| self._imdclient.stop() | ||
| logger.debug("IMDReader shut down gracefully.") | ||
Uh oh!
There was an error while loading. Please reload this page.