imdclient 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- imdclient/IMDClient.py +896 -0
- imdclient/IMDProtocol.py +164 -0
- imdclient/IMDREADER.py +129 -0
- imdclient/__init__.py +14 -0
- imdclient/backends.py +352 -0
- imdclient/data/__init__.py +0 -0
- imdclient/data/gromacs/md/gromacs_struct.gro +21151 -0
- imdclient/data/gromacs/md/gromacs_v3.top +11764 -0
- imdclient/data/gromacs/md/gromacs_v3_nst1.mdp +58 -0
- imdclient/data/gromacs/md/gromacs_v3_nst1.tpr +0 -0
- imdclient/data/gromacs/md/gromacs_v3_nst1.trr +0 -0
- imdclient/data/lammps/md/lammps_topol.data +8022 -0
- imdclient/data/lammps/md/lammps_trj.h5md +0 -0
- imdclient/data/lammps/md/lammps_v3.in +71 -0
- imdclient/data/namd/md/alanin.dcd +0 -0
- imdclient/data/namd/md/alanin.params +402 -0
- imdclient/data/namd/md/alanin.pdb +77 -0
- imdclient/data/namd/md/alanin.psf +206 -0
- imdclient/data/namd/md/namd_v3.namd +47 -0
- imdclient/results.py +332 -0
- imdclient/streamanalysis.py +1056 -0
- imdclient/streambase.py +199 -0
- imdclient/tests/__init__.py +0 -0
- imdclient/tests/base.py +122 -0
- imdclient/tests/conftest.py +38 -0
- imdclient/tests/datafiles.py +34 -0
- imdclient/tests/server.py +212 -0
- imdclient/tests/test_gromacs.py +33 -0
- imdclient/tests/test_imdclient.py +150 -0
- imdclient/tests/test_imdreader.py +644 -0
- imdclient/tests/test_lammps.py +38 -0
- imdclient/tests/test_manual.py +70 -0
- imdclient/tests/test_namd.py +38 -0
- imdclient/tests/test_stream_analysis.py +61 -0
- imdclient/tests/utils.py +41 -0
- imdclient/utils.py +118 -0
- imdclient-0.1.0.dist-info/AUTHORS.md +23 -0
- imdclient-0.1.0.dist-info/LICENSE +21 -0
- imdclient-0.1.0.dist-info/METADATA +143 -0
- imdclient-0.1.0.dist-info/RECORD +42 -0
- imdclient-0.1.0.dist-info/WHEEL +5 -0
- imdclient-0.1.0.dist-info/top_level.txt +1 -0
imdclient/IMDClient.py
ADDED
@@ -0,0 +1,896 @@
|
|
1
|
+
"""
|
2
|
+
|
3
|
+
IMDClient
|
4
|
+
^^^^^^^^^
|
5
|
+
|
6
|
+
.. autoclass:: IMDClient
|
7
|
+
:members:
|
8
|
+
|
9
|
+
.. autoclass:: IMDProducerV3
|
10
|
+
:members:
|
11
|
+
:inherited-members:
|
12
|
+
|
13
|
+
.. autoclass:: IMDFrameBuffer
|
14
|
+
:members:
|
15
|
+
|
16
|
+
"""
|
17
|
+
|
18
|
+
import socket
|
19
|
+
import threading
|
20
|
+
from .IMDProtocol import *
|
21
|
+
from .utils import read_into_buf, sock_contains_data, timeit
|
22
|
+
import logging
|
23
|
+
import queue
|
24
|
+
import time
|
25
|
+
import numpy as np
|
26
|
+
from typing import Union, Dict
|
27
|
+
import signal
|
28
|
+
|
29
|
+
logger = logging.getLogger(__name__)
|
30
|
+
|
31
|
+
|
32
|
+
class IMDClient:
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
host,
|
36
|
+
port,
|
37
|
+
n_atoms,
|
38
|
+
socket_bufsize=None,
|
39
|
+
multithreaded=True,
|
40
|
+
**kwargs,
|
41
|
+
):
|
42
|
+
"""
|
43
|
+
Parameters
|
44
|
+
----------
|
45
|
+
host : str
|
46
|
+
Hostname of the server
|
47
|
+
port : int
|
48
|
+
Port number of the server
|
49
|
+
n_atoms : int
|
50
|
+
Number of atoms in the simulation
|
51
|
+
socket_bufsize : int, optional
|
52
|
+
Size of the socket buffer in bytes. Default is to use the system default
|
53
|
+
buffer_size : int, optional
|
54
|
+
IMDFramebuffer will be filled with as many IMDFrames fit in `buffer_size` [``10MB``]
|
55
|
+
**kwargs : optional
|
56
|
+
Additional keyword arguments to pass to the IMDProducer and IMDFrameBuffer
|
57
|
+
"""
|
58
|
+
self._stopped = False
|
59
|
+
self._conn = self._connect_to_server(host, port, socket_bufsize)
|
60
|
+
self._imdsinfo = self._await_IMD_handshake()
|
61
|
+
self._multithreaded = multithreaded
|
62
|
+
|
63
|
+
if self._multithreaded:
|
64
|
+
self._buf = IMDFrameBuffer(
|
65
|
+
self._imdsinfo,
|
66
|
+
n_atoms,
|
67
|
+
**kwargs,
|
68
|
+
)
|
69
|
+
else:
|
70
|
+
self._buf = None
|
71
|
+
if self._imdsinfo.version == 2:
|
72
|
+
self._producer = IMDProducerV2(
|
73
|
+
self._conn,
|
74
|
+
self._buf,
|
75
|
+
self._imdsinfo,
|
76
|
+
n_atoms,
|
77
|
+
multithreaded,
|
78
|
+
**kwargs,
|
79
|
+
)
|
80
|
+
elif self._imdsinfo.version == 3:
|
81
|
+
self._producer = IMDProducerV3(
|
82
|
+
self._conn,
|
83
|
+
self._buf,
|
84
|
+
self._imdsinfo,
|
85
|
+
n_atoms,
|
86
|
+
multithreaded,
|
87
|
+
**kwargs,
|
88
|
+
)
|
89
|
+
|
90
|
+
self._go()
|
91
|
+
|
92
|
+
if self._multithreaded:
|
93
|
+
signal.signal(signal.SIGINT, self.signal_handler)
|
94
|
+
self._producer.start()
|
95
|
+
|
96
|
+
def signal_handler(self, sig, frame):
|
97
|
+
"""Catch SIGINT to allow clean shutdown on CTRL+C
|
98
|
+
This also ensures that main thread execution doesn't get stuck
|
99
|
+
waiting in buf.pop_full_imdframe()"""
|
100
|
+
self.stop()
|
101
|
+
|
102
|
+
def get_imdframe(self):
|
103
|
+
"""
|
104
|
+
Raises
|
105
|
+
------
|
106
|
+
EOFError
|
107
|
+
If there are no more frames to read from the stream
|
108
|
+
"""
|
109
|
+
if self._multithreaded:
|
110
|
+
try:
|
111
|
+
return self._buf.pop_full_imdframe()
|
112
|
+
except EOFError:
|
113
|
+
# in this case, consumer is already finished
|
114
|
+
# and doesn't need to be notified
|
115
|
+
self._disconnect()
|
116
|
+
self._stopped = True
|
117
|
+
raise EOFError
|
118
|
+
else:
|
119
|
+
try:
|
120
|
+
return self._producer._get_imdframe()
|
121
|
+
except EOFError:
|
122
|
+
self._disconnect()
|
123
|
+
raise EOFError
|
124
|
+
|
125
|
+
def get_imdsessioninfo(self):
|
126
|
+
return self._imdsinfo
|
127
|
+
|
128
|
+
def stop(self):
|
129
|
+
if self._multithreaded:
|
130
|
+
if not self._stopped:
|
131
|
+
self._buf.notify_consumer_finished()
|
132
|
+
self._disconnect()
|
133
|
+
self._stopped = True
|
134
|
+
else:
|
135
|
+
self._disconnect()
|
136
|
+
|
137
|
+
def _connect_to_server(self, host, port, socket_bufsize):
|
138
|
+
"""
|
139
|
+
Establish connection with the server, failing out instantly if server is not running
|
140
|
+
"""
|
141
|
+
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
142
|
+
if socket_bufsize is not None:
|
143
|
+
# Default (linux):
|
144
|
+
# /proc/sys/net/core/rmem_default
|
145
|
+
# Max (linux):
|
146
|
+
# /proc/sys/net/core/rmem_max
|
147
|
+
conn.setsockopt(
|
148
|
+
socket.SOL_SOCKET, socket.SO_RCVBUF, socket_bufsize
|
149
|
+
)
|
150
|
+
try:
|
151
|
+
logger.debug(f"IMDClient: Connecting to {host}:{port}")
|
152
|
+
conn.connect((host, port))
|
153
|
+
except ConnectionRefusedError:
|
154
|
+
raise ConnectionRefusedError(
|
155
|
+
f"IMDClient: Connection to {host}:{port} refused"
|
156
|
+
)
|
157
|
+
return conn
|
158
|
+
|
159
|
+
def _await_IMD_handshake(self) -> IMDSessionInfo:
|
160
|
+
"""
|
161
|
+
Wait for the server to send a handshake packet, then determine
|
162
|
+
IMD session information.
|
163
|
+
"""
|
164
|
+
end = ">"
|
165
|
+
ver = None
|
166
|
+
|
167
|
+
self._conn.settimeout(5)
|
168
|
+
|
169
|
+
h_buf = bytearray(IMDHEADERSIZE)
|
170
|
+
try:
|
171
|
+
read_into_buf(self._conn, h_buf)
|
172
|
+
except (ConnectionError, TimeoutError, Exception) as e:
|
173
|
+
logger.debug("IMDClient: No handshake packet received: %s", e)
|
174
|
+
raise ConnectionError("IMDClient: No handshake packet received")
|
175
|
+
|
176
|
+
header = IMDHeader(h_buf)
|
177
|
+
|
178
|
+
if header.type != IMDHeaderType.IMD_HANDSHAKE:
|
179
|
+
raise ValueError(
|
180
|
+
f"Expected header type `IMD_HANDSHAKE`, got {header.type}"
|
181
|
+
)
|
182
|
+
|
183
|
+
if header.length not in IMDVERSIONS:
|
184
|
+
# Try swapping endianness
|
185
|
+
swapped = struct.unpack("<i", struct.pack(">i", header.length))[0]
|
186
|
+
if swapped not in IMDVERSIONS:
|
187
|
+
err_version = min(swapped, header.length)
|
188
|
+
# NOTE: Add test for this
|
189
|
+
raise ValueError(
|
190
|
+
f"Incompatible IMD version. Expected version in {IMDVERSIONS}, got {err_version}"
|
191
|
+
)
|
192
|
+
else:
|
193
|
+
end = "<"
|
194
|
+
ver = swapped
|
195
|
+
else:
|
196
|
+
ver = header.length
|
197
|
+
|
198
|
+
sinfo = None
|
199
|
+
if ver == 2:
|
200
|
+
# IMD v2 does not send a configuration handshake body packet
|
201
|
+
sinfo = IMDSessionInfo(
|
202
|
+
version=ver,
|
203
|
+
endianness=end,
|
204
|
+
wrapped_coords=False,
|
205
|
+
time=False,
|
206
|
+
energies=True,
|
207
|
+
box=False,
|
208
|
+
positions=True,
|
209
|
+
velocities=False,
|
210
|
+
forces=False,
|
211
|
+
)
|
212
|
+
|
213
|
+
elif ver == 3:
|
214
|
+
read_into_buf(self._conn, h_buf)
|
215
|
+
header = IMDHeader(h_buf)
|
216
|
+
if header.type != IMDHeaderType.IMD_SESSIONINFO:
|
217
|
+
raise ValueError(
|
218
|
+
f"Expected header type `IMD_SESSIONINFO`, got {header.type}"
|
219
|
+
)
|
220
|
+
if header.length != 7:
|
221
|
+
raise ValueError(
|
222
|
+
f"Expected header length 7, got {header.length}"
|
223
|
+
)
|
224
|
+
data = bytearray(7)
|
225
|
+
read_into_buf(self._conn, data)
|
226
|
+
sinfo = parse_imdv3_session_info(data, end)
|
227
|
+
logger.debug(f"IMDClient: Received IMDv3 session info: {sinfo}")
|
228
|
+
|
229
|
+
return sinfo
|
230
|
+
|
231
|
+
def _go(self):
|
232
|
+
"""
|
233
|
+
Send a go packet to the client to start the simulation
|
234
|
+
and begin receiving data.
|
235
|
+
"""
|
236
|
+
go = create_header_bytes(IMDHeaderType.IMD_GO, 0)
|
237
|
+
self._conn.sendall(go)
|
238
|
+
logger.debug("IMDClient: Sent go packet to server")
|
239
|
+
|
240
|
+
def _disconnect(self):
|
241
|
+
# MUST disconnect before stopping execution
|
242
|
+
# if simulation already ended, this method will do nothing
|
243
|
+
try:
|
244
|
+
disconnect = create_header_bytes(IMDHeaderType.IMD_DISCONNECT, 0)
|
245
|
+
self._conn.sendall(disconnect)
|
246
|
+
logger.debug("IMDClient: Disconnected from server")
|
247
|
+
except (ConnectionResetError, BrokenPipeError, Exception) as e:
|
248
|
+
logger.debug(
|
249
|
+
f"IMDProducer: Attempted to disconnect but server already terminated the connection: %s",
|
250
|
+
e,
|
251
|
+
)
|
252
|
+
finally:
|
253
|
+
self._conn.close()
|
254
|
+
|
255
|
+
|
256
|
+
class BaseIMDProducer(threading.Thread):
|
257
|
+
|
258
|
+
def __init__(
|
259
|
+
self,
|
260
|
+
conn,
|
261
|
+
buffer,
|
262
|
+
sinfo,
|
263
|
+
n_atoms,
|
264
|
+
multithreaded=True,
|
265
|
+
timeout=5,
|
266
|
+
**kwargs,
|
267
|
+
):
|
268
|
+
"""
|
269
|
+
Parameters
|
270
|
+
----------
|
271
|
+
conn : socket.socket
|
272
|
+
Connection object to the server
|
273
|
+
buffer : IMDFrameBuffer
|
274
|
+
Buffer object to hold IMD frames. If `multithreaded` is False, this
|
275
|
+
argument is ignored
|
276
|
+
sinfo : IMDSessionInfo
|
277
|
+
Information about the IMD session
|
278
|
+
n_atoms : int
|
279
|
+
Number of atoms in the simulation
|
280
|
+
multithreaded : bool, optional
|
281
|
+
If True, socket interaction will occur in a separate thread &
|
282
|
+
frames will be buffered. Single-threaded, blocking IMDClient
|
283
|
+
should only be used in testing [[``True``]]
|
284
|
+
|
285
|
+
"""
|
286
|
+
super(BaseIMDProducer, self).__init__(daemon=True)
|
287
|
+
self._conn = conn
|
288
|
+
self._imdsinfo = sinfo
|
289
|
+
self._paused = False
|
290
|
+
|
291
|
+
# Timeout for first frame should be longer
|
292
|
+
# than rest of frames
|
293
|
+
self._timeout = timeout
|
294
|
+
self._conn.settimeout(self._timeout)
|
295
|
+
|
296
|
+
self._buf = buffer
|
297
|
+
|
298
|
+
self._frame = 0
|
299
|
+
self._parse_frame_time = 0
|
300
|
+
|
301
|
+
self._n_atoms = n_atoms
|
302
|
+
|
303
|
+
if not multithreaded:
|
304
|
+
self._imdf = IMDFrame(n_atoms, sinfo)
|
305
|
+
else:
|
306
|
+
# In this case, buffer performs frame allocation
|
307
|
+
self._imdf = None
|
308
|
+
|
309
|
+
self._header = bytearray(IMDHEADERSIZE)
|
310
|
+
|
311
|
+
def _parse_imdframe(self):
|
312
|
+
raise NotImplementedError
|
313
|
+
|
314
|
+
def _pause(self):
|
315
|
+
raise NotImplementedError
|
316
|
+
|
317
|
+
def _unpause(self):
|
318
|
+
raise NotImplementedError
|
319
|
+
|
320
|
+
def _get_imdframe(self):
|
321
|
+
"""Parses an IMD frame from the stream. Blocks until a frame is available.
|
322
|
+
|
323
|
+
Raises
|
324
|
+
------
|
325
|
+
EOFError
|
326
|
+
If there are no more frames to read from the stream
|
327
|
+
RuntimeError
|
328
|
+
If an unexpected error occurs
|
329
|
+
"""
|
330
|
+
try:
|
331
|
+
self._parse_imdframe()
|
332
|
+
except EOFError as e:
|
333
|
+
raise EOFError
|
334
|
+
except Exception as e:
|
335
|
+
raise RuntimeError("An unexpected error occurred") from e
|
336
|
+
|
337
|
+
return self._imdf
|
338
|
+
|
339
|
+
def run(self):
|
340
|
+
logger.debug("IMDProducer: Starting run loop")
|
341
|
+
try:
|
342
|
+
while True:
|
343
|
+
with timeit() as t:
|
344
|
+
logger.debug("IMDProducer: Checking for pause")
|
345
|
+
|
346
|
+
if not self._paused:
|
347
|
+
logger.debug("IMDProducer: Not paused")
|
348
|
+
if self._buf.is_full():
|
349
|
+
self._pause()
|
350
|
+
self._paused = True
|
351
|
+
|
352
|
+
logger.debug("IMDProducer: Checking for unpause")
|
353
|
+
|
354
|
+
if self._paused:
|
355
|
+
logger.debug("IMDProducer: Paused")
|
356
|
+
# wait for socket to empty before unpausing
|
357
|
+
if not sock_contains_data(self._conn, 0):
|
358
|
+
logger.debug("IMDProducer: Checked sock for data")
|
359
|
+
self._buf.wait_for_space()
|
360
|
+
self._unpause()
|
361
|
+
self._paused = False
|
362
|
+
|
363
|
+
self._imdf = self._buf.pop_empty_imdframe()
|
364
|
+
|
365
|
+
logger.debug("IMDProducer: Got empty frame")
|
366
|
+
|
367
|
+
self._parse_imdframe()
|
368
|
+
|
369
|
+
self._buf.push_full_imdframe(self._imdf)
|
370
|
+
|
371
|
+
self._frame += 1
|
372
|
+
logger.debug(
|
373
|
+
"IMDProducer: Frame %d parsed in %d seconds",
|
374
|
+
self._frame,
|
375
|
+
t.elapsed,
|
376
|
+
)
|
377
|
+
except EOFError:
|
378
|
+
# simulation ended in a way
|
379
|
+
# that we expected
|
380
|
+
# i.e. consumer stopped or read_into_buf didn't find
|
381
|
+
# full token of data
|
382
|
+
logger.debug("IMDProducer: Simulation ended normally, cleaning up")
|
383
|
+
except Exception as e:
|
384
|
+
logger.debug("IMDProducer: An unexpected error occurred: %s", e)
|
385
|
+
finally:
|
386
|
+
logger.debug("IMDProducer: Stopping run loop")
|
387
|
+
# Tell consumer not to expect more frames to be added
|
388
|
+
self._buf.notify_producer_finished()
|
389
|
+
|
390
|
+
def _expect_header(self, expected_type, expected_value=None):
|
391
|
+
|
392
|
+
header = self._get_header()
|
393
|
+
|
394
|
+
if header.type != expected_type:
|
395
|
+
raise RuntimeError(
|
396
|
+
f"IMDProducer: Expected header type {expected_type}, got {header.type}"
|
397
|
+
)
|
398
|
+
# Sometimes we do not care what the value is
|
399
|
+
if expected_value is not None and header.length != expected_value:
|
400
|
+
raise RuntimeError(
|
401
|
+
f"IMDProducer: Expected header value {expected_value}, got {header.length}"
|
402
|
+
)
|
403
|
+
|
404
|
+
def _get_header(self):
|
405
|
+
self._read(self._header)
|
406
|
+
return IMDHeader(self._header)
|
407
|
+
|
408
|
+
def _read(self, buf):
|
409
|
+
"""Wraps `read_into_buf` call to give uniform error handling which indicates end of stream"""
|
410
|
+
try:
|
411
|
+
read_into_buf(self._conn, buf)
|
412
|
+
except (ConnectionError, TimeoutError, BlockingIOError, Exception):
|
413
|
+
# ConnectionError: Server is definitely done sending frames, socket is closed
|
414
|
+
# TimeoutError: Server is *likely* done sending frames.
|
415
|
+
# BlockingIOError: Occurs when timeout is 0 in place of a TimeoutError. Server is *likely* done sending frames
|
416
|
+
# OSError: Occurs when main thread disconnects from the server and closes the socket, but producer thread attempts to read another frame
|
417
|
+
# Exception: Something unexpected happened
|
418
|
+
raise EOFError
|
419
|
+
|
420
|
+
|
421
|
+
class IMDProducerV2(BaseIMDProducer):
|
422
|
+
def __init__(self, conn, buffer, sinfo, n_atoms, multithreaded, **kwargs):
|
423
|
+
super(IMDProducerV2, self).__init__(
|
424
|
+
conn, buffer, sinfo, n_atoms, multithreaded, **kwargs
|
425
|
+
)
|
426
|
+
|
427
|
+
self._energies = bytearray(IMDENERGYPACKETLENGTH)
|
428
|
+
self._positions = bytearray(12 * self._n_atoms)
|
429
|
+
self._prev_energies = None
|
430
|
+
|
431
|
+
def _parse_imdframe(self):
|
432
|
+
# Not all IMDv2 implementations send energies
|
433
|
+
# so we can't expect them
|
434
|
+
|
435
|
+
# Even if they are sent, energies might not be sent every frame
|
436
|
+
# cache the last energies received
|
437
|
+
|
438
|
+
# Either receive energies + positions or just positions
|
439
|
+
header = self._get_header()
|
440
|
+
if header.type == IMDHeaderType.IMD_ENERGIES and header.length == 1:
|
441
|
+
self._imdsinfo.energies = True
|
442
|
+
self._read(self._energies)
|
443
|
+
self._imdf.energies.update(
|
444
|
+
parse_energy_bytes(self._energies, self._imdsinfo.endianness)
|
445
|
+
)
|
446
|
+
self._prev_energies = self._imdf.energies
|
447
|
+
|
448
|
+
self._expect_header(
|
449
|
+
IMDHeaderType.IMD_FCOORDS, expected_value=self._n_atoms
|
450
|
+
)
|
451
|
+
self._read(self._positions)
|
452
|
+
np.copyto(
|
453
|
+
self._imdf.positions,
|
454
|
+
np.frombuffer(
|
455
|
+
self._positions, dtype=f"{self._imdsinfo.endianness}f"
|
456
|
+
).reshape((self._n_atoms, 3)),
|
457
|
+
)
|
458
|
+
elif (
|
459
|
+
header.type == IMDHeaderType.IMD_FCOORDS
|
460
|
+
and header.length == self._n_atoms
|
461
|
+
):
|
462
|
+
# If we received positions but no energies
|
463
|
+
# use the last energies received
|
464
|
+
if self._prev_energies is not None:
|
465
|
+
self._imdf.energies = self._prev_energies
|
466
|
+
else:
|
467
|
+
self._imdf.energies = None
|
468
|
+
self._read(self._positions)
|
469
|
+
np.copyto(
|
470
|
+
self._imdf.positions,
|
471
|
+
np.frombuffer(
|
472
|
+
self._positions, dtype=f"{self._imdsinfo.endianness}f"
|
473
|
+
).reshape((self._n_atoms, 3)),
|
474
|
+
)
|
475
|
+
else:
|
476
|
+
raise RuntimeError("IMDProducer: Unexpected packet type or length")
|
477
|
+
|
478
|
+
def _pause(self):
|
479
|
+
self._conn.settimeout(0)
|
480
|
+
logger.debug(
|
481
|
+
"IMDProducer: Pausing simulation because buffer is almost full"
|
482
|
+
)
|
483
|
+
pause = create_header_bytes(IMDHeaderType.IMD_PAUSE, 0)
|
484
|
+
try:
|
485
|
+
self._conn.sendall(pause)
|
486
|
+
except ConnectionResetError as e:
|
487
|
+
# Simulation has already ended by the time we paused
|
488
|
+
raise EOFError
|
489
|
+
# Edge case: pause occured in the time between server sends its last frame
|
490
|
+
# and closing socket
|
491
|
+
# Simulation is not actually paused but is over, but we still want to read remaining data
|
492
|
+
# from the socket
|
493
|
+
|
494
|
+
def _unpause(self):
|
495
|
+
self._conn.settimeout(self._timeout)
|
496
|
+
logger.debug("IMDProducer: Unpausing simulation, buffer has space")
|
497
|
+
unpause = create_header_bytes(IMDHeaderType.IMD_PAUSE, 0)
|
498
|
+
try:
|
499
|
+
self._conn.sendall(unpause)
|
500
|
+
except ConnectionResetError as e:
|
501
|
+
# Edge case: pause occured in the time between server sends its last frame
|
502
|
+
# and closing socket
|
503
|
+
# Simulation was never actually paused in this case and is now over
|
504
|
+
raise EOFError
|
505
|
+
# Edge case: pause & unpause occured in the time between server sends its last frame and closing socket
|
506
|
+
# in this case, the simulation isn't actually unpaused but over
|
507
|
+
|
508
|
+
|
509
|
+
class IMDProducerV3(BaseIMDProducer):
|
510
|
+
def __init__(
|
511
|
+
self,
|
512
|
+
conn,
|
513
|
+
buffer,
|
514
|
+
sinfo,
|
515
|
+
n_atoms,
|
516
|
+
multithreaded,
|
517
|
+
**kwargs,
|
518
|
+
):
|
519
|
+
super(IMDProducerV3, self).__init__(
|
520
|
+
conn,
|
521
|
+
buffer,
|
522
|
+
sinfo,
|
523
|
+
n_atoms,
|
524
|
+
multithreaded,
|
525
|
+
**kwargs,
|
526
|
+
)
|
527
|
+
# The body of an x/v/f packet should contain
|
528
|
+
# (4 bytes per float * 3 atoms * n_atoms) bytes
|
529
|
+
xvf_bytes = 12 * n_atoms
|
530
|
+
if self._imdsinfo.energies:
|
531
|
+
self._energies = bytearray(IMDENERGYPACKETLENGTH)
|
532
|
+
if self._imdsinfo.time:
|
533
|
+
self._time = bytearray(IMDTIMEPACKETLENGTH)
|
534
|
+
if self._imdsinfo.box:
|
535
|
+
self._box = bytearray(IMDBOXPACKETLENGTH)
|
536
|
+
if self._imdsinfo.positions:
|
537
|
+
self._positions = bytearray(xvf_bytes)
|
538
|
+
if self._imdsinfo.velocities:
|
539
|
+
self._velocities = bytearray(xvf_bytes)
|
540
|
+
if self._imdsinfo.forces:
|
541
|
+
self._forces = bytearray(xvf_bytes)
|
542
|
+
|
543
|
+
def _pause(self):
|
544
|
+
self._conn.settimeout(0)
|
545
|
+
logger.debug(
|
546
|
+
"IMDProducer: Pausing simulation because buffer is almost full"
|
547
|
+
)
|
548
|
+
pause = create_header_bytes(IMDHeaderType.IMD_PAUSE, 0)
|
549
|
+
try:
|
550
|
+
self._conn.sendall(pause)
|
551
|
+
except ConnectionResetError as e:
|
552
|
+
# Simulation has already ended by the time we paused
|
553
|
+
raise EOFError
|
554
|
+
# Edge case: pause occured in the time between server sends its last frame
|
555
|
+
# and closing socket
|
556
|
+
# Simulation is not actually paused but is over, but we still want to read remaining data
|
557
|
+
# from the socket
|
558
|
+
|
559
|
+
def _unpause(self):
|
560
|
+
self._conn.settimeout(self._timeout)
|
561
|
+
logger.debug("IMDProducer: Unpausing simulation, buffer has space")
|
562
|
+
unpause = create_header_bytes(IMDHeaderType.IMD_RESUME, 0)
|
563
|
+
try:
|
564
|
+
self._conn.sendall(unpause)
|
565
|
+
except ConnectionResetError as e:
|
566
|
+
# Edge case: pause occured in the time between server sends its last frame
|
567
|
+
# and closing socket
|
568
|
+
# Simulation was never actually paused in this case and is now over
|
569
|
+
raise EOFError
|
570
|
+
# Edge case: pause & unpause occured in the time between server sends its last frame and closing socket
|
571
|
+
# in this case, the simulation isn't actually unpaused but over
|
572
|
+
|
573
|
+
def _parse_imdframe(self):
|
574
|
+
if self._imdsinfo.time:
|
575
|
+
self._expect_header(IMDHeaderType.IMD_TIME, expected_value=1)
|
576
|
+
# use header buf to hold time data sicnce it is also
|
577
|
+
# 8 bytes
|
578
|
+
self._read(self._time)
|
579
|
+
t = IMDTime(self._time, self._imdsinfo.endianness)
|
580
|
+
self._imdf.dt = t.dt
|
581
|
+
self._imdf.time = t.time
|
582
|
+
self._imdf.step = t.step
|
583
|
+
|
584
|
+
logger.debug(
|
585
|
+
f"IMDProducer: Time: {self._imdf.time}, dt: {self._imdf.dt}, step: {self._imdf.step}"
|
586
|
+
)
|
587
|
+
if self._imdsinfo.energies:
|
588
|
+
self._expect_header(IMDHeaderType.IMD_ENERGIES, expected_value=1)
|
589
|
+
self._read(self._energies)
|
590
|
+
self._imdf.energies.update(
|
591
|
+
parse_energy_bytes(self._energies, self._imdsinfo.endianness)
|
592
|
+
)
|
593
|
+
if self._imdsinfo.box:
|
594
|
+
self._expect_header(IMDHeaderType.IMD_BOX, expected_value=1)
|
595
|
+
self._read(self._box)
|
596
|
+
self._imdf.box = parse_box_bytes(
|
597
|
+
self._box, self._imdsinfo.endianness
|
598
|
+
)
|
599
|
+
if self._imdsinfo.positions:
|
600
|
+
self._expect_header(
|
601
|
+
IMDHeaderType.IMD_FCOORDS, expected_value=self._n_atoms
|
602
|
+
)
|
603
|
+
self._read(self._positions)
|
604
|
+
np.copyto(
|
605
|
+
self._imdf.positions,
|
606
|
+
np.frombuffer(
|
607
|
+
self._positions, dtype=f"{self._imdsinfo.endianness}f"
|
608
|
+
).reshape((self._n_atoms, 3)),
|
609
|
+
)
|
610
|
+
if self._imdsinfo.velocities:
|
611
|
+
self._expect_header(
|
612
|
+
IMDHeaderType.IMD_VELOCITIES, expected_value=self._n_atoms
|
613
|
+
)
|
614
|
+
self._read(self._velocities)
|
615
|
+
np.copyto(
|
616
|
+
self._imdf.velocities,
|
617
|
+
np.frombuffer(
|
618
|
+
self._velocities, dtype=f"{self._imdsinfo.endianness}f"
|
619
|
+
).reshape((self._n_atoms, 3)),
|
620
|
+
)
|
621
|
+
if self._imdsinfo.forces:
|
622
|
+
self._expect_header(
|
623
|
+
IMDHeaderType.IMD_FORCES, expected_value=self._n_atoms
|
624
|
+
)
|
625
|
+
self._read(self._forces)
|
626
|
+
np.copyto(
|
627
|
+
self._imdf.forces,
|
628
|
+
np.frombuffer(
|
629
|
+
self._forces, dtype=f"{self._imdsinfo.endianness}f"
|
630
|
+
).reshape((self._n_atoms, 3)),
|
631
|
+
)
|
632
|
+
|
633
|
+
def __del__(self):
|
634
|
+
logger.debug("IMDProducer: I am being deleted")
|
635
|
+
|
636
|
+
|
637
|
+
class IMDFrameBuffer:
|
638
|
+
"""
|
639
|
+
Acts as interface between producer (IMDProducer) and consumer (IMDClient) threads
|
640
|
+
when IMDClient runs in multithreaded mode
|
641
|
+
"""
|
642
|
+
|
643
|
+
def __init__(
|
644
|
+
self,
|
645
|
+
imdsinfo,
|
646
|
+
n_atoms,
|
647
|
+
buffer_size=(10 * 1024**2),
|
648
|
+
pause_empty_proportion=0.25,
|
649
|
+
unpause_empty_proportion=0.5,
|
650
|
+
**kwargs,
|
651
|
+
):
|
652
|
+
"""
|
653
|
+
Parameters
|
654
|
+
----------
|
655
|
+
imdsinfo : IMDSessionInfo
|
656
|
+
Information about the IMD session
|
657
|
+
n_atoms : int
|
658
|
+
Number of atoms in the simulation
|
659
|
+
buffer_size : int, optional
|
660
|
+
Size of the buffer in bytes [``10MB``]
|
661
|
+
pause_empty_proportion : float, optional
|
662
|
+
Lower threshold proportion of the buffer's IMDFrames that are empty
|
663
|
+
before the simulation is paused [``0.25``]
|
664
|
+
unpause_empty_proportion : float, optional
|
665
|
+
Proportion of the buffer's IMDFrames that must be empty
|
666
|
+
before the simulation is unpaused [``0.5``]
|
667
|
+
"""
|
668
|
+
|
669
|
+
# Syncing reader and producer
|
670
|
+
self._producer_finished = False
|
671
|
+
self._consumer_finished = False
|
672
|
+
|
673
|
+
self._prev_empty_imdf = None
|
674
|
+
|
675
|
+
self._empty_q = queue.Queue()
|
676
|
+
self._full_q = queue.Queue()
|
677
|
+
self._empty_imdf_avail = threading.Condition(threading.Lock())
|
678
|
+
self._full_imdf_avail = threading.Condition(threading.Lock())
|
679
|
+
|
680
|
+
if pause_empty_proportion < 0 or pause_empty_proportion > 1:
|
681
|
+
raise ValueError("pause_empty_proportion must be between 0 and 1")
|
682
|
+
self._pause_empty_proportion = pause_empty_proportion
|
683
|
+
if unpause_empty_proportion < 0 or unpause_empty_proportion > 1:
|
684
|
+
raise ValueError(
|
685
|
+
"unpause_empty_proportion must be between 0 and 1"
|
686
|
+
)
|
687
|
+
self._unpause_empty_proportion = unpause_empty_proportion
|
688
|
+
|
689
|
+
if buffer_size <= 0:
|
690
|
+
raise ValueError("Buffer size must be positive")
|
691
|
+
# Allocate IMDFrames with all of xvf present in imdsinfo
|
692
|
+
# even if they aren't sent every frame. Can be optimized if needed
|
693
|
+
imdf_memsize = imdframe_memsize(n_atoms, imdsinfo)
|
694
|
+
self._total_imdf = buffer_size // imdf_memsize
|
695
|
+
logger.debug(
|
696
|
+
f"IMDFrameBuffer: Total IMDFrames allocated: {self._total_imdf}"
|
697
|
+
)
|
698
|
+
if self._total_imdf == 0:
|
699
|
+
raise ValueError(
|
700
|
+
"Buffer size is too small to hold a single IMDFrame"
|
701
|
+
)
|
702
|
+
for i in range(self._total_imdf):
|
703
|
+
self._empty_q.put(IMDFrame(n_atoms, imdsinfo))
|
704
|
+
|
705
|
+
# Timing for analysis
|
706
|
+
self._t1 = None
|
707
|
+
self._t2 = None
|
708
|
+
|
709
|
+
self._frame = 0
|
710
|
+
|
711
|
+
def is_full(self):
|
712
|
+
logger.debug("IMDFrameBuffer: Checking if full")
|
713
|
+
if (
|
714
|
+
self._empty_q.qsize() / self._total_imdf
|
715
|
+
<= self._pause_empty_proportion
|
716
|
+
):
|
717
|
+
return True
|
718
|
+
|
719
|
+
logger.debug(
|
720
|
+
f"IMDFrameBuffer: Full frames: {self._full_q.qsize()}/{self._total_imdf}"
|
721
|
+
)
|
722
|
+
return False
|
723
|
+
|
724
|
+
def wait_for_space(self):
|
725
|
+
logger.debug("IMDProducer: Waiting for space in buffer")
|
726
|
+
|
727
|
+
# Before acquiring the lock, check if we can return immediately
|
728
|
+
if (
|
729
|
+
self._empty_q.qsize() / self._total_imdf
|
730
|
+
>= self._unpause_empty_proportion
|
731
|
+
) and not self._consumer_finished:
|
732
|
+
return
|
733
|
+
try:
|
734
|
+
with self._empty_imdf_avail:
|
735
|
+
while (
|
736
|
+
self._empty_q.qsize() / self._total_imdf
|
737
|
+
< self._unpause_empty_proportion
|
738
|
+
) and not self._consumer_finished:
|
739
|
+
logger.debug("IMDProducer: Waiting...")
|
740
|
+
logger.debug(
|
741
|
+
f"IMDProducer: consumer_finished: {self._consumer_finished}"
|
742
|
+
)
|
743
|
+
self._empty_imdf_avail.wait()
|
744
|
+
|
745
|
+
logger.debug(
|
746
|
+
"IMDProducer: Got space in buffer or consumer finished"
|
747
|
+
)
|
748
|
+
|
749
|
+
if self._consumer_finished:
|
750
|
+
logger.debug("IMDProducer: Noticing consumer finished")
|
751
|
+
raise EOFError
|
752
|
+
except Exception as e:
|
753
|
+
logger.debug(
|
754
|
+
f"IMDProducer: Error waiting for space in buffer: {e}"
|
755
|
+
)
|
756
|
+
|
757
|
+
def pop_empty_imdframe(self):
|
758
|
+
logger.debug("IMDProducer: Getting empty frame")
|
759
|
+
|
760
|
+
# If there are empty frames available, don't wait
|
761
|
+
if self._empty_q.qsize() and not self._consumer_finished:
|
762
|
+
return self._empty_q.get()
|
763
|
+
|
764
|
+
# If there are no empty frames available, wait for one
|
765
|
+
with self._empty_imdf_avail:
|
766
|
+
while self._empty_q.qsize() == 0 and not self._consumer_finished:
|
767
|
+
self._empty_imdf_avail.wait()
|
768
|
+
|
769
|
+
if self._consumer_finished:
|
770
|
+
logger.debug("IMDProducer: Noticing consumer finished")
|
771
|
+
raise EOFError
|
772
|
+
|
773
|
+
return self._empty_q.get()
|
774
|
+
|
775
|
+
def push_full_imdframe(self, imdf):
|
776
|
+
# If the full q is empty, the consumer is waiting
|
777
|
+
# and needs to be awakened
|
778
|
+
self._full_q.put(imdf)
|
779
|
+
with self._full_imdf_avail:
|
780
|
+
self._full_imdf_avail.notify()
|
781
|
+
|
782
|
+
def pop_full_imdframe(self):
|
783
|
+
"""Put empty_ts in the empty_q and get the next full timestep"""
|
784
|
+
# Start timer- one frame of analysis is starting (including removal
|
785
|
+
# from buffer)
|
786
|
+
self._t1 = self._t2
|
787
|
+
self._t2 = time.time()
|
788
|
+
|
789
|
+
# Return the processed IMDFrame
|
790
|
+
if self._prev_empty_imdf is not None:
|
791
|
+
self._empty_q.put(self._prev_empty_imdf)
|
792
|
+
with self._empty_imdf_avail:
|
793
|
+
self._empty_imdf_avail.notify()
|
794
|
+
|
795
|
+
# Get the next IMDFrame
|
796
|
+
imdf = None
|
797
|
+
if self._full_q.qsize() and not self._consumer_finished:
|
798
|
+
imdf = self._full_q.get()
|
799
|
+
else:
|
800
|
+
with self._full_imdf_avail:
|
801
|
+
while (
|
802
|
+
self._full_q.qsize() == 0 and not self._producer_finished
|
803
|
+
):
|
804
|
+
self._full_imdf_avail.wait()
|
805
|
+
|
806
|
+
if self._producer_finished and self._full_q.qsize() == 0:
|
807
|
+
logger.debug("IMDFrameBuffer(Consumer): Producer finished")
|
808
|
+
raise EOFError
|
809
|
+
|
810
|
+
imdf = self._full_q.get()
|
811
|
+
|
812
|
+
self._prev_empty_imdf = imdf
|
813
|
+
|
814
|
+
if self._t1 is not None:
|
815
|
+
logger.debug(
|
816
|
+
f"IMDFrameBuffer(Consumer): Frame #{self._frame} analyzed in {self._t2 - self._t1} seconds"
|
817
|
+
)
|
818
|
+
self._frame += 1
|
819
|
+
|
820
|
+
return imdf
|
821
|
+
|
822
|
+
def notify_producer_finished(self):
|
823
|
+
self._producer_finished = True
|
824
|
+
with self._full_imdf_avail:
|
825
|
+
self._full_imdf_avail.notify()
|
826
|
+
|
827
|
+
def notify_consumer_finished(self):
|
828
|
+
self._consumer_finished = True
|
829
|
+
with self._empty_imdf_avail:
|
830
|
+
# noop if producer isn't waiting
|
831
|
+
self._empty_imdf_avail.notify()
|
832
|
+
|
833
|
+
|
834
|
+
class IMDFrame:
|
835
|
+
|
836
|
+
def __init__(self, n_atoms, imdsinfo):
|
837
|
+
if imdsinfo.time:
|
838
|
+
self.time = 0.0
|
839
|
+
self.dt = 0.0
|
840
|
+
self.step = 0.0
|
841
|
+
else:
|
842
|
+
self.time = None
|
843
|
+
self.dt = None
|
844
|
+
self.step = None
|
845
|
+
if imdsinfo.energies:
|
846
|
+
self.energies = {
|
847
|
+
"step": 0,
|
848
|
+
"temperature": 0.0,
|
849
|
+
"total_energy": 0.0,
|
850
|
+
"potential_energy": 0.0,
|
851
|
+
"van_der_walls_energy": 0.0,
|
852
|
+
"coulomb_energy": 0.0,
|
853
|
+
"bonds_energy": 0.0,
|
854
|
+
"angles_energy": 0.0,
|
855
|
+
"dihedrals_energy": 0.0,
|
856
|
+
"improper_dihedrals_energy": 0.0,
|
857
|
+
}
|
858
|
+
else:
|
859
|
+
self.energies = None
|
860
|
+
if imdsinfo.box:
|
861
|
+
self.box = np.empty((3, 3), dtype=np.float32)
|
862
|
+
else:
|
863
|
+
self.box = None
|
864
|
+
if imdsinfo.positions:
|
865
|
+
self.positions = np.empty((n_atoms, 3), dtype=np.float32)
|
866
|
+
else:
|
867
|
+
self.positions = None
|
868
|
+
if imdsinfo.velocities:
|
869
|
+
self.velocities = np.empty((n_atoms, 3), dtype=np.float32)
|
870
|
+
else:
|
871
|
+
self.velocities = None
|
872
|
+
if imdsinfo.forces:
|
873
|
+
self.forces = np.empty((n_atoms, 3), dtype=np.float32)
|
874
|
+
else:
|
875
|
+
self.forces = None
|
876
|
+
|
877
|
+
|
878
|
+
def imdframe_memsize(n_atoms, imdsinfo) -> int:
|
879
|
+
"""
|
880
|
+
Calculate the memory size of an IMDFrame in bytes
|
881
|
+
"""
|
882
|
+
memsize = 0
|
883
|
+
if imdsinfo.time:
|
884
|
+
memsize += 8 * 3
|
885
|
+
if imdsinfo.energies:
|
886
|
+
memsize += 4 * 10
|
887
|
+
if imdsinfo.box:
|
888
|
+
memsize += 4 * 9
|
889
|
+
if imdsinfo.positions:
|
890
|
+
memsize += 4 * 3 * n_atoms
|
891
|
+
if imdsinfo.velocities:
|
892
|
+
memsize += 4 * 3 * n_atoms
|
893
|
+
if imdsinfo.forces:
|
894
|
+
memsize += 4 * 3 * n_atoms
|
895
|
+
|
896
|
+
return memsize
|