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/slurm.py CHANGED
@@ -12,6 +12,18 @@
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 implementation of the classes to interact with Slurm.
17
+
18
+ The SlurmClusterMediator is a singleton class (handling all sacct calls in a
19
+ coordinated fashion) for all SlurmProcess instances.
20
+ The SlurmProcess is a drop-in replacement for asyncio.subprocess.Subprocess and
21
+ in this spirit this module also contains the function create_slurmprocess_submit,
22
+ which similarly to asyncio.create_subprocess_exec, creates a SlurmProcess and
23
+ directly submits the job.
24
+ Finally this module contains two functions to set the configuration of this module,
25
+ set_all_slurm_settings and set_slurm_settings.
26
+ """
15
27
  import asyncio
16
28
  import collections
17
29
  import logging
@@ -28,6 +40,7 @@ from .tools import (ensure_executable_available,
28
40
  remove_file_if_exist_async,
29
41
  remove_file_if_exist,
30
42
  )
43
+ from .tools import attach_kwargs_to_object as _attach_kwargs_to_object
31
44
  from ._config import _SEMAPHORES
32
45
 
33
46
 
@@ -108,8 +121,6 @@ class SlurmClusterMediator:
108
121
  success_to_fail_ratio : int
109
122
  Number of successful jobs we need to observe per node to decrease the
110
123
  failed job counter by one.
111
- exclude_nodes : list[str]
112
- List of nodes to exclude in job submissions.
113
124
 
114
125
  """
115
126
 
@@ -133,21 +144,7 @@ class SlurmClusterMediator:
133
144
  self._exclude_nodes: list[str] = []
134
145
  # make it possible to set any attribute via kwargs
135
146
  # check the type for attributes with default values
136
- dval = object()
137
- for kwarg, value in kwargs.items():
138
- cval = getattr(self, kwarg, dval)
139
- if cval is not dval:
140
- if isinstance(value, type(cval)):
141
- # value is of same type as default so set it
142
- setattr(self, kwarg, value)
143
- else:
144
- raise TypeError(f"Setting attribute {kwarg} with "
145
- + f"mismatching type ({type(value)}). "
146
- + f" Default type is {type(cval)}."
147
- )
148
- else:
149
- # not previously defined, so warn that we ignore it
150
- logger.warning("Ignoring unknown keyword-argument %s.", kwarg)
147
+ _attach_kwargs_to_object(obj=self, logger=logger, **kwargs)
151
148
  # this either checks for our defaults or whatever we just set via kwargs
152
149
  self.sacct_executable = ensure_executable_available(self.sacct_executable)
153
150
  self.sinfo_executable = ensure_executable_available(self.sinfo_executable)
@@ -332,12 +329,12 @@ class SlurmClusterMediator:
332
329
  # (note that one semaphore counts for 3 files!)
333
330
  await _SEMAPHORES["MAX_FILES_OPEN"].acquire()
334
331
  try:
335
- sacct_proc = await asyncio.subprocess.create_subprocess_exec(
332
+ sacct_proc = await asyncio.create_subprocess_exec(
336
333
  *shlex.split(sacct_cmd),
337
334
  stdout=asyncio.subprocess.PIPE,
338
335
  stderr=asyncio.subprocess.PIPE,
339
336
  close_fds=True,
340
- )
337
+ )
341
338
  stdout, stderr = await sacct_proc.communicate()
342
339
  sacct_return = stdout.decode()
343
340
  except asyncio.CancelledError as e:
@@ -624,11 +621,11 @@ class SlurmProcess:
624
621
  default as either specified in the sbatch script or the partition.
625
622
  sbatch_options : dict or None
626
623
  Dictionary of sbatch options, keys are long names for options,
627
- values are the correponding values. The keys/long names are given
628
- without the dashes, e.g. to specify "--mem=1024" the dictionary
629
- needs to be {"mem": "1024"}. To specify options without values use
630
- keys with empty strings as values, e.g. to specify "--contiguous"
631
- the dictionary needs to be {"contiguous": ""}.
624
+ values are the corresponding values. The keys/long names are given
625
+ without the dashes, e.g. to specify ``--mem=1024`` the dictionary
626
+ needs to be ``{"mem": "1024"}``. To specify options without values
627
+ use keys with empty strings as values, e.g. to specify
628
+ ``--contiguous`` the dictionary needs to be ``{"contiguous": ""}``.
632
629
  See the SLURM documentation for a full list of sbatch options
633
630
  (https://slurm.schedmd.com/sbatch.html).
634
631
  stdfiles_removal : str
@@ -649,21 +646,7 @@ class SlurmProcess:
649
646
  # we expect sbatch_script to be a path to a file
650
647
  # make it possible to set any attribute via kwargs
651
648
  # check the type for attributes with default values
652
- dval = object()
653
- for kwarg, value in kwargs.items():
654
- cval = getattr(self, kwarg, dval)
655
- if cval is not dval:
656
- if isinstance(value, type(cval)):
657
- # value is of same type as default so set it
658
- setattr(self, kwarg, value)
659
- else:
660
- raise TypeError(f"Setting attribute {kwarg} with "
661
- + f"mismatching type ({type(value)}). "
662
- + f" Default type is {type(cval)}."
663
- )
664
- else:
665
- # not previously defined, so warn that we ignore it
666
- logger.warning("Ignoring unknown keyword-argument %s.", kwarg)
649
+ _attach_kwargs_to_object(obj=self, logger=logger, **kwargs)
667
650
  # this either checks for our defaults or whatever we just set via kwargs
668
651
  ensure_executable_available(self.sbatch_executable)
669
652
  ensure_executable_available(self.scancel_executable)
@@ -892,13 +875,13 @@ class SlurmProcess:
892
875
  # Note: one semaphore counts for 3 open files!
893
876
  await _SEMAPHORES["MAX_FILES_OPEN"].acquire()
894
877
  try:
895
- sbatch_proc = await asyncio.subprocess.create_subprocess_exec(
878
+ sbatch_proc = await asyncio.create_subprocess_exec(
896
879
  *shlex.split(sbatch_cmd),
897
880
  stdout=asyncio.subprocess.PIPE,
898
881
  stderr=asyncio.subprocess.PIPE,
899
882
  cwd=self.workdir,
900
883
  close_fds=True,
901
- )
884
+ )
902
885
  stdout, stderr = await sbatch_proc.communicate()
903
886
  sbatch_return = stdout.decode()
904
887
  except asyncio.CancelledError as e:
@@ -1197,11 +1180,11 @@ async def create_slurmprocess_submit(jobname: str,
1197
1180
  default as either specified in the sbatch script or the partition.
1198
1181
  sbatch_options : dict or None
1199
1182
  Dictionary of sbatch options, keys are long names for options,
1200
- values are the correponding values. The keys/long names are given
1201
- without the dashes, e.g. to specify "--mem=1024" the dictionary
1202
- needs to be {"mem": "1024"}. To specify options without values use
1203
- keys with empty strings as values, e.g. to specify "--contiguous"
1204
- the dictionary needs to be {"contiguous": ""}.
1183
+ values are the corresponding values. The keys/long names are given
1184
+ without the dashes, e.g. to specify ``--mem=1024`` the dictionary
1185
+ needs to be ``{"mem": "1024"}``. To specify options without values
1186
+ use keys with empty strings as values, e.g. to specify
1187
+ ``--contiguous`` the dictionary needs to be ``{"contiguous": ""}``.
1205
1188
  See the SLURM documentation for a full list of sbatch options
1206
1189
  (https://slurm.schedmd.com/sbatch.html).
1207
1190
  stdfiles_removal : str
asyncmd/tools.py CHANGED
@@ -12,8 +12,33 @@
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 contains functions and classes (re)used internally in asyncmd.
17
+
18
+ These functions and classes are not (thought to be) exposed to the users but
19
+ instead intended to be (re)used in newly added asyncmd code.
20
+ This is also not the place for MD-related utility functions, for this see utils.py
21
+
22
+ Currently in here are:
23
+
24
+ - ensure_executable_available
25
+ - remove_file_if_exist and remove_file_if_exist_async
26
+ - attach_kwargs_to_object: a function to attach kwargs to an object as properties
27
+ or attributes. This does type checking and warns when previously unset things
28
+ are set. It is used, e.g., in the GmxEngine and SlurmProcess classes.
29
+ - DescriptorWithDefaultOnInstanceAndClass and DescriptorOutputTrajType: two descriptor
30
+ classes to make default values accessible on the class level but still enable checks
31
+ when setting on the instance level (like a property), used in the GmxEngine classes
32
+ but could/should be useful for any MDEngine class
33
+ - FlagChangeList (and its typed sibling): lists with some sugar to remember if
34
+ their content has changed
35
+
36
+ """
37
+ import collections
15
38
  import os
16
39
  import shutil
40
+ import logging
41
+ import typing
17
42
  import aiofiles
18
43
 
19
44
 
@@ -45,11 +70,11 @@ def ensure_executable_available(executable: str) -> str:
45
70
  executable = os.path.abspath(executable)
46
71
  if not os.access(executable, os.X_OK):
47
72
  raise ValueError(f"{executable} must be executable.")
48
- elif shutil.which(executable) is not None:
73
+ elif (which_exe := shutil.which(executable)) is not None:
49
74
  # see if we find it in $PATH
50
- executable = shutil.which(executable)
75
+ executable = which_exe
51
76
  else:
52
- raise ValueError(f"{executable} must be an existing path or accesible "
77
+ raise ValueError(f"{executable} must be an existing path or accessible "
53
78
  + "via the $PATH environment variable.")
54
79
  return executable
55
80
 
@@ -66,7 +91,6 @@ def remove_file_if_exist(f: str):
66
91
  try:
67
92
  os.remove(f)
68
93
  except FileNotFoundError:
69
- # TODO: should we info/warn if the file is not there?
70
94
  pass
71
95
 
72
96
 
@@ -82,5 +106,260 @@ async def remove_file_if_exist_async(f: str):
82
106
  try:
83
107
  await aiofiles.os.remove(f)
84
108
  except FileNotFoundError:
85
- # TODO: should we info/warn if the file is not there?
86
109
  pass
110
+
111
+
112
+ def attach_kwargs_to_object(obj, *, logger: logging.Logger,
113
+ **kwargs
114
+ ) -> None:
115
+ """
116
+ Set all kwargs as object attributes/properties, error on mismatching type.
117
+
118
+ Warn when we set an unknown (i.e. previously undefined attribute/property)
119
+
120
+ Parameters
121
+ ----------
122
+ obj : object
123
+ The object to attach the kwargs to.
124
+ logger: logging.Logger
125
+ The logger to use for logging.
126
+ **kwargs : dict
127
+ Zero to N keyword arguments.
128
+ """
129
+ dval = object()
130
+ for kwarg, value in kwargs.items():
131
+ if (cval := getattr(obj, kwarg, dval)) is not dval:
132
+ if isinstance(value, type(cval)):
133
+ # value is of same type as default so set it
134
+ setattr(obj, kwarg, value)
135
+ else:
136
+ raise TypeError(f"Setting attribute {kwarg} with "
137
+ + f"mismatching type ({type(value)}). "
138
+ + f" Default type is {type(cval)}."
139
+ )
140
+ else:
141
+ # not previously defined, so warn that we ignore it
142
+ logger.warning("Ignoring unknown keyword-argument %s.", kwarg)
143
+
144
+
145
+ class DescriptorWithDefaultOnInstanceAndClass:
146
+ """
147
+ A descriptor that makes the (default) value of the private attribute
148
+ ``_name`` of the class it is attached to accessible as ``name`` on both the
149
+ class and the instance level.
150
+ Accessing the default value works from the class-level, i.e. without
151
+ instantiating the object, but note that setting on the class level
152
+ overwrites the descriptor and does not call ``__set__``.
153
+ Setting from an instance calls __set__ and therefore only sets the attribute
154
+ for the given instance (and also runs potential checks done in ``__set__``).
155
+ Also see the python docs:
156
+ https://docs.python.org/3/howto/descriptor.html#customized-names
157
+ """
158
+ private_name: str
159
+
160
+ def __set_name__(self, owner, name: str) -> None:
161
+ self.private_name = "_" + name
162
+
163
+ def __get__(self, obj, objtype=None) -> typing.Any:
164
+ if obj is None:
165
+ # I (hejung) think if obj is None objtype will always be set
166
+ # to the class of the obj
167
+ obj = objtype
168
+ val = getattr(obj, self.private_name)
169
+ return val
170
+
171
+ def __set__(self, obj, val) -> None:
172
+ setattr(obj, self.private_name, val)
173
+
174
+
175
+ class DescriptorOutputTrajType(DescriptorWithDefaultOnInstanceAndClass):
176
+ """
177
+ Check the value given is in the set of allowed values before setting.
178
+
179
+ Used to check ``output_traj_type`` of MDEngines for consistency when setting.
180
+ """
181
+ # set of allowed values, e.g., trajectory file endings (without "." and all lower case)
182
+ ALLOWED_VALUES: set[str] = set()
183
+
184
+ def __set_name__(self, owner, name: str) -> None:
185
+ if not self.ALLOWED_VALUES:
186
+ # make sure we can only instantiate with ALLOWED_VALUES set,
187
+ # i.e. make this class a sort of ABC :)
188
+ raise NotImplementedError(f"Can not instantiate {type(self)} " # pragma: no cover
189
+ "without allowed trajectory types set. "
190
+ "Set ``ALLOWED_VALUES`` to a set of strings.")
191
+ super().__set_name__(owner, name)
192
+
193
+ def __set__(self, obj, val: str) -> None:
194
+ if (val := val.lower()) not in self.ALLOWED_VALUES:
195
+ raise ValueError("output_traj_type must be one of "
196
+ + f"{self.ALLOWED_VALUES}, but was {val}."
197
+ )
198
+ super().__set__(obj, val)
199
+
200
+ def __get__(self, obj, objtype=None) -> str:
201
+ return super().__get__(obj=obj, objtype=objtype)
202
+
203
+
204
+ class FlagChangeList(collections.abc.MutableSequence):
205
+ """A list that knows if it has been changed after initializing."""
206
+
207
+ def __init__(self, data: collections.abc.Iterable) -> None:
208
+ """
209
+ Initialize a `FlagChangeList`.
210
+
211
+ Parameters
212
+ ----------
213
+ data : Iterable
214
+ The data this `FlagChangeList` will hold.
215
+
216
+ Raises
217
+ ------
218
+ TypeError
219
+ Raised when data is not an :class:`Iterable`.
220
+ """
221
+ self._data = list(data)
222
+ self._changed = False
223
+
224
+ @property
225
+ def changed(self) -> bool:
226
+ """
227
+ Whether this `FlagChangeList` has been modified since creation.
228
+
229
+ Returns
230
+ -------
231
+ bool
232
+ """
233
+ return self._changed
234
+
235
+ def __repr__(self) -> str: # pragma: no cover
236
+ return self._data.__repr__()
237
+
238
+ def __getitem__(self, index: int | slice) -> typing.Any:
239
+ return self._data.__getitem__(index)
240
+
241
+ def __len__(self) -> int:
242
+ return self._data.__len__()
243
+
244
+ def __setitem__(self, index: int | slice, value) -> None:
245
+ self._data.__setitem__(index, value)
246
+ self._changed = True
247
+
248
+ def __delitem__(self, index: int | slice) -> None:
249
+ self._data.__delitem__(index)
250
+ self._changed = True
251
+
252
+ def insert(self, index: int, value: typing.Any):
253
+ """
254
+ Insert `value` at position given by `index`.
255
+
256
+ Parameters
257
+ ----------
258
+ index : int
259
+ The index of the new value in the `FlagChangeList`.
260
+ value : typing.Any
261
+ The value to insert into this `FlagChangeList`.
262
+ """
263
+ self._data.insert(index, value)
264
+ self._changed = True
265
+
266
+ def __add__(self, other: collections.abc.Iterable):
267
+ return FlagChangeList(data=self._data + list(other))
268
+
269
+ def __iadd__(self, other: collections.abc.Iterable):
270
+ for val in other:
271
+ self.append(val)
272
+ return self
273
+
274
+
275
+ class TypedFlagChangeList(FlagChangeList):
276
+ """
277
+ A :class:`FlagChangeList` with an ensured type for individual list items.
278
+
279
+ Note that single strings are not treated as Iterable, i.e. (as opposed to
280
+ a "normal" list) `TypedFlagChangeList(data="abc")` will result in
281
+ `data=["abc"]` (and not `data=["a", "b", "c"]`).
282
+ """
283
+
284
+ def __init__(self, data: collections.abc.Iterable, dtype: type) -> None:
285
+ """
286
+ Initialize a `TypedFlagChangeList`.
287
+
288
+ Parameters
289
+ ----------
290
+ data : Iterable
291
+ (Initial) data for this `TypedFlagChangeList`.
292
+ dtype : Callable datatype
293
+ The datatype for all entries in this `TypedFlagChangeList`. Will be
294
+ called on every value separately and is expected to convert to the
295
+ desired datatype.
296
+ """
297
+ self._dtype = dtype # set first to use in _convert_type method
298
+ data = self._ensure_iterable(data)
299
+ typed_data = [self._convert_type(v, index=i)
300
+ for i, v in enumerate(data)]
301
+ super().__init__(data=typed_data)
302
+
303
+ @property
304
+ def dtype(self) -> type:
305
+ """
306
+ All values in this `TypedFlagChangeList` are converted to dtype.
307
+
308
+ Returns
309
+ -------
310
+ type
311
+ """
312
+ return self._dtype
313
+
314
+ def _ensure_iterable(self, data) -> collections.abc.Iterable:
315
+ if getattr(data, '__iter__', None) is None:
316
+ # convenience for singular options,
317
+ # if it has no iter attribute we assume it is the only item
318
+ data = [data]
319
+ elif isinstance(data, str):
320
+ # strings have an iter but we still do not want to split them into
321
+ # single letters, so just put a list around
322
+ data = [data]
323
+ return data
324
+
325
+ def _convert_type(self, value,
326
+ index: int | slice | None = None) -> list:
327
+ # here we ignore index, but passing it should in principal make it
328
+ # possible to use different dtypes for different indices
329
+ if isinstance(index, int):
330
+ return self._dtype(value)
331
+ return [self._dtype(v) for v in value]
332
+
333
+ def __setitem__(self, index: int | slice, value) -> None:
334
+ typed_value = self._convert_type(value, index=index)
335
+ self._data.__setitem__(index, typed_value)
336
+ self._changed = True
337
+
338
+ def insert(self, index: int, value) -> None:
339
+ """
340
+ Insert `value` at position given by `index`.
341
+
342
+ Parameters
343
+ ----------
344
+ index : int
345
+ The index of the new value in the `TypedFlagChangeList`.
346
+ value : typing.Any
347
+ The value to insert into this `TypedFlagChangeList`.
348
+ """
349
+ typed_value = self._convert_type(value, index=index)
350
+ self._data.insert(index, typed_value)
351
+ self._changed = True
352
+
353
+ def __add__(self, other: collections.abc.Iterable):
354
+ # cast other to an iterable as we expect it (excluding the strings)
355
+ other = self._ensure_iterable(other)
356
+ ret = TypedFlagChangeList(data=self._data + list(other),
357
+ dtype=self._dtype)
358
+ return ret
359
+
360
+ def __iadd__(self, other: collections.abc.Iterable):
361
+ # cast other to an iterable as we expect it (excluding the strings)
362
+ other = self._ensure_iterable(other)
363
+ for val in other:
364
+ self.append(val)
365
+ return self
@@ -12,13 +12,31 @@
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 classes and functions for engine-agnostic but trajectory-related operations.
17
+
18
+ All user-facing classes and functions are reexported here for convenience.
19
+ This includes:
20
+
21
+ - the TrajectoryFunctionWrapper classes for CV value calculation and caching,
22
+ - the Conditional/InParts TrajectoryPropagator classes for propagation of MD in
23
+ parts and/or until a condition is reached (and related functions),
24
+ - and classes for extracting and concatenating trajectories (FrameExtractors
25
+ and TrajectoryConcatenator)
26
+
27
+ """
28
+ from .convert import (NoModificationFrameExtractor,
29
+ InvertedVelocitiesFrameExtractor,
30
+ RandomVelocitiesFrameExtractor,
31
+ TrajectoryConcatenator,
32
+ )
15
33
  from .functionwrapper import (PyTrajectoryFunctionWrapper,
16
34
  SlurmTrajectoryFunctionWrapper,
17
35
  )
18
36
  from .propagate import (ConditionalTrajectoryPropagator,
19
37
  TrajectoryPropagatorUntilAnyState,
20
38
  InPartsTrajectoryPropagator,
21
- construct_TP_from_plus_and_minus_traj_segments,
39
+ construct_tp_from_plus_and_minus_traj_segments,
22
40
  )
23
41
  from .trajectory import (_forget_trajectory,
24
42
  _forget_all_trajectories,