Skip to content
This repository has been archived by the owner on Oct 9, 2024. It is now read-only.

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
ljwoods2 committed May 10, 2024
1 parent 481e3c6 commit 00cb9aa
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 143 deletions.
61 changes: 32 additions & 29 deletions docs/source/protocol_current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,16 @@ IMD Protocol as it is currently implemented in GROMACS
======================================================

Version: 2
Headersize: 8 bytes
Endianness: Little endian and big endian both allowed
Headersize: 8 bytes (two 32 bit integers)
Endianness: Little endian and big endian are both allowed for position and energy data. In headers, big endian is used except in
the handshake signal where the endianness of the server is preserved unswapped in the packet for the client to check.

Only one client is allowed per GROMACS server at a time

Header types
------------

NOTE: Divide these into client and server headers since they are mostly disjoint

Shared headers
^^^^^^^^^^^^^^

.. list-table::
:widths: 10 30 90
:header-rows: 1

* - Type
- Enum Integer Value
- Description
* - IMD_DISCONNECT
- 0
-
* - IMD_IOERROR
- 9
-

GROMACS Server-Only-Sent Headers
GROMACS->Client Headers
^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. list-table::
Expand All @@ -49,7 +32,7 @@ GROMACS Server-Only-Sent Headers
- 4
-

Client-Only-Sent Headers
Client->GROMACS Headers
^^^^^^^^^^^^^^^^^^^^^^^^

.. list-table::
Expand All @@ -59,12 +42,17 @@ Client-Only-Sent Headers
* - Type
- Enum Integer Value
- Description
* - IMD_DISCONNECT
- 0
- Tells the GROMACS server to disconnect its client socket and reset its IMD step to its default
* - IMD_GO
- 3
-
- After receiving the handshake from GROMACS, the client has 1 second to send this signal to the server to begin
receiving data. If the server does not receive this signal, it will disconnect the client. If the GROMACS server
was started with ``-imdwait``, the simulation will not begin until this signal is received.
* - IMD_KILL
- 5
-
- Tells GROMACS to stop the simulation (requires that the simulation was started with ``-imdterm``)
* - IMD_MDCOMM
- 6
-
Expand All @@ -76,10 +64,23 @@ Client-Only-Sent Headers
* - IMD_TRATE
- 8
-

Unused headers
^^^^^^^^^^^^^^

.. list-table::
:widths: 10 30 90
:header-rows: 1

* - Type
- Enum Integer Value
- Description
* - IMD_IOERROR
- 9
-

- Used internally by GROMACS to signal an error in the I/O operations with the client. This is not sent by the client.
* - Count
- 10
- Defined in GROMACS source code but unused

Protocol steps
--------------
Expand Down Expand Up @@ -123,7 +124,7 @@ GROMACS (Server) 6: In every iteration of the md_do loop, first check for incomi
<val> (Number of forces that will be sent in the packet)

Force packet:
<val> number of 32 byte force integers representing indices of atoms to apply force to
<count> of 32 byte force integers representing indices of atoms to apply force to
<val> * 3 number of 32 byte force floats representing the force to apply to the atoms at
the corresponding indices

Expand Down Expand Up @@ -154,4 +155,6 @@ Next, send energies and position data to the client IF this integration step lan
3. Disconnection
################
GROMACS (Server) 1: Shutdown the socket if it isn't already shutdown
GROMACS (Server) 2: Set the IMD frequency to -1
GROMACS (Server) 2: Set the IMD frequency to default and begin listening for connections again

# If connecting a second time, handshake must be sent a second time (test this)
5 changes: 1 addition & 4 deletions imdreader/IMDProtocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
In a HANDSHAKE header packet, the length attribute is used to store the version
of the IMD protocol
"""
IMDHEADERSIZE = 8
IMDENERGYPACKETLENGTH = 40
Expand All @@ -35,9 +34,7 @@ class IMDType(Enum):


class IMDHeader:
"""
Convenience class to represent the header of an IMD packet
"""
"""Convenience class to represent the header of an IMD packet"""

def __init__(self, msg_type: IMDType, length: int):
self.type = msg_type
Expand Down
52 changes: 29 additions & 23 deletions imdreader/IMDREADER.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ def __init__(
self.n_atoms = num_atoms
self.ts = self._Timestep(self.n_atoms, **self._ts_kwargs)

self.units = {
"time": "ps",
"length": "nm",
"force": "kJ/(mol*nm)",
}

# The body of a force or position packet should contain
# (4 bytes per float * 3 atoms * n_atoms) bytes
self._expected_data_bytes = 12 * self.n_atoms
Expand Down Expand Up @@ -80,7 +86,7 @@ def _await_IMD_handshake(self):
and check IMD Protocol version.
"""
print("waiting for handshake...")
handshake = self._expect_header(IMDType.IMD_HANDSHAKE)
handshake = self._expect_header(expected_type=IMDType.IMD_HANDSHAKE)
if handshake.length != IMDVERSION:
# Try swapping endianness
swapped = struct.unpack("<i", struct.pack(">i", handshake.length))[
Expand Down Expand Up @@ -111,6 +117,13 @@ def _send_go_packet(self):
print("sending go packet...")
go = create_header_bytes(IMDType.IMD_GO, 0)
self._conn.sendall(go)
# a test
dis = create_header_bytes(IMDType.IMD_DISCONNECT, 0)
self._conn.sendall(dis)

self._conn.connect((self._host, self._port))
go = create_header_bytes(IMDType.IMD_GO, 0)
self._conn.sendall(go)

def _start_producer_thread(self):
"""
Expand All @@ -129,36 +142,29 @@ def _control_flow(self):
self._parsed_frames = 0

while self._parsed_frames < self.n_frames:
header = self._expect_header()
if header.type == IMDType.IMD_ENERGIES and header.length == 1:
self._recv_energies()
header2 = self._expect_header(IMDType.IMD_FCOORDS)
if header2.length == self.n_atoms:
self._recv_fcoords()
self._parsed_frames += 1
else:
raise ValueError(
f"Unexpected coordinate packet length of "
+ f"{header2.length} bytes, expected "
+ f"{self._expected_data_bytes} bytes"
)
else:
raise ValueError(
f"Unexpected packet type {header.type} "
+ f"of length {header.length}"
)
header1 = self._expect_header(
expected_type=IMDType.IMD_ENERGIES, expected_value=1
)
self._recv_energies()
header2 = self._expect_header(
IMDType.IMD_FCOORDS, expected_value=self.n_atoms
)
self._recv_fcoords()
self._parsed_frames += 1
print(self._parsed_frames)

def _expect_header(self, expected_type=None):
def _expect_header(self, expected_type=None, expected_value=None):
"""
Read a header packet from the socket.
"""
header = parse_header_bytes(self._recv_n_bytes(IMDHEADERSIZE))
if expected_type is not None and header.type != expected_type:
raise ValueError(
"Expected packet type {}, got {}".format(
expected_type, header.type
)
f"Expected packet type {expected_type}, got {header.type}"
)
elif expected_value is not None and header.length != expected_value:
raise ValueError(
f"Expected packet length {expected_value}, got {header.length}"
)
return header

Expand Down
2 changes: 1 addition & 1 deletion imdreader/fake_server.py → imdreader/client_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


u = mda.Universe(
"selected_atoms.pdb",
"example/imdexample/selected_atoms.pdb",
"localhost:8888",
n_frames=100,
num_atoms=1789,
Expand Down
23 changes: 0 additions & 23 deletions imdreader/fake_client.py

This file was deleted.

File renamed without changes.
134 changes: 71 additions & 63 deletions imdreader/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,85 @@
from typing import Tuple

from imdreader.IMDProtocol import *
import MDAnalysis as mda
from MDAnalysis.coordinates.memory import MemoryReader
import numpy as np
import socket


def make_Universe(
extras: Tuple[str] = tuple(),
size: Tuple[int, int, int] = (125, 25, 5),
n_frames: int = 0,
velocities: bool = False,
forces: bool = False
) -> mda.Universe:
"""Make a dummy reference Universe
class DummyIMDServer:
def __init__(
self,
imdwait=True,
imdpull=False,
imdterm=True,
port=8888,
endianness="<",
version=2,
):
self.port = port
self.imdstep = -1
self.imdwait = imdwait
self.imdpull = imdpull
self.imdterm = imdterm
self.endian = endianness
self.version = version

Allows the construction of arbitrary-sized Universes. Suitable for
the generation of structures for output.
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.bind(("localhost", self.port))
# GROMACS waits for a connection for an unlimited time here
self.socket.listen(1)
self.conn, self.address = self.socket.accept()

Preferable for testing core components because:
* minimises dependencies within the package
* very fast compared to a "real" Universe
self._send_handshake()
self._simulation_loop()

Parameters
----------
extras : tuple of strings, optional
extra attributes to add to Universe:
u = make_Universe(('masses', 'charges'))
Creates a lightweight Universe with only masses and charges.
size : tuple of int, optional
number of elements of the Universe (n_atoms, n_residues, n_segments)
n_frames : int
If positive, create a fake Reader object attached to Universe
velocities : bool, optional
if the fake Reader provides velocities
force : bool, optional
if the fake Reader provides forces
def _send_handshake(self):
type = struct.pack("!i", IMDType.IMD_HANDSHAKE.value)
length = struct.pack(f"{self.endian}i", self.version)
header = type + length
self.conn.send(header)

Returns
-------
MDAnalysis.core.universe.Universe object
def disconnect(self):
header = create_header_bytes(IMDType.IMD_DISCONNECT, 0)
self.connection.close()
self.socket.close()

"""
def _simulation_loop(self):
if self.imdwait and self.imdstep == -1:
self._await_go()
pass

n_atoms, n_residues, n_segments = size
trajectory = n_frames > 0
u = mda.Universe.empty(
# topology things
n_atoms=n_atoms,
n_residues=n_residues,
n_segments=n_segments,
atom_resindex=np.repeat(
np.arange(n_residues), n_atoms // n_residues),
residue_segindex=np.repeat(
np.arange(n_segments), n_residues // n_segments),
# trajectory things
trajectory=trajectory,
velocities=velocities,
forces=forces,
)
if extras is None:
extras = []
for ex in extras:
u.add_TopologyAttr(ex)
def _await_go(self):
# must recieve go within 1 second of sending handshake
# if not, terminate connection
self.conn.settimeout(1)
header = self._expect_header(expected_type=IMDType.IMD_GO)
self.conn.settimeout(None)

if trajectory:
pos = np.arange(3 * n_atoms * n_frames).reshape(n_frames, n_atoms, 3)
vel = pos + 100 if velocities else None
fcs = pos + 10000 if forces else None
reader = MemoryReader(
pos,
velocities=vel,
forces=fcs,
)
u.trajectory = reader
def _expect_header(self, expected_type=None, expected_value=None):
"""
Read a header packet from the socket.
"""
header = parse_header_bytes(self._recv_n_bytes(IMDHEADERSIZE))
if expected_type is not None and header.type != expected_type:
raise ValueError(
f"Expected packet type {expected_type}, got {header.type}"
)
elif expected_value is not None and header.length != expected_value:
raise ValueError(
f"Expected packet length {expected_value}, got {header.length}"
)
return header

return u
def _recv_n_bytes(self, num_bytes):
"""Receive an arbitrary number of bytes from the socket."""
data = bytearray(num_bytes)
view = memoryview(data)
total_received = 0
while total_received < num_bytes:
chunk = self.conn.recv(num_bytes - total_received)
if not chunk:
raise ConnectionError("Socket connection was closed")
view[total_received : total_received + len(chunk)] = chunk
total_received += len(chunk)
return data

0 comments on commit 00cb9aa

Please sign in to comment.