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.
Files changed (42) hide show
  1. imdclient/IMDClient.py +896 -0
  2. imdclient/IMDProtocol.py +164 -0
  3. imdclient/IMDREADER.py +129 -0
  4. imdclient/__init__.py +14 -0
  5. imdclient/backends.py +352 -0
  6. imdclient/data/__init__.py +0 -0
  7. imdclient/data/gromacs/md/gromacs_struct.gro +21151 -0
  8. imdclient/data/gromacs/md/gromacs_v3.top +11764 -0
  9. imdclient/data/gromacs/md/gromacs_v3_nst1.mdp +58 -0
  10. imdclient/data/gromacs/md/gromacs_v3_nst1.tpr +0 -0
  11. imdclient/data/gromacs/md/gromacs_v3_nst1.trr +0 -0
  12. imdclient/data/lammps/md/lammps_topol.data +8022 -0
  13. imdclient/data/lammps/md/lammps_trj.h5md +0 -0
  14. imdclient/data/lammps/md/lammps_v3.in +71 -0
  15. imdclient/data/namd/md/alanin.dcd +0 -0
  16. imdclient/data/namd/md/alanin.params +402 -0
  17. imdclient/data/namd/md/alanin.pdb +77 -0
  18. imdclient/data/namd/md/alanin.psf +206 -0
  19. imdclient/data/namd/md/namd_v3.namd +47 -0
  20. imdclient/results.py +332 -0
  21. imdclient/streamanalysis.py +1056 -0
  22. imdclient/streambase.py +199 -0
  23. imdclient/tests/__init__.py +0 -0
  24. imdclient/tests/base.py +122 -0
  25. imdclient/tests/conftest.py +38 -0
  26. imdclient/tests/datafiles.py +34 -0
  27. imdclient/tests/server.py +212 -0
  28. imdclient/tests/test_gromacs.py +33 -0
  29. imdclient/tests/test_imdclient.py +150 -0
  30. imdclient/tests/test_imdreader.py +644 -0
  31. imdclient/tests/test_lammps.py +38 -0
  32. imdclient/tests/test_manual.py +70 -0
  33. imdclient/tests/test_namd.py +38 -0
  34. imdclient/tests/test_stream_analysis.py +61 -0
  35. imdclient/tests/utils.py +41 -0
  36. imdclient/utils.py +118 -0
  37. imdclient-0.1.0.dist-info/AUTHORS.md +23 -0
  38. imdclient-0.1.0.dist-info/LICENSE +21 -0
  39. imdclient-0.1.0.dist-info/METADATA +143 -0
  40. imdclient-0.1.0.dist-info/RECORD +42 -0
  41. imdclient-0.1.0.dist-info/WHEEL +5 -0
  42. 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