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/__init__.py +18 -0
- asyncmd/_config.py +26 -0
- asyncmd/_version.py +75 -0
- asyncmd/config.py +203 -0
- asyncmd/gromacs/__init__.py +16 -0
- asyncmd/gromacs/mdconfig.py +351 -0
- asyncmd/gromacs/mdengine.py +1127 -0
- asyncmd/gromacs/utils.py +197 -0
- asyncmd/mdconfig.py +440 -0
- asyncmd/mdengine.py +100 -0
- asyncmd/slurm.py +1199 -0
- asyncmd/tools.py +86 -0
- asyncmd/trajectory/__init__.py +25 -0
- asyncmd/trajectory/convert.py +577 -0
- asyncmd/trajectory/functionwrapper.py +556 -0
- asyncmd/trajectory/propagate.py +937 -0
- asyncmd/trajectory/trajectory.py +1103 -0
- asyncmd/utils.py +148 -0
- asyncmd-0.3.2.dist-info/LICENSE +232 -0
- asyncmd-0.3.2.dist-info/METADATA +179 -0
- asyncmd-0.3.2.dist-info/RECORD +23 -0
- asyncmd-0.3.2.dist-info/WHEEL +5 -0
- asyncmd-0.3.2.dist-info/top_level.txt +1 -0
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)
|