asyncmd 0.3.2__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.
asyncmd/tools.py ADDED
@@ -0,0 +1,86 @@
1
+ # This file is part of asyncmd.
2
+ #
3
+ # asyncmd is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # asyncmd is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with asyncmd. If not, see <https://www.gnu.org/licenses/>.
15
+ import os
16
+ import shutil
17
+ import aiofiles
18
+
19
+
20
+ def ensure_executable_available(executable: str) -> str:
21
+ """
22
+ Ensure the given executable is available and executable.
23
+
24
+ Takes a relative or absolute path to an executable or the name of an
25
+ executable available in $PATH. Returns the full path to the executable.
26
+
27
+ Parameters
28
+ ----------
29
+ executable : str
30
+ Name or path of an executable.
31
+
32
+ Returns
33
+ -------
34
+ path_to_executable : str
35
+ Full path to the given executable if it exists.
36
+
37
+ Raises
38
+ ------
39
+ ValueError
40
+ If the given name does not exist or can not be executed.
41
+ """
42
+ if os.path.isfile(os.path.abspath(executable)):
43
+ # see if it is a relative path starting from cwd
44
+ # (or a full path starting with /)
45
+ executable = os.path.abspath(executable)
46
+ if not os.access(executable, os.X_OK):
47
+ raise ValueError(f"{executable} must be executable.")
48
+ elif shutil.which(executable) is not None:
49
+ # see if we find it in $PATH
50
+ executable = shutil.which(executable)
51
+ else:
52
+ raise ValueError(f"{executable} must be an existing path or accesible "
53
+ + "via the $PATH environment variable.")
54
+ return executable
55
+
56
+
57
+ def remove_file_if_exist(f: str):
58
+ """
59
+ Remove a given file if it exists.
60
+
61
+ Parameters
62
+ ----------
63
+ f : str
64
+ Path to the file to remove.
65
+ """
66
+ try:
67
+ os.remove(f)
68
+ except FileNotFoundError:
69
+ # TODO: should we info/warn if the file is not there?
70
+ pass
71
+
72
+
73
+ async def remove_file_if_exist_async(f: str):
74
+ """
75
+ Remove a given file if it exists asynchronously.
76
+
77
+ Parameters
78
+ ----------
79
+ f : str
80
+ Path to the file to remove.
81
+ """
82
+ try:
83
+ await aiofiles.os.remove(f)
84
+ except FileNotFoundError:
85
+ # TODO: should we info/warn if the file is not there?
86
+ pass
@@ -0,0 +1,25 @@
1
+ # This file is part of asyncmd.
2
+ #
3
+ # asyncmd is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # asyncmd is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with asyncmd. If not, see <https://www.gnu.org/licenses/>.
15
+ from .functionwrapper import (PyTrajectoryFunctionWrapper,
16
+ SlurmTrajectoryFunctionWrapper,
17
+ )
18
+ from .propagate import (ConditionalTrajectoryPropagator,
19
+ TrajectoryPropagatorUntilAnyState,
20
+ InPartsTrajectoryPropagator,
21
+ construct_TP_from_plus_and_minus_traj_segments,
22
+ )
23
+ from .trajectory import (_forget_trajectory,
24
+ _forget_all_trajectories,
25
+ )
@@ -0,0 +1,577 @@
1
+ # This file is part of asyncmd.
2
+ #
3
+ # asyncmd is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # asyncmd is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with asyncmd. If not, see <https://www.gnu.org/licenses/>.
15
+ import os
16
+ import abc
17
+ import typing
18
+ import asyncio
19
+ import logging
20
+ import functools
21
+ from concurrent.futures import ThreadPoolExecutor
22
+ import numpy as np
23
+ import MDAnalysis as mda
24
+ try:
25
+ # mda v>=2.3 has moved the timestep class
26
+ from MDAnalysis.coordinates.timestep import Timestep as mda_Timestep
27
+ except ImportError:
28
+ # this is where it lives for mda v<=2.2
29
+ from MDAnalysis.coordinates.base import Timestep as mda_Timestep
30
+ from scipy import constants
31
+
32
+ from .._config import _SEMAPHORES
33
+ from .trajectory import Trajectory
34
+
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def _is_documented_by(original):
40
+ """
41
+ Decorator to copy the docstring of a given method to the decorated method.
42
+ """
43
+ def wrapper(target):
44
+ target.__doc__ = original.__doc__
45
+ return target
46
+ return wrapper
47
+
48
+
49
+ def _attach_mda_trafos_to_universe(
50
+ universe: mda.Universe,
51
+ mda_transformations: typing.Optional[list[typing.Callable]] = None,
52
+ mda_transformations_setup_func: typing.Optional[typing.Callable] = None,
53
+ ) -> mda.Universe:
54
+ """
55
+ Attach MDAnalysis transformations to a given universe.
56
+
57
+ Can either pass a list of on-the-fly transformations or a setup function
58
+ that attaches an arbitrary number of user-defined transformations (that
59
+ then can also depend on e.g. atomgroups in the universe). Note that only
60
+ either a list of transformations or a setup function can be passed but
61
+ never both at the same time.
62
+
63
+ Parameters
64
+ ----------
65
+ universe : MDAnalysis.core.universe.Universe
66
+ The universe to attach the transformations to.
67
+ mda_transformations : typing.Optional[list[typing.Callable]], optional
68
+ List of MDAnalysis transformations to attach, by default None
69
+ mda_transformations_setup_func : typing.Optional[typing.Callable], optional
70
+ Setup function to attach MDAnalysis transformatiosn to the universe,
71
+ by default None
72
+
73
+ Returns
74
+ -------
75
+ MDAnalysis.core.universe.Universe
76
+ The universe with on-the-fly transformations attached.
77
+
78
+ Raises
79
+ ------
80
+ ValueError
81
+ If both ``mda_transformations`` and ``mda_transformations_setupt_func``
82
+ are given.
83
+ """
84
+ # NOTE: this func is used to attach the MDAnalysis transformations to
85
+ # the given universe in the TrajectoryConcatenator and
86
+ # FrameExtractor classes.
87
+ if (mda_transformations is not None
88
+ and mda_transformations_setup_func is not None):
89
+ raise ValueError("`mda_transformations` and "
90
+ "`mda_transformations_setup_func` are mutually "
91
+ "exclusive, but both were given."
92
+ )
93
+ if mda_transformations_setup_func is not None:
94
+ universe = mda_transformations_setup_func(universe)
95
+ elif mda_transformations is not None:
96
+ universe.trajectory.add_transformations(*mda_transformations)
97
+ return universe
98
+
99
+
100
+ class TrajectoryConcatenator:
101
+ """
102
+ Create concatenated trajectory from given trajectories and frames.
103
+
104
+ The concatenate method takes a list of trajectories plus a list of slices
105
+ and returns one trajectory containing only the selected frames in the order
106
+ specified by the slices.
107
+ Velocities are automatically inverted if the step of a slice is negative,
108
+ this can be controlled via the invert_v_for_negative_step attribute.
109
+ We assume that all trajs have the same structure file and attach the
110
+ structure of the first traj if not told otherwise.
111
+ Note that you can pass MDAnalysis transformations to this class to
112
+ transform your trajectories on-the-fly, see the ``mda_transformations`` and
113
+ ``mda_transformations_setup_func`` arguments to :meth:`__init__`.
114
+
115
+ Attributes
116
+ ----------
117
+ invert_v_for_negative_step : bool
118
+ Whether to invert all momenta for segments with negative stride.
119
+ """
120
+
121
+ def __init__(self,
122
+ invert_v_for_negative_step: bool = True,
123
+ mda_transformations: typing.Optional[list[typing.Callable]] = None,
124
+ mda_transformations_setup_func: typing.Optional[typing.Callable] = None,
125
+ ) -> None:
126
+ """
127
+ Initialize a :class:`TrajectoryConcatenator`.
128
+
129
+ Parameters
130
+ ----------
131
+ invert_v_for_negative_step : bool, optional
132
+ Whether to invert all momenta for segments with negative stride,
133
+ by default True.
134
+ mda_transformations : list of callables, optional
135
+ If given will be added as a list of transformations to the
136
+ MDAnalysis universe as
137
+ ``universe.trajectory.add_transformation(*mda_transformations)``.
138
+ See the ``mda_transformations_setup_func`` argument if your
139
+ transformations need additional universe-dependant arguments, e.g.
140
+ atomgroups from the universe.
141
+ See https://docs.mdanalysis.org/stable/documentation_pages/trajectory_transformations.html
142
+ for more on MDAnalysis transformations.
143
+ mda_transformations_setup_func: callable, optional
144
+ If given will be called to attach user-defined MDAnalysis
145
+ transformations to the universe. The function must take a universe
146
+ as argument and return the universe with attached transformations.
147
+ I.e. it is expected that the function calls
148
+ ``universe.trajectory.add_transformations(*list_of_trafos)``
149
+ after defining ``list_of_trafos`` (potentially depending on the
150
+ universe or atomgroups therein) and then finally returns the
151
+ universe with trafos.
152
+ See https://docs.mdanalysis.org/stable/documentation_pages/trajectory_transformations.html
153
+ for more on MDAnalysis transformations.
154
+ """
155
+ self.invert_v_for_negative_step = invert_v_for_negative_step
156
+ if (mda_transformations is not None
157
+ and mda_transformations_setup_func is not None):
158
+ raise ValueError("`mda_transformations` and "
159
+ "`mda_transformations_setup_func` are mutually "
160
+ "exclusive, but both were given."
161
+ )
162
+ self.mda_transformations = mda_transformations
163
+ self.mda_transformations_setup_func = mda_transformations_setup_func
164
+
165
+ def concatenate(self, trajs: "list[Trajectory]", slices: "list[tuple]",
166
+ tra_out: str, struct_out: typing.Optional[str] = None,
167
+ overwrite: bool = False,
168
+ remove_double_frames: bool = True) -> Trajectory:
169
+ """
170
+ Create concatenated trajectory from given trajectories and frames.
171
+
172
+ Parameters
173
+ ----------
174
+ trajs : list[Trajectory]
175
+ List of :class:`asyncmd.Trajectory` objects to concatenate.
176
+ slices : list[tuple]
177
+ List of tuples (start, stop, step) specifing the slices of the
178
+ trajectories to take. Must be of len(trajs).
179
+ tra_out : str
180
+ Output trajectory filepath, absolute or relativ to current working
181
+ directory.
182
+ struct_out : str or None, optional
183
+ Output structure filepath, if None we will take the structure file
184
+ of the first trajectory in trajs, by default None.
185
+ overwrite : bool, optional
186
+ Whether we should overwrite existing output trajectories,
187
+ by default False.
188
+ remove_double_frames : bool, optional
189
+ Wheter we should try to remove double frames from the concatenated
190
+ output trajectory.
191
+ Note that we use a simple heuristic to determine double frames,
192
+ we just check if the integration time is the same for both frames,
193
+ by default True
194
+
195
+ Returns
196
+ -------
197
+ Trajectory
198
+ The concatenated output trajectory.
199
+
200
+ Raises
201
+ ------
202
+ FileExistsError
203
+ If ``tra_out`` exists and ``overwrite=False``.
204
+ FileNotFoundError
205
+ If ``struct_out`` given but the file is not accessible.
206
+ """
207
+ tra_out = os.path.relpath(tra_out)
208
+ if os.path.exists(tra_out) and not overwrite:
209
+ raise FileExistsError(f"overwrite=False and tra_out exists: {tra_out}")
210
+ struct_out = (trajs[0].structure_file if struct_out is None
211
+ else os.path.relpath(struct_out))
212
+ if not os.path.isfile(struct_out):
213
+ # although we would expect that it exists if it comes from an
214
+ # existing traj, we still check to catch other unrelated issues :)
215
+ raise FileNotFoundError(
216
+ f"Output structure file must exist ({struct_out})."
217
+ )
218
+
219
+ # special treatment for traj0 because we need n_atoms for the writer
220
+ u0 = mda.Universe(trajs[0].structure_file, *trajs[0].trajectory_files)
221
+ u0 = _attach_mda_trafos_to_universe(
222
+ universe=u0,
223
+ mda_transformations=self.mda_transformations,
224
+ mda_transformations_setup_func=self.mda_transformations_setup_func,
225
+ )
226
+ start0, stop0, step0 = slices[0]
227
+ if remove_double_frames:
228
+ last_time_seen = None
229
+ # if the file exists MDAnalysis will silently overwrite
230
+ with mda.Writer(tra_out, n_atoms=u0.trajectory.n_atoms) as W:
231
+ for ts in u0.trajectory[start0:stop0:step0]:
232
+ if (self.invert_v_for_negative_step and step0 < 0
233
+ and ts.has_velocities):
234
+ u0.atoms.velocities *= -1
235
+ W.write(u0.atoms)
236
+ if remove_double_frames:
237
+ # remember the last timestamp, so we can take it out
238
+ last_time_seen = ts.data["time"]
239
+ # close the trajectory file for and delete the original universe
240
+ u0.trajectory.close()
241
+ del u0
242
+ for traj, sl in zip(trajs[1:], slices[1:]):
243
+ u = mda.Universe(traj.structure_file, *traj.trajectory_files)
244
+ u = _attach_mda_trafos_to_universe(
245
+ universe=u,
246
+ mda_transformations=self.mda_transformations,
247
+ mda_transformations_setup_func=self.mda_transformations_setup_func,
248
+ )
249
+ start, stop, step = sl
250
+ for ts in u.trajectory[start:stop:step]:
251
+ if remove_double_frames and (last_time_seen is not None):
252
+ if last_time_seen == ts.data["time"]:
253
+ # this is a no-op, as they are they same...
254
+ # last_time_seen = ts.data["time"]
255
+ continue # skip this timestep/go to next iteration
256
+ if (self.invert_v_for_negative_step and step < 0
257
+ and ts.has_velocities):
258
+ u.atoms.velocities *= -1
259
+ W.write(u.atoms)
260
+ if remove_double_frames:
261
+ last_time_seen = ts.data["time"]
262
+ # make sure MDAnalysis closes the underlying trajectory file
263
+ u.trajectory.close()
264
+ del u # and delete the universe just because we can
265
+ # return (file paths to) the finished trajectory
266
+ return Trajectory(tra_out, struct_out)
267
+
268
+ @_is_documented_by(concatenate)
269
+ # pylint: disable-next=missing-function-docstring
270
+ async def concatenate_async(self, trajs: "list[Trajectory]",
271
+ slices: "list[tuple]", tra_out: str,
272
+ struct_out: typing.Optional[str] = None,
273
+ overwrite: bool = False,
274
+ remove_double_frames: bool = True) -> Trajectory:
275
+ concat_fx = functools.partial(self.concatenate,
276
+ trajs=trajs,
277
+ slices=slices,
278
+ tra_out=tra_out,
279
+ struct_out=struct_out,
280
+ overwrite=overwrite,
281
+ remove_double_frames=remove_double_frames,
282
+ )
283
+ loop = asyncio.get_running_loop()
284
+ async with _SEMAPHORES["MAX_FILES_OPEN"]:
285
+ async with _SEMAPHORES["MAX_PROCESS"]:
286
+ with ThreadPoolExecutor(max_workers=1,
287
+ thread_name_prefix="concat_thread",
288
+ ) as pool:
289
+ return await loop.run_in_executor(pool, concat_fx)
290
+
291
+
292
+ class FrameExtractor(abc.ABC):
293
+ """
294
+ Abstract base class for FrameExtractors.
295
+
296
+ Implements the `extract` method which is common in all FrameExtractors.
297
+ Subclasses only need to implement `apply_modification` which is called by
298
+ `extract` to modify the frame just before writing it out.
299
+ """
300
+
301
+ # extract a single frame with given idx from a trajectory and write it out
302
+ # simplest case is without modification, but useful modifications are e.g.
303
+ # with inverted velocities, with random Maxwell-Boltzmann velocities, etc.
304
+
305
+ def __init__(
306
+ self,
307
+ mda_transformations: typing.Optional[list[typing.Callable]] = None,
308
+ mda_transformations_setup_func: typing.Optional[typing.Callable] = None,
309
+ ) -> None:
310
+ """
311
+ Initialize a :class:`FrameExtractor`.
312
+
313
+ Parameters
314
+ ----------
315
+ mda_transformations : list of callables, optional
316
+ If given will be added as a list of transformations to the
317
+ MDAnalysis universe as
318
+ ``universe.trajectory.add_transformation(*mda_transformations)``.
319
+ See the ``mda_transformations_setup_func`` argument if your
320
+ transformations need additional universe-dependant arguments, e.g.
321
+ atomgroups from the universe.
322
+ See https://docs.mdanalysis.org/stable/documentation_pages/trajectory_transformations.html
323
+ for more on MDAnalysis transformations.
324
+ mda_transformations_setup_func: callable, optional
325
+ If given will be called to attach user-defined MDAnalysis
326
+ transformations to the universe. The function must take a universe
327
+ as argument and return the universe with attached transformations.
328
+ I.e. it is expected that the function calls
329
+ ``universe.trajectory.add_transformations(*list_of_trafos)``
330
+ after defining ``list_of_trafos`` (potentially depending on the
331
+ universe or atomgroups therein) and then finally returns the
332
+ universe with trafos.
333
+ See https://docs.mdanalysis.org/stable/documentation_pages/trajectory_transformations.html
334
+ for more on MDAnalysis transformations.
335
+ """
336
+ if (mda_transformations is not None
337
+ and mda_transformations_setup_func is not None):
338
+ raise ValueError("`mda_transformations` and "
339
+ "`mda_transformations_setup_func` are mutually "
340
+ "exclusive, but both were given."
341
+ )
342
+ self.mda_transformations = mda_transformations
343
+ self.mda_transformations_setup_func = mda_transformations_setup_func
344
+
345
+ @abc.abstractmethod
346
+ def apply_modification(self,
347
+ universe: mda.Universe,
348
+ ts: mda_Timestep,
349
+ ):
350
+ """
351
+ Apply modification to selected frame (timestep/universe).
352
+
353
+ This function will be called when the current timestep is at the
354
+ chosen frame index and is expected to apply the subclass specific
355
+ modifications to the frame via modifying the mdanalysis timestep and
356
+ universe objects **inplace**.
357
+ After this function finishes the frame is written out, i.e. with any
358
+ potential modifications applied.
359
+ No return value is expected or considered from this method, the
360
+ modifications of the timestep/universe are nonlocal anyway.
361
+
362
+ Parameters
363
+ ----------
364
+ universe : MDAnalysis.core.universe.Universe
365
+ The mdanalysis universe associated with the trajectory.
366
+ ts : MDAnalysis.coordinates.base.Timestep
367
+ The mdanalysis timestep of the frame to extract.
368
+ """
369
+ raise NotImplementedError
370
+
371
+ def extract(self, outfile, traj_in: Trajectory, idx: int,
372
+ struct_out=None, overwrite: bool = False) -> Trajectory:
373
+ """
374
+ Extract a single frame from given trajectory and write it out.
375
+
376
+ Parameters
377
+ ----------
378
+ outfile : str
379
+ Absolute or relative path to the output trajectory. Expected to be
380
+ with file ending, e.g. "traj.trr".
381
+ traj_in : Trajectory
382
+ Input trajectory from which we will extract the frame at `idx`.
383
+ idx : int
384
+ Index of the frame to extract in `traj_in`.
385
+ struct_out : str, optional
386
+ None, or absolute or relative path to a structure file,
387
+ by default None. If not None we will use the given file as
388
+ structure file for the returned trajectory object, else we use the
389
+ structure file of `traj_in`.
390
+ overwrite : bool, optional
391
+ Whether to overwrite `outfile` if it exists, by default False.
392
+
393
+ Returns
394
+ -------
395
+ Trajectory
396
+ Trajectory object holding a trajectory with the extracted frame.
397
+
398
+ Raises
399
+ ------
400
+ FileExistsError
401
+ If `outfile` exists and `overwrite=False`.
402
+ FileNotFoundError
403
+ If `struct_out` is given but does not exist.
404
+ """
405
+ # TODO: should we check that idx is an idx, i.e. an int?
406
+ # TODO: make it possible to select a subset of atoms to write out
407
+ # and also for modification?
408
+ # TODO: should we make it possible to extract multiple frames, i.e.
409
+ # enable the use of slices (and iterables of indices?)
410
+ outfile = os.path.relpath(outfile)
411
+ if os.path.exists(outfile) and not overwrite:
412
+ raise FileExistsError(f"overwrite=False but outfile={outfile} exists.")
413
+ struct_out = (traj_in.structure_file if struct_out is None
414
+ else os.path.relpath(struct_out))
415
+ if not os.path.isfile(struct_out):
416
+ # although we would expect that it exists if it comes from an
417
+ # existing traj, we still check to catch other unrelated issues :)
418
+ raise FileNotFoundError("Output structure file must exist."
419
+ + f"(given struct_out is {struct_out})."
420
+ )
421
+ u = mda.Universe(traj_in.structure_file, *traj_in.trajectory_files)
422
+ u = _attach_mda_trafos_to_universe(
423
+ universe=u,
424
+ mda_transformations=self.mda_transformations,
425
+ mda_transformations_setup_func=self.mda_transformations_setup_func,
426
+ )
427
+ with mda.Writer(outfile, n_atoms=u.trajectory.n_atoms) as W:
428
+ ts = u.trajectory[idx]
429
+ self.apply_modification(u, ts)
430
+ W.write(u.atoms)
431
+ # make sure MDAnalysis closes the underlying trajectory files
432
+ u.trajectory.close()
433
+ del u
434
+ return Trajectory(trajectory_files=outfile, structure_file=struct_out)
435
+
436
+ @_is_documented_by(extract)
437
+ # pylint: disable-next=missing-function-docstring
438
+ async def extract_async(self, outfile, traj_in: Trajectory, idx: int,
439
+ struct_out=None, overwrite: bool = False) -> Trajectory:
440
+ extract_fx = functools.partial(self.extract,
441
+ outfile=outfile,
442
+ traj_in=traj_in,
443
+ idx=idx,
444
+ struct_out=struct_out,
445
+ overwrite=overwrite,
446
+ )
447
+ loop = asyncio.get_running_loop()
448
+ async with _SEMAPHORES["MAX_FILES_OPEN"]:
449
+ async with _SEMAPHORES["MAX_PROCESS"]:
450
+ with ThreadPoolExecutor(max_workers=1,
451
+ thread_name_prefix="concat_thread",
452
+ ) as pool:
453
+ return await loop.run_in_executor(pool, extract_fx)
454
+
455
+
456
+ class NoModificationFrameExtractor(FrameExtractor):
457
+ """Extract a frame from a trajectory, write it out without modification."""
458
+
459
+ def apply_modification(self,
460
+ universe: mda.Universe,
461
+ ts: mda_Timestep,
462
+ ):
463
+ """
464
+ Apply no modification to the extracted frame.
465
+
466
+ Parameters
467
+ ----------
468
+ universe : MDAnalysis.core.universe.Universe
469
+ The mdanalysis universe associated with the trajectory.
470
+ ts : MDAnalysis.coordinates.base.Timestep
471
+ The mdanalysis timestep of the frame to extract.
472
+ """
473
+
474
+
475
+ class InvertedVelocitiesFrameExtractor(FrameExtractor):
476
+ """
477
+ Extract a frame from a trajectory, write it out with inverted velocities.
478
+ """
479
+
480
+ def apply_modification(self,
481
+ universe: mda.Universe,
482
+ ts: mda_Timestep,
483
+ ):
484
+ """
485
+ Invert all momenta of the extracted frame.
486
+
487
+ Parameters
488
+ ----------
489
+ universe : MDAnalysis.core.universe.Universe
490
+ The mdanalysis universe associated with the trajectory.
491
+ ts : MDAnalysis.coordinates.base.Timestep
492
+ The mdanalysis timestep of the frame to extract.
493
+ """
494
+ ts.velocities *= -1.
495
+
496
+
497
+ class RandomVelocitiesFrameExtractor(FrameExtractor):
498
+ """
499
+ Extract a frame from a trajectory, write it out with randomized velocities.
500
+
501
+ Attributes
502
+ ----------
503
+ T : float
504
+ Temperature of the Maxwell-Boltzmann distribution for velocity
505
+ generation, in Kelvin.
506
+ """
507
+
508
+ def __init__(
509
+ self,
510
+ T: float,
511
+ mda_transformations: typing.Optional[list[typing.Callable]] = None,
512
+ mda_transformations_setup_func: typing.Optional[typing.Callable] = None,
513
+ ) -> None:
514
+ """
515
+ Initialize a :class:`RandomVelocitiesFrameExtractor`.
516
+
517
+ Parameters
518
+ ----------
519
+ T : float
520
+ Temperature of the Maxwell-Boltzmann distribution, in Kelvin.
521
+ mda_transformations : list of callables, optional
522
+ If given will be added as a list of transformations to the
523
+ MDAnalysis universe as
524
+ ``universe.trajectory.add_transformation(*mda_transformations)``.
525
+ See the ``mda_transformations_setup_func`` argument if your
526
+ transformations need additional universe-dependant arguments, e.g.
527
+ atomgroups from the universe.
528
+ See https://docs.mdanalysis.org/stable/documentation_pages/trajectory_transformations.html
529
+ for more on MDAnalysis transformations.
530
+ mda_transformations_setup_func: callable, optional
531
+ If given will be called to attach user-defined MDAnalysis
532
+ transformations to the universe. The function must take a universe
533
+ as argument and return the universe with attached transformations.
534
+ I.e. it is expected that the function calls
535
+ ``universe.trajectory.add_transformations(*list_of_trafos)``
536
+ after defining ``list_of_trafos`` (potentially depending on the
537
+ universe or atomgroups therein) and then finally returns the
538
+ universe with trafos.
539
+ See https://docs.mdanalysis.org/stable/documentation_pages/trajectory_transformations.html
540
+ for more on MDAnalysis transformations.
541
+ """
542
+ super().__init__(
543
+ mda_transformations=mda_transformations,
544
+ mda_transformations_setup_func=mda_transformations_setup_func,
545
+ )
546
+ self.T = T # in K
547
+ self._rng = np.random.default_rng()
548
+
549
+ def apply_modification(self,
550
+ universe: mda.Universe,
551
+ ts: mda_Timestep,
552
+ ):
553
+ """
554
+ Draw random Maxwell-Boltzmann velocities for extracted frame.
555
+
556
+ Parameters
557
+ ----------
558
+ universe : MDAnalysis.core.universe.Universe
559
+ The mdanalysis universe associated with the trajectory.
560
+ ts : MDAnalysis.coordinates.base.Timestep
561
+ The mdanalysis timestep of the frame to extract.
562
+ """
563
+ # m is in units of g / mol
564
+ # v should be in units of \AA / ps = 100 m / s
565
+ # which means m [10**-3 kg / mol] v**2 [10000 (m/s)**2]
566
+ # is in units of [ 10 kg m**s / (mol * s**2) ]
567
+ # so we use R = N_A * k_B [J / (mol * K) = kg m**2 / (s**2 * mol * K)]
568
+ # and add in a factor 10 to get 1/σ**2 = m / (k_B * T)
569
+ # in the correct units
570
+ scale = np.empty((ts.n_atoms, 3), dtype=np.float64)
571
+ s1d = np.sqrt((self.T * constants.R * 0.1)
572
+ / universe.atoms.masses
573
+ )
574
+ # sigma is the same for all 3 cartesian dimensions
575
+ for i in range(3):
576
+ scale[:, i] = s1d
577
+ ts.velocities = self._rng.normal(loc=0, scale=scale)