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/__init__.py CHANGED
@@ -12,6 +12,13 @@
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
+ The asyncmd toplevel module.
17
+
18
+ It imports the central Trajectory objects and the config(uration) functions for
19
+ user convenience.
20
+ It also makes public some information on the asyncmd version/git_hash.
21
+ """
15
22
  from ._version import __version__, __git_hash__
16
23
 
17
24
  from . import config
asyncmd/_config.py CHANGED
@@ -12,15 +12,22 @@
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
+ Configuration dictionaries to influence asyncmd runtime behavior.
15
17
 
18
+ NOTE: This file **only** contains the dictionaries with the values
19
+ and **no** functions to set them, the funcs all live in 'config.py'.
20
+ The idea here is that we can then without any issues import additional
21
+ stuff (like the config functions from 'slurm.py') in 'config.py'
22
+ without risking circular imports because all asyncmd files should only
23
+ need to import the _CONFIG and _SEMAPHORES dicts from '_config.py'.
24
+ """
25
+ import asyncio
26
+ import typing
16
27
 
17
- # NOTE: This file **only** contains the dictionaries with the values
18
- # and **no** functions to set them, the funcs all live in 'config.py'.
19
- # The idea here is that we can then without any issues import additional
20
- # stuff (like the config functions from 'slurm.py') in 'config.py'
21
- # without risking circular imports becasue all asyncmd files should only
22
- # need to import the _CONFIG and _SEMAPHORES dicts from '_config.py'.
23
28
 
24
-
25
- _GLOBALS = {}
26
- _SEMAPHORES = {}
29
+ _GLOBALS: dict[str, typing.Any] = {}
30
+ _SEMAPHORES: dict[str, asyncio.BoundedSemaphore] = {}
31
+ # These semaphores are optional (i.e. can be None, which means unlimited)
32
+ # e.g. slurm_max_jobs
33
+ _OPT_SEMAPHORES: dict[str, asyncio.BoundedSemaphore | None] = {}
asyncmd/_version.py CHANGED
@@ -12,24 +12,15 @@
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
+ Helpers to populate asyncmd.__version__.
17
+
18
+ If we are in a git-repository (and detect commits since the last version-tagged
19
+ commit) we will add a git-hash to the version.
20
+ """
15
21
  import os
16
22
  import subprocess
17
-
18
-
19
- def _get_version_from_pyproject():
20
- """Get version string from pyproject.toml file."""
21
- pyproject_toml = os.path.join(os.path.dirname(__file__),
22
- "../../pyproject.toml")
23
- with open(pyproject_toml) as f:
24
- line = f.readline()
25
- while line:
26
- if line.startswith("version ="):
27
- version_line = line
28
- break
29
- line = f.readline()
30
- version = version_line.strip().split(" = ")[1]
31
- version = version.replace('"', '').replace("'", "")
32
- return version
23
+ import importlib.metadata
33
24
 
34
25
 
35
26
  def _get_git_hash_and_tag():
@@ -37,39 +28,34 @@ def _get_git_hash_and_tag():
37
28
  git_hash = ""
38
29
  git_date = ""
39
30
  git_tag = ""
40
- p = subprocess.Popen(
31
+ with subprocess.Popen(
41
32
  ["git", "log", "-1", "--format='%H || %as || %(describe:tags=true,match=v*)'"],
42
33
  stdout=subprocess.PIPE,
43
34
  stderr=subprocess.PIPE,
44
35
  cwd=os.path.dirname(__file__),
45
- )
46
- stdout, stderr = p.communicate()
47
- if p.returncode == 0:
36
+ ) as p:
37
+ stdout, _ = p.communicate()
38
+ returncode = p.returncode
39
+ if not returncode:
48
40
  git_hash, git_date, git_describe = (stdout.decode("utf-8")
49
41
  .replace("'", "").replace('"', '')
50
42
  .strip().split("||"))
51
43
  git_date = git_date.strip().replace("-", "")
52
44
  git_describe = git_describe.strip()
53
- if "-" not in git_describe and git_describe != "":
45
+ if git_describe and "-" not in git_describe:
54
46
  # git-describe returns either the git-tag or (if we are not exactly
55
47
  # at a tag) something like
56
48
  # $GITTAG-$NUM_COMMITS_DISTANCE-$CURRENT_COMMIT_HASH
57
49
  git_tag = git_describe[1:] # strip of the 'v'
58
50
  return git_hash, git_date, git_tag
59
51
 
60
- try:
61
- _version = _get_version_from_pyproject()
62
- except FileNotFoundError:
63
- # pyproject.toml not found
64
- import importlib.metadata
65
- __version__ = importlib.metadata.version("asyncmd")
66
- __git_hash__ = ""
52
+
53
+ _version = importlib.metadata.version("asyncmd")
54
+ _git_hash, _git_date, _git_tag = _get_git_hash_and_tag()
55
+ __git_hash__ = _git_hash
56
+ if _version == _git_tag or not _git_hash:
57
+ # dont append git_hash to version, if it is a version-tagged commit or if
58
+ # git_hash is empty (happens if git is installed but we are not in a repo)
59
+ __version__ = _version
67
60
  else:
68
- _git_hash, _git_date, _git_tag = _get_git_hash_and_tag()
69
- __git_hash__ = _git_hash
70
- if _version == _git_tag or _git_hash == "":
71
- # dont append git_hash to version, if it is a version-tagged commit or if
72
- # git_hash is empty (happens if git is installed but we are not in a repo)
73
- __version__ = _version
74
- else:
75
- __version__ = _version + f"+git{_git_date}.{_git_hash[:7]}"
61
+ __version__ = _version + f"+git{_git_date}.{_git_hash[:7]}"
asyncmd/config.py CHANGED
@@ -12,23 +12,29 @@
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 functions configuring asyncmd behavior.
17
+
18
+ It also import the configuration functions for submodules (like slurm) to make
19
+ them accessible to users in one central place.
20
+ """
15
21
  import os
16
22
  import asyncio
17
23
  import logging
18
24
  import resource
19
- import typing
20
25
 
21
26
 
22
- from ._config import _GLOBALS, _SEMAPHORES
27
+ from ._config import _GLOBALS, _SEMAPHORES, _OPT_SEMAPHORES
28
+ from .trajectory.trajectory import _update_cache_type_for_all_trajectories
29
+ # pylint: disable-next=unused-import
23
30
  from .slurm import set_slurm_settings, set_all_slurm_settings
24
- # TODO: Do we want to set the _GLOBALS defaults here? E.g. CACHE_TYPE="npz"?
25
31
 
26
32
 
27
33
  logger = logging.getLogger(__name__)
28
34
 
29
35
 
30
36
  # can be called by the user to (re) set maximum number of processes used
31
- def set_max_process(num=None, max_num=None):
37
+ def set_max_process(num: int | None = None, max_num: int | None = None) -> None:
32
38
  """
33
39
  Set the maximum number of concurrent python processes.
34
40
 
@@ -45,16 +51,14 @@ def set_max_process(num=None, max_num=None):
45
51
  spawning hundreds of processes.
46
52
  """
47
53
  # NOTE: I think we should use a conservative default, e.g. 0.25*cpu_count()
48
- # TODO: limit to 30-40?, i.e never higher even if we have 1111 cores?
54
+ # pylint: disable-next=global-variable-not-assigned
49
55
  global _SEMAPHORES
50
56
  if num is None:
51
- logical_cpu_count = os.cpu_count()
52
- if logical_cpu_count is not None:
53
- num = int(logical_cpu_count / 4)
57
+ if (logical_cpu_count := os.cpu_count()) is not None:
58
+ num = max(1, int(logical_cpu_count / 4))
54
59
  else:
55
60
  # fallback if os.cpu_count() can not determine the number of cpus
56
61
  # play it save and not have more than 2?
57
- # TODO: think about a good number!
58
62
  num = 2
59
63
  if max_num is not None:
60
64
  num = min((num, max_num))
@@ -64,7 +68,7 @@ def set_max_process(num=None, max_num=None):
64
68
  set_max_process()
65
69
 
66
70
 
67
- def set_max_files_open(num: typing.Optional[int] = None, margin: int = 30):
71
+ def set_max_files_open(num: int | None = None, margin: int = 30) -> None:
68
72
  """
69
73
  Set the maximum number of concurrently opened files.
70
74
 
@@ -86,10 +90,11 @@ def set_max_files_open(num: typing.Optional[int] = None, margin: int = 30):
86
90
  """
87
91
  # ensure that we do not open too many files
88
92
  # resource.getrlimit returns a tuple (soft, hard); we take the soft-limit
89
- # and to be sure 30 less (the reason beeing that we can not use the
93
+ # and to be sure 30 less (the reason being that we can not use the
90
94
  # semaphores from non-async code, but sometimes use the sync subprocess.run
91
95
  # and subprocess.check_call [which also need files/pipes to work])
92
96
  # also maybe we need other open files like a storage :)
97
+ # pylint: disable-next=global-variable-not-assigned
93
98
  global _SEMAPHORES
94
99
  rlim_soft = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
95
100
  if num is None:
@@ -125,10 +130,9 @@ set_max_files_open()
125
130
  # SLURM semaphore stuff:
126
131
  # TODO: move this to slurm.py? and initialize only if slurm is available?
127
132
  # slurm max job semaphore, if the user sets it it will be used,
128
- # otherwise we can use an unlimited number of syncronous slurm-jobs
133
+ # otherwise we can use an unlimited number of synchronous slurm-jobs
129
134
  # (if the simulation requires that much)
130
- # TODO: document that somewhere, bc usually clusters have a job number limit?!
131
- def set_slurm_max_jobs(num: typing.Union[int, None]):
135
+ def set_slurm_max_jobs(num: int | None) -> None:
132
136
  """
133
137
  Set the maximum number of simultaneously submitted SLURM jobs.
134
138
 
@@ -138,66 +142,95 @@ def set_slurm_max_jobs(num: typing.Union[int, None]):
138
142
  The maximum number of simultaneous SLURM jobs for this invocation of
139
143
  python/asyncmd. `None` means do not limit the maximum number of jobs.
140
144
  """
141
- global _SEMAPHORES
145
+ # pylint: disable-next=global-variable-not-assigned
146
+ global _OPT_SEMAPHORES
142
147
  if num is None:
143
- _SEMAPHORES["SLURM_MAX_JOB"] = None
148
+ _OPT_SEMAPHORES["SLURM_MAX_JOB"] = None
144
149
  else:
145
- _SEMAPHORES["SLURM_MAX_JOB"] = asyncio.BoundedSemaphore(num)
150
+ _OPT_SEMAPHORES["SLURM_MAX_JOB"] = asyncio.BoundedSemaphore(num)
146
151
 
147
152
 
148
153
  set_slurm_max_jobs(num=None)
149
154
 
150
155
 
151
156
  # Trajectory function value config
152
- def set_default_trajectory_cache_type(cache_type: str):
157
+ def set_trajectory_cache_type(cache_type: str,
158
+ copy_content: bool = True,
159
+ clear_old_cache: bool = False
160
+ ) -> None:
153
161
  """
154
- Set the default cache type for TrajectoryFunctionValues.
162
+ Set the cache type for TrajectoryFunctionWrapper values.
155
163
 
156
- Note that this can be overwritten on a per trajectory basis by passing
157
- ``cache_type`` to ``Trajectory.__init__``.
164
+ By default the content of the current caches is copied to the new caches.
165
+ To clear the old/previously set caches (after copying their values), pass
166
+ ``clear_old_cache=True``.
158
167
 
159
168
  Parameters
160
169
  ----------
161
170
  cache_type : str
162
171
  One of "h5py", "npz", "memory".
172
+ copy_content : bool, optional
173
+ Whether to copy the current cache content to the new cache,
174
+ by default True
175
+ clear_old_cache : bool, optional
176
+ Whether to clear the old/previously set cache, by default False.
163
177
 
164
178
  Raises
165
179
  ------
166
180
  ValueError
167
181
  Raised if ``cache_type`` is not one of the allowed values.
168
182
  """
183
+ # pylint: disable-next=global-variable-not-assigned
169
184
  global _GLOBALS
170
185
  allowed_values = ["h5py", "npz", "memory"]
171
- cache_type = cache_type.lower()
172
- if cache_type not in allowed_values:
186
+ if (cache_type := cache_type.lower()) not in allowed_values:
173
187
  raise ValueError(f"Given cache type must be one of {allowed_values}."
174
188
  + f" Was: {cache_type}.")
175
- _GLOBALS["TRAJECTORY_FUNCTION_CACHE_TYPE"] = cache_type
189
+ if _GLOBALS.get("TRAJECTORY_FUNCTION_CACHE_TYPE", "not_set") != cache_type:
190
+ # only do something if the new cache type differs from what we have
191
+ _GLOBALS["TRAJECTORY_FUNCTION_CACHE_TYPE"] = cache_type
192
+ _update_cache_type_for_all_trajectories(copy_content=copy_content,
193
+ clear_old_cache=clear_old_cache,
194
+ )
195
+
196
+
197
+ set_trajectory_cache_type("npz")
176
198
 
177
199
 
178
- def register_h5py_cache(h5py_group, make_default: bool = False):
200
+ def register_h5py_cache(h5py_group) -> None:
179
201
  """
180
202
  Register a h5py file or group for CV value caching.
181
203
 
182
204
  Note that this also sets the default cache type to "h5py", i.e. it calls
183
- :func:`set_default_trajectory_cache_type` with ``cache_type="h5py"``.
205
+ :func:`set_trajectory_cache_type` with ``cache_type="h5py"``.
184
206
 
185
207
  Note that a ``h5py.File`` is just a slightly special ``h5py.Group``, so you
186
- can pass either. :mod:`asyncmd` will use euther the file or the group as
208
+ can pass either. :mod:`asyncmd` will use either the file or the group as
187
209
  the root of its own stored values.
188
210
  E.g. you will have ``h5py_group["asyncmd/TrajectoryFunctionValueCache"]``
189
211
  always pointing to the cached trajectory values and if ``h5py_group`` is
190
- the top-level group (i.e. the file) you also have ``(file["/asyncmd/TrajectoryFunctionValueCache"] == h5py_group["asyncmd/TrajectoryFunctionValueCache"])``.
212
+ the top-level group (i.e. the file) you also have
213
+ ``(file["/asyncmd/TrajectoryFunctionValueCache"] ==\
214
+ h5py_group["asyncmd/TrajectoryFunctionValueCache"])``.
191
215
 
192
216
  Parameters
193
217
  ----------
194
218
  h5py_group : h5py.Group or h5py.File
195
219
  The file or group to use for caching.
196
- make_default: bool,
197
- Whether we should also make "h5py" the default trajectory function
198
- cache type. By default False.
199
220
  """
221
+ # pylint: disable-next=global-variable-not-assigned
200
222
  global _GLOBALS
201
- if make_default:
202
- set_default_trajectory_cache_type(cache_type="h5py")
203
223
  _GLOBALS["H5PY_CACHE"] = h5py_group
224
+ set_trajectory_cache_type(cache_type="h5py")
225
+
226
+
227
+ def show_config() -> None:
228
+ """
229
+ Print/show current configuration.
230
+ """
231
+ print(f"Values controlling caching: {_GLOBALS}")
232
+ # pylint: disable-next=protected-access
233
+ sem_print = {key: sem._value
234
+ for key, sem in {**_SEMAPHORES, **_OPT_SEMAPHORES}.items()
235
+ if sem is not None}
236
+ print(f"Semaphores controlling resource usage: {sem_print}")
@@ -12,5 +12,8 @@
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 exports all user-facing classes and functions for running gromacs.
17
+ """
15
18
  from .mdconfig import MDP
16
19
  from .mdengine import GmxEngine, SlurmGmxEngine
@@ -12,6 +12,9 @@
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 the MDP class to parse, modify, and write gromacs mdp files.
17
+ """
15
18
  import shlex
16
19
  import logging
17
20
  from ..mdconfig import LineBasedMDConfig
@@ -27,18 +30,6 @@ class MDP(LineBasedMDConfig):
27
30
  option, list of values pairs. Includes automatic types for known options
28
31
  and keeps track if any options have been changed compared to the original
29
32
  file.
30
-
31
- Parameters
32
- ----------
33
- original_file : str
34
- absolute or relative path to original config file to parse
35
-
36
- Methods
37
- -------
38
- write(outfile)
39
- write the current (modified) configuration state to a given file
40
- parse()
41
- read the current original_file and update own state with it
42
33
  """
43
34
 
44
35
  _KEY_VALUE_SEPARATOR = " = "
@@ -298,11 +289,11 @@ class MDP(LineBasedMDConfig):
298
289
  # before or after the equal sign
299
290
  # 4. no splits at '=' and no splits at ';' -> weired line, probably
300
291
  # not a valid line(?)
301
- if splits_at_comment[0] == "":
292
+ if not splits_at_comment[0]:
302
293
  # option 2 (and 3 if the comment is before the equal sign)
303
294
  # comment sign is the first letter, so the whole line is
304
295
  # (most probably) a comment line
305
- logger.debug(f"mdp line parsed as comment: {line}")
296
+ logger.debug("mdp line parsed as comment: %s", line)
306
297
  return {}
307
298
  if ((len(splits_at_equal) == 2 and len(splits_at_comment) == 1) # option 1
308
299
  # or option 3 with equal sign before comment sign
@@ -318,10 +309,9 @@ class MDP(LineBasedMDConfig):
318
309
  parser.commenters = ";"
319
310
  # puncutation_chars=True adds "~-./*?=" to wordchars
320
311
  # such that we do not split floats and file paths and similar
321
- tokens = list(parser)
322
312
  # gromacs mdp can have 0-N tokens/values to the RHS of the '='
323
- if len(tokens) == 0:
324
- # line with empty options, e.g. 'define = '
313
+ if not (tokens := list(parser)):
314
+ # zero tokens -> line with empty options, e.g. 'define = '
325
315
  return {self._key_char_replace(key): []}
326
316
  # lines with content, we always return a list (and let our
327
317
  # type_dispatch sort out the singleton options and the typing)