asyncmd 0.3.3__py3-none-any.whl → 0.4.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.
asyncmd/gromacs/utils.py CHANGED
@@ -12,6 +12,11 @@
12
12
  #
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with asyncmd. If not, see <https://www.gnu.org/licenses/>.
15
+ """
16
+ This file implements gromacs-specific variants of the utility functions related to MD usage.
17
+
18
+ The general variants of these functions can be found in asyncmd.utils.
19
+ """
15
20
  import os
16
21
  import logging
17
22
  import aiofiles.os
@@ -48,30 +53,42 @@ def nstout_from_mdp(mdp: MDP, traj_type: str = "TRR") -> int:
48
53
  Raised when the given MDP would result in no output for the given
49
54
  trajectory format `traj_type`.
50
55
  """
56
+ def get_value_from_mdp(k):
57
+ try:
58
+ v = mdp[k]
59
+ except KeyError:
60
+ # not set, defaults to 0
61
+ v = float("inf")
62
+ else:
63
+ # need to check for 0 (== no output!) in case somone puts the
64
+ # defaults (or reads an mdout.mdp where gmx lists all the defaults)
65
+ if not v:
66
+ v = float("inf")
67
+ return v
68
+
51
69
  if traj_type.upper() == "TRR":
52
- keys = ["nstxout", "nstvout", "nstfout"]
70
+ keys = ["nstxout"]
53
71
  elif traj_type.upper() == "XTC":
54
72
  keys = ["nstxout-compressed", "nstxtcout"]
55
73
  else:
56
74
  raise ValueError("traj_type must be one of 'TRR' or 'XTC'.")
57
-
58
75
  vals = []
59
76
  for k in keys:
60
- try:
61
- # need to check for 0 (== no output!) in case somone puts the
62
- # defaults (or reads an mdout.mdp where gmx lists all the defaults)
63
- v = mdp[k]
64
- if v == 0:
65
- v = float("inf")
66
- vals += [v]
67
- except KeyError:
68
- # not set, defaults to 0, so we ignore it
69
- pass
70
-
71
- nstout = min(vals, default=None)
72
- if (nstout is None) or (nstout == float("inf")):
77
+ vals += [get_value_from_mdp(k=k)]
78
+ if (nstout := min(vals)) == float("inf"):
73
79
  raise ValueError(f"The MDP you passed results in no {traj_type} "
74
- +"trajectory output.")
80
+ + "trajectory output.")
81
+ if traj_type.upper == "TRR":
82
+ # additional checks that nstvout and nstfout are multiples of nstxout
83
+ # (if they are defined)
84
+ additional_keys = ["nstvout", "nstfout"]
85
+ for k in additional_keys:
86
+ if (v := get_value_from_mdp(k=k)) != float("inf"):
87
+ if v % nstout:
88
+ logger.warning("%s trajectory output is not a multiple of "
89
+ "the nstxout frequency (%s=%d, nstxout=%d).",
90
+ k, k, v, nstout)
91
+
75
92
  return nstout
76
93
 
77
94
 
@@ -128,7 +145,7 @@ async def get_all_file_parts(folder: str, deffnm: str, file_ending: str) -> "lis
128
145
  """
129
146
  def partnum_suffix(num):
130
147
  # construct gromacs num part suffix from simulation_part
131
- num_suffix = ".part{:04d}".format(num)
148
+ num_suffix = f".part{num:04d}"
132
149
  return num_suffix
133
150
 
134
151
  if not file_ending.startswith("."):
asyncmd/mdconfig.py CHANGED
@@ -12,160 +12,51 @@
12
12
  #
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with asyncmd. If not, see <https://www.gnu.org/licenses/>.
15
+ """
16
+ This module contains abstract base classes to use for MD config file parsing.
17
+
18
+ It contains the MDConfig class, which only serves to define the interface and
19
+ the LineBasedMDConfig class, which implements useful methods to parse MD configuration
20
+ files in which options never span over multiple lines, i.e. in which every line
21
+ contains only one (or multiple) options.
22
+ """
15
23
  import os
16
24
  import abc
17
- import typing
18
25
  import shutil
19
26
  import logging
20
27
  import collections
21
28
 
29
+ from .tools import TypedFlagChangeList
22
30
 
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- class FlagChangeList(collections.abc.MutableSequence):
27
- """A list that knows if it has been changed after initializing."""
28
-
29
- def __init__(self, data: list) -> None:
30
- """
31
- Initialize a `FlagChangeList`.
32
-
33
- Parameters
34
- ----------
35
- data : list
36
- The data this `FlagChangeList` will hold.
37
-
38
- Raises
39
- ------
40
- TypeError
41
- Raised when data is not a :class:`list`.
42
- """
43
- if not isinstance(data, list):
44
- raise TypeError("FlagChangeList must be initialized with a list.")
45
- self._data = data
46
- self._changed = False
47
-
48
- @property
49
- def changed(self) -> bool:
50
- """
51
- Whether this `FlagChangeList` has been modified since creation.
52
-
53
- Returns
54
- -------
55
- bool
56
- """
57
- return self._changed
58
-
59
- def __repr__(self) -> str:
60
- return self._data.__repr__()
61
-
62
- def __getitem__(self, index: int) -> typing.Any:
63
- return self._data.__getitem__(index)
64
-
65
- def __len__(self) -> int:
66
- return self._data.__len__()
67
-
68
- def __setitem__(self, index: int, value) -> None:
69
- self._data.__setitem__(index, value)
70
- self._changed = True
71
-
72
- def __delitem__(self, index: int) -> None:
73
- self._data.__delitem__(index)
74
- self._changed = True
75
31
 
76
- def insert(self, index: int, value: typing.Any):
77
- """
78
- Insert `value` at position given by `index`.
79
-
80
- Parameters
81
- ----------
82
- index : int
83
- The index of the new value in the `FlagChangeList`.
84
- value : typing.Any
85
- The value to insert into this `FlagChangeList`.
86
- """
87
- self._data.insert(index, value)
88
- self._changed = True
89
-
90
-
91
- class TypedFlagChangeList(FlagChangeList):
92
- """
93
- A :class:`FlagChangeList` with an ensured type for individual list items.
94
- """
95
-
96
- def __init__(self, data: typing.Iterable, dtype) -> None:
97
- """
98
- Initialize a `TypedFlagChangeList`.
99
-
100
- Parameters
101
- ----------
102
- data : Iterable
103
- (Initial) data for this `TypedFlagChangeList`.
104
- dtype : Callable datatype
105
- The datatype for all entries in this `TypedFlagChangeList`. Will be
106
- called on every value seperately and is expected to convert to the
107
- desired datatype.
108
- """
109
- self._dtype = dtype # set first to use in _convert_type method
110
- if getattr(data, '__len__', None) is None:
111
- # convienience for singular options,
112
- # if it has no len attribute we assume it is the only item
113
- data = [data]
114
- elif isinstance(data, str):
115
- # strings have a length but we still do not want to split them into
116
- # single letters, so just put a list around
117
- data = [data]
118
- typed_data = [self._convert_type(v, index=i)
119
- for i, v in enumerate(data)]
120
- super().__init__(data=typed_data)
121
-
122
- def _convert_type(self, value, index=None):
123
- # here we ignore index, but passing it should in principal make it
124
- # possible to use different dtypes for different indices
125
- return self._dtype(value)
126
-
127
- def __setitem__(self, index: int, value) -> None:
128
- typed_value = self._convert_type(value, index=index)
129
- self._data.__setitem__(index, typed_value)
130
- self._changed = True
131
-
132
- def insert(self, index: int, value) -> None:
133
- """
134
- Insert `value` at position given by `index`.
135
-
136
- Parameters
137
- ----------
138
- index : int
139
- The index of the new value in the `TypedFlagChangeList`.
140
- value : typing.Any
141
- The value to insert into this `TypedFlagChangeList`.
142
- """
143
- typed_value = self._convert_type(value, index=index)
144
- self._data.insert(index, typed_value)
145
- self._changed = True
32
+ logger = logging.getLogger(__name__)
146
33
 
147
34
 
148
- # NOTE: only to define the interface
149
35
  class MDConfig(collections.abc.MutableMapping):
36
+ """Abstract base class only to define the interface."""
37
+
150
38
  @abc.abstractmethod
151
39
  def parse(self):
152
- # should read original file and populate self with key, value pairs
40
+ """Should read original file and populate self with key, value pairs."""
153
41
  raise NotImplementedError
154
42
 
155
43
  @abc.abstractmethod
156
44
  def write(self, outfile):
157
- # write out current config stored in self to outfile
45
+ """Should write out current config stored in self to outfile."""
158
46
  raise NotImplementedError
159
47
 
160
48
 
161
49
  class LineBasedMDConfig(MDConfig):
162
- # abstract base class for line based parsing and writing,
163
- # subclasses must implement `_parse_line()` method and should set the
164
- # appropriate separator characters for their line format
165
- # We assume that every line/option can be parsed and written on its own!
166
- # We assume the order of the options in the written file is not relevant!
167
- # We represent every line/option with a key (str), list of values pair
168
- # values can have a specific type (e.g. int or float) or default to str.
50
+ """
51
+ Abstract base class for line based parsing and writing.
52
+
53
+ Subclasses must implement `_parse_line()` method and should set the
54
+ appropriate separator characters for their line format.
55
+ We assume that every line/option can be parsed and written on its own!
56
+ We assume the order of the options in the written file is not relevant!
57
+ We represent every line/option with a key (str), list of values pair.
58
+ Values can have a specific type (e.g. int or float) or default to str.
59
+ """
169
60
  # NOTE: Initially written for gmx, but we already had e.g. namd in mind and
170
61
  # tried to make this as general as possible
171
62
 
@@ -177,11 +68,11 @@ class LineBasedMDConfig(MDConfig):
177
68
  # use these to specify config parameters that are of type int or float
178
69
  # parsed lines with dict key matching will then be converted
179
70
  # any lines not matching will be left in their default str type
180
- _FLOAT_PARAMS = [] # can have multiple values per config option
181
- _FLOAT_SINGLETON_PARAMS = [] # must have one value per config option
182
- _INT_PARAMS = [] # multiple int per option
183
- _INT_SINGLETON_PARAMS = [] # one int per option
184
- _STR_SINGLETON_PARAMS = [] # strings with only one value per option
71
+ _FLOAT_PARAMS: list[str] = [] # can have multiple values per config option
72
+ _FLOAT_SINGLETON_PARAMS: list[str] = [] # must have one value per config option
73
+ _INT_PARAMS: list[str] = [] # multiple int per option
74
+ _INT_SINGLETON_PARAMS: list[str] = [] # one int per option
75
+ _STR_SINGLETON_PARAMS: list[str] = [] # strings with only one value per option
185
76
  # NOTE on SPECIAL_PARAM_DISPATCH
186
77
  # can be used to set custom type convert functions on a per parameter basis
187
78
  # the key must match the key in the dict for in the parsed line,
@@ -192,7 +83,7 @@ class LineBasedMDConfig(MDConfig):
192
83
  # new values into the expected FlagChangeList format
193
84
  # [note that it is probably easiest to subclass TypedFlagChangeList and
194
85
  # overwrite only the '_check_type()' method]
195
- _SPECIAL_PARAM_DISPATCH = {}
86
+ _SPECIAL_PARAM_DISPATCH: dict[str, collections.abc.Callable] = {}
196
87
 
197
88
  def __init__(self, original_file: str) -> None:
198
89
  """
@@ -203,7 +94,7 @@ class LineBasedMDConfig(MDConfig):
203
94
  original_file : str
204
95
  Path to original config file (absolute or relative).
205
96
  """
206
- self._config = {}
97
+ self._config: dict[str, TypedFlagChangeList | int | float | str] = {}
207
98
  self._changed = False
208
99
  self._type_dispatch = self._construct_type_dispatch()
209
100
  # property to set/check file and parse to config dictionary all in one
@@ -218,8 +109,7 @@ class LineBasedMDConfig(MDConfig):
218
109
  # singleton vals, i.e. "val" instead of ["val"]
219
110
  if isinstance(val, str) or getattr(val, '__len__', None) is None:
220
111
  return dtype(val)
221
- else:
222
- return dtype(val[0])
112
+ return dtype(val[0])
223
113
 
224
114
  # construct type conversion dispatch
225
115
  type_dispatch = collections.defaultdict(
@@ -310,14 +200,14 @@ class LineBasedMDConfig(MDConfig):
310
200
  def __len__(self) -> int:
311
201
  return self._config.__len__()
312
202
 
313
- def __repr__(self) -> str:
203
+ def __repr__(self) -> str: # pragma: no cover
314
204
  return str({"changed": self._changed,
315
205
  "original_file": self.original_file,
316
206
  "content": self._config.__repr__(),
317
207
  }
318
208
  )
319
209
 
320
- def __str__(self) -> str:
210
+ def __str__(self) -> str: # pragma: no cover
321
211
  repr_str = (f"{type(self)} has been changed since parsing: "
322
212
  + f"{self._changed}\n"
323
213
  )
@@ -362,15 +252,14 @@ class LineBasedMDConfig(MDConfig):
362
252
  # NOTE: we default to False, i.e. we expect that anything that
363
253
  # does not have a self.changed attribute is not a container
364
254
  # and we (the dictionary) would know that it changed
365
- return self._changed or any([getattr(v, "changed", False)
366
- for v in self._config.values()]
367
- )
255
+ return self._changed or any(getattr(v, "changed", False)
256
+ for v in self._config.values())
368
257
 
369
258
  def parse(self):
370
259
  """Parse the current ``self.original_file`` to update own state."""
371
- with open(self.original_file, "r") as f:
260
+ with open(self.original_file, "r", encoding="locale") as f:
372
261
  # NOTE: we split at newlines on all platforms by iterating over the
373
- # file, i.e. python takes care of the differnt platforms and
262
+ # file, i.e. python takes care of the different platforms and
374
263
  # newline chars for us :)
375
264
  parsed = {}
376
265
  for line in f:
@@ -384,7 +273,7 @@ class LineBasedMDConfig(MDConfig):
384
273
  # as it should be
385
274
  pass
386
275
  else:
387
- # warn that we will only keep the last occurenc of key
276
+ # warn that we will only keep the last occurrence of key
388
277
  logger.warning("Parsed duplicate configuration option "
389
278
  "(%s). Last values encountered take "
390
279
  "precedence.", key)
@@ -421,20 +310,21 @@ class LineBasedMDConfig(MDConfig):
421
310
  lines = []
422
311
  for key, value in self._config.items():
423
312
  line = f"{key}{self._KEY_VALUE_SEPARATOR}"
424
- try:
425
- if len(value) >= 0:
426
- if isinstance(value, str):
427
- # it is a string singleton option
428
- line += f"{value}"
429
- else:
430
- line += self._INTER_VALUE_CHAR.join(str(v)
431
- for v in value
432
- )
433
- except TypeError:
434
- # not a Sequence/Iterable or string,
435
- # i.e. (probably) one of the float/int singleton options
313
+ if isinstance(value, (str, float, int)):
314
+ # it is a string/float/int singleton option
436
315
  line += f"{value}"
316
+ else:
317
+ # not a singleton, so lets try to iterate over it
318
+ try:
319
+ line += self._INTER_VALUE_CHAR.join(str(v)
320
+ for v in value
321
+ )
322
+ except TypeError:
323
+ # Note: need this except to catch user-added types
324
+ # (via special param dispatch), that are not
325
+ # str/float/int singletons but also not iterable
326
+ line += f"{value}"
437
327
  lines += [line]
438
328
  # concatenate the lines and write out at once
439
- with open(outfile, "w") as f:
329
+ with open(outfile, "w", encoding="locale") as f:
440
330
  f.write("\n".join(lines))
asyncmd/mdengine.py CHANGED
@@ -12,18 +12,21 @@
12
12
  #
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with asyncmd. If not, see <https://www.gnu.org/licenses/>.
15
+ """
16
+ This module contains the abstract base class defining the interface for all MDEngines.
17
+
18
+ It also defines the commonly used exceptions for MDEngines.
19
+ """
15
20
  import abc
16
21
  from .trajectory.trajectory import Trajectory
17
22
 
18
23
 
19
24
  class EngineError(Exception):
20
25
  """Exception raised when something goes wrong with the (MD)-Engine."""
21
- pass
22
26
 
23
27
 
24
28
  class EngineCrashedError(EngineError):
25
29
  """Exception raised when the (MD)-Engine crashes during a run."""
26
- pass
27
30
 
28
31
 
29
32
  class MDEngine(abc.ABC):
@@ -33,68 +36,146 @@ class MDEngine(abc.ABC):
33
36
  @abc.abstractmethod
34
37
  async def apply_constraints(self, conf_in: Trajectory,
35
38
  conf_out_name: str) -> Trajectory:
36
- # apply constraints to given conf_in, write conf_out_name and return it
39
+ """
40
+ Apply constraints to given conf_in, write conf_out_name and return it.
41
+
42
+ Parameters
43
+ ----------
44
+ conf_in : Trajectory
45
+ conf_out_name : str
46
+
47
+ Returns
48
+ -------
49
+ Trajectory
50
+ """
37
51
  raise NotImplementedError
38
52
 
39
53
  @abc.abstractmethod
40
- # TODO: think about the most general interface!
41
- # NOTE: We assume that we do not change the system for/in one engine,
42
- # i.e. .top, .ndx, mdp-object, ...?! should go into __init__
43
54
  async def prepare(self, starting_configuration: Trajectory, workdir: str,
44
55
  deffnm: str) -> None:
56
+ """
57
+ Prepare the engine to run a MD from starting_configuration.
58
+
59
+ NOTE: We assume that we do not change the system for/in one engine,
60
+ i.e. .top, .ndx, mdp-object, ...?! should go into __init__.
61
+
62
+ Parameters
63
+ ----------
64
+ starting_configuration : Trajectory
65
+ The initial configuration.
66
+ workdir : str
67
+ The directory in which the MD will be performed.
68
+ deffnm : str
69
+ The standard filename to use for this MD run.
70
+ """
45
71
  raise NotImplementedError
46
72
 
47
73
  @abc.abstractmethod
48
- # TODO: should this be a classmethod?
49
74
  #@classmethod
50
75
  async def prepare_from_files(self, workdir: str, deffnm: str) -> None:
51
- # this should prepare the engine to continue a previously stopped simulation
52
- # starting with the last trajectory part in workdir that is compatible with deffnm
76
+ """
77
+ Prepare the engine to continue a previously stopped simulation starting
78
+ with the last trajectory part in workdir that is compatible with deffnm.
79
+
80
+ NOTE: This can not be a classmethod (reliably) because we set top/ndx/
81
+ mdconfig/etc in '__init__'.
82
+
83
+ Parameters
84
+ ----------
85
+ workdir : str
86
+ The directory in which the MD will be/ was previously performed.
87
+ deffnm : str
88
+ The standard filename to use for this MD run.
89
+ """
53
90
  raise NotImplementedError
54
91
 
55
92
  @abc.abstractmethod
56
- async def run_walltime(self, walltime: float) -> Trajectory:
57
- # run for specified walltime
58
- # NOTE: must be possible to run this multiple times after preparing once!
93
+ async def run_walltime(self, walltime: float, max_steps: int | None = None,
94
+ ) -> Trajectory | None:
95
+ """
96
+ Run for specified walltime.
97
+
98
+ NOTE: Must be possible to run this multiple times after preparing once!
99
+
100
+ It is optional (but recommended if possible) for engines to respect the
101
+ ``max_steps`` argument. I.e. terminating upon reaching max_steps is
102
+ optional and no code should rely on it. See the :meth:`run_steps` if a
103
+ fixed number of integration steps is required.
104
+
105
+ Return None if no integration is needed because max_steps integration
106
+ steps have already been performed.
107
+
108
+ Parameters
109
+ ----------
110
+ walltime : float
111
+ Walltime in hours.
112
+ max_steps : int | None, optional
113
+ If not None, (optionally) terminate when max_steps integration steps
114
+ in total are reached, also if this is before walltime is reached.
115
+ By default None.
116
+
117
+ Returns
118
+ -------
119
+ Trajectory | None
120
+ """
59
121
  raise NotImplementedError
60
122
 
61
123
  @abc.abstractmethod
62
124
  async def run_steps(self, nsteps: int,
63
- steps_per_part: bool = False) -> Trajectory:
64
- # run for specified number of steps
65
- # NOTE: not sure if we need it, but could be useful
66
- # NOTE: make sure we can run multiple times after preparing once!
125
+ steps_per_part: bool = False) -> Trajectory | None:
126
+ """
127
+ Run for a specified number of integration steps.
128
+
129
+ Return None if no integration is needed because nsteps integration steps
130
+ have already been performed.
131
+
132
+ NOTE: Make sure we can run multiple times after preparing once!
133
+
134
+ Parameters
135
+ ----------
136
+ nsteps : int
137
+ steps_per_part : bool, optional
138
+ Count nsteps for this part/run or in total, by default False
139
+
140
+ Returns
141
+ -------
142
+ Trajectory | None
143
+ """
67
144
  raise NotImplementedError
68
145
 
69
- @abc.abstractproperty
70
- def current_trajectory(self) -> Trajectory:
71
- # return current trajectory: Trajectory or None
72
- # if we retun a Trajectory it is either what we are working on atm
146
+ @property
147
+ @abc.abstractmethod
148
+ def current_trajectory(self) -> Trajectory | None:
149
+ """
150
+ Return current trajectory: Trajectory or None.
151
+ """
152
+ # if we return a Trajectory it is either what we are working on atm
73
153
  # or the trajectory we finished last
74
154
  raise NotImplementedError
75
155
 
76
- @abc.abstractproperty
156
+ @property
157
+ @abc.abstractmethod
77
158
  def output_traj_type(self) -> str:
78
- # return a string with the ending (without ".") of the trajectory
79
- # type this engine uses
80
- # NOTE: this should not be implemented as a property in subclasses
81
- # as it must be available at the classlevel too
82
- # so cls.output_traj_type must also be the string
83
- # If you want/need to check the values (i.e. you would like to
84
- # execute code like in a property) have a look at the descriptor
85
- # implementation in gromacs/mdengine.py which checks for allowed
86
- # values (at least when set on an instance) but is accesible from
87
- # the class level too, e.g. like a 'classproperty' (which is not
88
- # a thing in python)
89
- raise NotImplementedError
159
+ """
160
+ Return a string with the ending (without ".") of the trajectory type this
161
+ engine uses.
90
162
 
91
- # TODO/FIXME: remove this function?
92
- # NOTE: I think we wont really need/use this anyway since the run_ funcs
93
- # are all awaitable
94
- @abc.abstractproperty
95
- def running(self) -> bool:
163
+ NOTE: This should not be implemented as a property in subclasses as it
164
+ must be available at the classlevel too, i.e. cls.output_traj_type must
165
+ also return the string.
166
+ So this should just be overwritten with a string with the correct value,
167
+ or if your engine supports multiple output_traj_types you should have a
168
+ look at the descriptor implementation in asyncmd/tools.py (and, e.g.,
169
+ used in asyncmd/gromacs/mdengine.py), which checks for allowed values
170
+ (at least when set on an instance) but is accessible from the class
171
+ level too, i.e. like a 'classproperty' (which is not a thing in python).
172
+ """
96
173
  raise NotImplementedError
97
174
 
98
- @abc.abstractproperty
175
+ @property
176
+ @abc.abstractmethod
99
177
  def steps_done(self) -> int:
178
+ """
179
+ Return the number of integration steps this engine has performed in total.
180
+ """
100
181
  raise NotImplementedError