asyncmd 0.3.2__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/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,