asyncmd 0.3.2__py3-none-any.whl → 0.3.3__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.
@@ -302,7 +302,7 @@ class MDP(LineBasedMDConfig):
302
302
  # option 2 (and 3 if the comment is before the equal sign)
303
303
  # comment sign is the first letter, so the whole line is
304
304
  # (most probably) a comment line
305
- logger.debug(f"mdp line parsed as comment: {line}")
305
+ logger.debug("mdp line parsed as comment: %s", line)
306
306
  return {}
307
307
  if ((len(splits_at_equal) == 2 and len(splits_at_comment) == 1) # option 1
308
308
  # or option 3 with equal sign before comment sign
@@ -626,8 +626,8 @@ class GmxEngine(MDEngine):
626
626
  + f"{cpt_fname} does not exist."
627
627
  )
628
628
  starting_configuration = cpt_fname
629
- logger.warning("Starting value for 'simulation-part' > 1 (=%s)"
630
- " and existing checkpoint file found (%s). "
629
+ logger.warning("Starting value for 'simulation-part' > 1 (=%s) "
630
+ "and existing checkpoint file found (%s). "
631
631
  "Using the checkpoint file as "
632
632
  "`starting_configuration`.",
633
633
  sim_part, cpt_fname)
@@ -712,7 +712,7 @@ class GmxEngine(MDEngine):
712
712
  stdout, stderr = await grompp_proc.communicate()
713
713
  return_code = grompp_proc.returncode
714
714
  logger.debug("gmx grompp command returned return code %s.",
715
- return_code)
715
+ str(return_code) if return_code is not None else "not available")
716
716
  #logger.debug("grompp stdout:\n%s", stdout.decode())
717
717
  #logger.debug("grompp stderr:\n%s", stderr.decode())
718
718
  if return_code != 0:
@@ -754,9 +754,9 @@ class GmxEngine(MDEngine):
754
754
  last_partnum = int(last_trajname[len(deffnm) + 5:len(deffnm) + 9])
755
755
  if last_partnum != len(previous_trajs):
756
756
  logger.warning("Not all previous trajectory parts seem to be "
757
- + "present in the current workdir. Assuming the "
758
- + "highest part number corresponds to the "
759
- + "checkpoint file and continuing anyway."
757
+ "present in the current workdir. Assuming the "
758
+ "highest part number corresponds to the "
759
+ "checkpoint file and continuing anyway."
760
760
  )
761
761
  # load the 'old' mdp_in
762
762
  async with _SEMAPHORES["MAX_FILES_OPEN"]:
@@ -869,7 +869,7 @@ class GmxEngine(MDEngine):
869
869
  raise e from None # reraise the error for encompassing coroutines
870
870
  else:
871
871
  logger.debug("gmx mdrun command returned return code %s.",
872
- returncode)
872
+ str(returncode) if returncode is not None else "not available")
873
873
  #logger.debug("gmx mdrun stdout:\n%s", stdout.decode())
874
874
  #logger.debug("gmx mdrun stderr:\n%s", stderr.decode())
875
875
  if returncode == 0:
@@ -1025,6 +1025,7 @@ class SlurmGmxEngine(GmxEngine):
1025
1025
  # although the mdrun was successfull.
1026
1026
 
1027
1027
  def __init__(self, mdconfig, gro_file, top_file, sbatch_script, ndx_file=None,
1028
+ sbatch_options: dict | None = None,
1028
1029
  **kwargs):
1029
1030
  """
1030
1031
  Initialize a :class:`SlurmGmxEngine`.
@@ -1047,6 +1048,19 @@ class SlurmGmxEngine(GmxEngine):
1047
1048
 
1048
1049
  ndx_file: str or None
1049
1050
  Optional, absolute or relative path to a gromacs index file.
1051
+ sbatch_options : dict or None
1052
+ Dictionary of sbatch options, keys are long names for options,
1053
+ values are the correponding values. The keys/long names are given
1054
+ without the dashes, e.g. to specify "--mem=1024" the dictionary
1055
+ needs to be {"mem": "1024"}. To specify options without values use
1056
+ keys with empty strings as values, e.g. to specify "--contiguous"
1057
+ the dictionary needs to be {"contiguous": ""}.
1058
+ See the SLURM documentation for a full list of sbatch options
1059
+ (https://slurm.schedmd.com/sbatch.html).
1060
+ Note: This argument is passed as is to the `SlurmProcess` in which
1061
+ the computation is performed. Each call to the engines `run` method
1062
+ triggers the creation of a new `SlurmProcess` and will use the then
1063
+ current `sbatch_options`.
1050
1064
 
1051
1065
  Note that all attributes can be set at intialization by passing keyword
1052
1066
  arguments with their name, e.g. mdrun_extra_args="-ntomp 2" to instruct
@@ -1063,6 +1077,7 @@ class SlurmGmxEngine(GmxEngine):
1063
1077
  with open(sbatch_script, 'r') as f:
1064
1078
  sbatch_script = f.read()
1065
1079
  self.sbatch_script = sbatch_script
1080
+ self.sbatch_options = sbatch_options
1066
1081
 
1067
1082
  def _name_from_name_or_none(self, run_name: typing.Optional[str]) -> str:
1068
1083
  if run_name is not None:
@@ -1089,12 +1104,13 @@ class SlurmGmxEngine(GmxEngine):
1089
1104
  async with aiofiles.open(fname, 'w') as f:
1090
1105
  await f.write(script)
1091
1106
  self._proc = await slurm.create_slurmprocess_submit(
1092
- jobname=name,
1093
- sbatch_script=fname,
1094
- workdir=workdir,
1095
- time=walltime,
1096
- stdfiles_removal="success",
1097
- stdin=None,
1107
+ jobname=name,
1108
+ sbatch_script=fname,
1109
+ workdir=workdir,
1110
+ time=walltime,
1111
+ sbatch_options=self.sbatch_options,
1112
+ stdfiles_removal="success",
1113
+ stdin=None,
1098
1114
  )
1099
1115
 
1100
1116
  async def _acquire_resources_gmx_mdrun(self, **kwargs):
asyncmd/gromacs/utils.py CHANGED
@@ -172,12 +172,12 @@ def ensure_mdp_options(mdp: MDP, genvel: str = "no", continuation: str = "yes")
172
172
  # make sure we do not generate velocities with gromacs
173
173
  genvel_test = mdp["gen-vel"]
174
174
  except KeyError:
175
- logger.info(f"Setting 'gen-vel = {genvel}' in mdp.")
175
+ logger.info("Setting 'gen-vel = %s' in mdp.", genvel)
176
176
  mdp["gen-vel"] = genvel
177
177
  else:
178
178
  if genvel_test != genvel:
179
- logger.warning(f"Setting 'gen-vel = {genvel}' in mdp "
180
- + f"(was '{genvel_test}').")
179
+ logger.warning("Setting 'gen-vel = %s' in mdp "
180
+ "(was '%s').", genvel, genvel_test)
181
181
  mdp["gen-vel"] = genvel
182
182
  try:
183
183
  # TODO/FIXME: this could also be 'unconstrained-start'!
@@ -186,12 +186,12 @@ def ensure_mdp_options(mdp: MDP, genvel: str = "no", continuation: str = "yes")
186
186
  # so I think we can ignore that for now?!
187
187
  continuation_test = mdp["continuation"]
188
188
  except KeyError:
189
- logger.info(f"Setting 'continuation = {continuation}' in mdp.")
189
+ logger.info("Setting 'continuation = %s' in mdp.", continuation)
190
190
  mdp["continuation"] = continuation
191
191
  else:
192
192
  if continuation_test != continuation:
193
- logger.warning(f"Setting 'continuation = {continuation}' in mdp "
194
- + f"(was '{continuation_test}').")
193
+ logger.warning("Setting 'continuation = %s' in mdp "
194
+ "(was '%s').", continuation, continuation_test)
195
195
  mdp["continuation"] = continuation
196
196
 
197
197
  return mdp
asyncmd/mdconfig.py CHANGED
@@ -386,8 +386,8 @@ class LineBasedMDConfig(MDConfig):
386
386
  else:
387
387
  # warn that we will only keep the last occurenc of key
388
388
  logger.warning("Parsed duplicate configuration option "
389
- + f"({key}). Last values encountered "
390
- + "take precedence.")
389
+ "(%s). Last values encountered take "
390
+ "precedence.", key)
391
391
  parsed.update(line_parsed)
392
392
  # convert the known types
393
393
  self._config = {key: self._type_dispatch[key](value)
asyncmd/slurm.py CHANGED
@@ -130,7 +130,7 @@ class SlurmClusterMediator:
130
130
  # (here forever means until we reinitialize SlurmClusterMediator)
131
131
 
132
132
  def __init__(self, **kwargs) -> None:
133
- self._exclude_nodes = []
133
+ self._exclude_nodes: list[str] = []
134
134
  # make it possible to set any attribute via kwargs
135
135
  # check the type for attributes with default values
136
136
  dval = object()
@@ -151,17 +151,17 @@ class SlurmClusterMediator:
151
151
  # this either checks for our defaults or whatever we just set via kwargs
152
152
  self.sacct_executable = ensure_executable_available(self.sacct_executable)
153
153
  self.sinfo_executable = ensure_executable_available(self.sinfo_executable)
154
- self._node_job_fails = collections.Counter()
155
- self._node_job_successes = collections.Counter()
154
+ self._node_job_fails: dict[str,int] = collections.Counter()
155
+ self._node_job_successes: dict[str,int] = collections.Counter()
156
156
  self._all_nodes = self.list_all_nodes()
157
- self._jobids = [] # list of jobids of jobs we know about
158
- self._jobids_sacct = [] # list of jobids we monitor actively via sacct
157
+ self._jobids: list[str] = [] # list of jobids of jobs we know about
158
+ self._jobids_sacct: list[str] = [] # list of jobids we monitor actively via sacct
159
159
  # we will store the info about jobs in a dict keys are jobids
160
160
  # values are dicts with key queried option and value the (parsed)
161
161
  # return value
162
162
  # currently queried options are: state, exitcode and nodelist
163
- self._jobinfo = {}
164
- self._last_sacct_call = 0 # make sure we dont call sacct too often
163
+ self._jobinfo: dict[str,dict] = {}
164
+ self._last_sacct_call = 0. # make sure we dont call sacct too often
165
165
  # make sure we can only call sacct once at a time
166
166
  # (since there is only one ClusterMediator at a time we can create
167
167
  # the semaphore here in __init__)
@@ -383,17 +383,18 @@ class SlurmClusterMediator:
383
383
  self._jobinfo[jobid]["nodelist"] = nodelist
384
384
  self._jobinfo[jobid]["exitcode"] = exitcode
385
385
  self._jobinfo[jobid]["state"] = state
386
- logger.debug(f"Extracted from sacct output: jobid {jobid},"
387
- + f" state {state}, exitcode {exitcode} and "
388
- + f"nodelist {nodelist}.")
386
+ logger.debug("Extracted from sacct output: jobid %s, state %s, "
387
+ "exitcode %s and nodelist %s.",
388
+ jobid, state, exitcode, nodelist,
389
+ )
389
390
  parsed_ec = self._parse_exitcode_from_slurm_state(slurm_state=state)
390
391
  self._jobinfo[jobid]["parsed_exitcode"] = parsed_ec
391
392
  if parsed_ec is not None:
392
- logger.debug("Parsed slurm state %s for job %s"
393
- " as returncode %s. Removing job"
394
- "from sacct calls because its state will"
395
- " not change anymore.",
396
- state, jobid, parsed_ec,
393
+ logger.debug("Parsed slurm state %s for job %s as "
394
+ "returncode %s. Removing job from sacct calls "
395
+ "because its state will not change anymore.",
396
+ state, jobid, str(parsed_ec) if parsed_ec is not None
397
+ else "not available",
397
398
  )
398
399
  self._jobids_sacct.remove(jobid)
399
400
  self._node_fail_heuristic(jobid=jobid,
@@ -434,8 +435,8 @@ class SlurmClusterMediator:
434
435
  # make the string a list of single node hostnames
435
436
  hostnameprefix, nums = nodelist.split("[")
436
437
  nums = nums.rstrip("]")
437
- nums = nums.split(",")
438
- return [f"{hostnameprefix}{num}" for num in nums]
438
+ nums_list = nums.split(",")
439
+ return [f"{hostnameprefix}{num}" for num in nums_list]
439
440
 
440
441
  def _parse_exitcode_from_slurm_state(self,
441
442
  slurm_state: str,
@@ -443,8 +444,9 @@ class SlurmClusterMediator:
443
444
  for ecode, regexp in self._ecode_for_slurmstate_regexps.items():
444
445
  if regexp.search(slurm_state):
445
446
  # regexp matches the given slurm_state
446
- logger.debug("Parsed SLURM state %s as exitcode %d.",
447
- slurm_state, ecode,
447
+ logger.debug("Parsed SLURM state %s as exitcode %s.",
448
+ slurm_state, str(ecode) if ecode is not None
449
+ else "not available",
448
450
  )
449
451
  return ecode
450
452
  # we should never finish the loop, it means we miss a slurm job state
@@ -522,11 +524,11 @@ class SlurmClusterMediator:
522
524
  all_nodes = len(self._all_nodes)
523
525
  exclude_nodes = len(self._exclude_nodes)
524
526
  if exclude_nodes >= all_nodes / 4:
525
- logger.error("We already declared 1/4 of the cluster as broken."
526
- + "Houston, we might have a problem?")
527
+ logger.error("We already declared 1/4 of the cluster as broken. "
528
+ "Houston, we might have a problem?")
527
529
  if exclude_nodes >= all_nodes / 2:
528
- logger.error("In fact we declared 1/2 of the cluster as broken."
529
- + "Houston, we *do* have a problem!")
530
+ logger.error("In fact we declared 1/2 of the cluster as broken. "
531
+ "Houston, we *do* have a problem!")
530
532
  if exclude_nodes >= all_nodes * 0.75:
531
533
  raise RuntimeError("Houston? 3/4 of the cluster is broken?")
532
534
 
@@ -581,9 +583,9 @@ class SlurmProcess:
581
583
  _slurm_cluster_mediator = None
582
584
  # we raise a ValueError if sacct/sinfo are not available
583
585
  logger.warning("Could not initialize SLURM cluster handling. "
584
- "If you are sure SLURM (sinfo/sacct/etc) is available"
585
- " try calling `asyncmd.config.set_slurm_settings()`"
586
- " with the appropriate arguments.")
586
+ "If you are sure SLURM (sinfo/sacct/etc) is available "
587
+ "try calling `asyncmd.config.set_slurm_settings()` "
588
+ "with the appropriate arguments.")
587
589
  # we can not simply wait for the subprocess, since slurm exits directly
588
590
  # so we will sleep for this long between checks if slurm-job completed
589
591
  sleep_time = 15 # TODO: heuristic? dynamically adapt?
@@ -597,8 +599,9 @@ class SlurmProcess:
597
599
  scancel_executable = "scancel"
598
600
 
599
601
  def __init__(self, jobname: str, sbatch_script: str,
600
- workdir: typing.Optional[str] = None,
601
- time: typing.Optional[float] = None,
602
+ workdir: str | None = None,
603
+ time: float | None = None,
604
+ sbatch_options: dict | None = None,
602
605
  stdfiles_removal: str = "success",
603
606
  **kwargs) -> None:
604
607
  """
@@ -619,6 +622,15 @@ class SlurmProcess:
619
622
  time : float or None
620
623
  Timelimit for the job in hours. None will result in using the
621
624
  default as either specified in the sbatch script or the partition.
625
+ sbatch_options : dict or None
626
+ 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": ""}.
632
+ See the SLURM documentation for a full list of sbatch options
633
+ (https://slurm.schedmd.com/sbatch.html).
622
634
  stdfiles_removal : str
623
635
  Whether to remove the stdout, stderr (and possibly stdin) files.
624
636
  Possible values are:
@@ -664,13 +676,136 @@ class SlurmProcess:
664
676
  if workdir is None:
665
677
  workdir = os.getcwd()
666
678
  self.workdir = os.path.abspath(workdir)
667
- self.time = time
679
+ self._time = time
680
+ # Use the property to directly call _sanitize_sbatch_options when assigning
681
+ # Do this **after** setting self._time to ensure consistency
682
+ if sbatch_options is None:
683
+ sbatch_options = {}
684
+ self.sbatch_options = sbatch_options
668
685
  self.stdfiles_removal = stdfiles_removal
669
- self._jobid = None
670
- self._jobinfo = {} # dict with jobinfo cached from slurm cluster mediator
671
- self._stdout_data = None
672
- self._stderr_data = None
673
- self._stdin = None
686
+ self._jobid: None | str = None
687
+ # dict with jobinfo cached from slurm cluster mediator
688
+ self._jobinfo: dict[str,typing.Any] = {}
689
+ self._stdout_data: None | bytes = None
690
+ self._stderr_data: None | bytes = None
691
+ self._stdin: None | str = None
692
+
693
+ def _sanitize_sbatch_options(self, sbatch_options: dict) -> dict:
694
+ """
695
+ Return sane sbatch_options dictionary to be consistent (with self).
696
+
697
+ Parameters
698
+ ----------
699
+ sbatch_options : dict
700
+ Dictionary of sbatch options.
701
+
702
+ Returns
703
+ -------
704
+ dict
705
+ Dictionary with sanitized sbatch options.
706
+ """
707
+ # NOTE: this should be called every time we modify sbatch_options or self.time!
708
+ # This is the list of sbatch options we use ourself, they should not
709
+ # be in the dict to avoid unforseen effects. We treat 'time' special
710
+ # because we want to allow for it to be specified via sbtach_options if
711
+ # it is not set via the attribute self.time.
712
+ reserved_sbatch_options = ["job-name", "chdir", "output", "error",
713
+ "input", "exclude", "parsable"]
714
+ new_sbatch_options = sbatch_options.copy()
715
+ if "time" in sbatch_options:
716
+ if self._time is not None:
717
+ logger.warning("Removing sbatch option 'time' from 'sbatch_options'. "
718
+ "Using the 'time' argument instead.")
719
+ del new_sbatch_options["time"]
720
+ else:
721
+ logger.debug("Using 'time' from 'sbatch_options' because "
722
+ "self.time is None.")
723
+ for option in reserved_sbatch_options:
724
+ if option in sbatch_options:
725
+ logger.warning("Removing sbatch option '%s' from "
726
+ "'sbatch_options' because it is used internaly "
727
+ "by the `SlurmProcess`.", option)
728
+ del new_sbatch_options[option]
729
+
730
+ return new_sbatch_options
731
+
732
+ def _slurm_timelimit_from_time_in_hours(self, time: float) -> str:
733
+ """
734
+ Create timelimit in SLURM compatible format from time in hours.
735
+
736
+ Parameters
737
+ ----------
738
+ timelimit : float
739
+ Timelimit for job in hours
740
+
741
+ Returns
742
+ -------
743
+ str
744
+ Timelimit for job as SLURM compatible string.
745
+ """
746
+ timelimit = time * 60
747
+ timelimit_min = int(timelimit) # take only the full minutes
748
+ timelimit_sec = round(60 * (timelimit - timelimit_min))
749
+ timelimit_str = f"{timelimit_min}:{timelimit_sec}"
750
+ return timelimit_str
751
+
752
+ @property
753
+ def time(self) -> float | None:
754
+ """
755
+ Timelimit for SLURM job in hours.
756
+
757
+ Can be a float or None (meaning do not specify a timelimit).
758
+ """
759
+ return self._time
760
+
761
+ @time.setter
762
+ def time(self, val: float | None) -> None:
763
+ self._time = val
764
+ self._sbatch_options: dict = self._sanitize_sbatch_options(self._sbatch_options)
765
+
766
+ @property
767
+ def sbatch_options(self) -> dict:
768
+ """
769
+ A copy of the sbatch_options dictionary.
770
+
771
+ Note that modifying single key, value pairs has no effect, to modify
772
+ (single) sbatch_options either use the `set_sbatch_option` and
773
+ `del_sbatch_option` methods or (re)assign a dictionary to
774
+ `sbatch_options`.
775
+ """
776
+ return self._sbatch_options.copy()
777
+
778
+ @sbatch_options.setter
779
+ def sbatch_options(self, val: dict) -> None:
780
+ self._sbatch_options = self._sanitize_sbatch_options(val)
781
+
782
+ def set_sbatch_option(self, key: str, value: str) -> None:
783
+ """
784
+ Set sbatch option with given key to value.
785
+
786
+ I.e. add/modify single key, value pair in sbatch_options.
787
+
788
+ Parameters
789
+ ----------
790
+ key : str
791
+ The name of the sbatch option.
792
+ value : str
793
+ The value for the sbatch option.
794
+ """
795
+ self._sbatch_options[key] = value
796
+ self._sbatch_options = self._sanitize_sbatch_options(self._sbatch_options)
797
+
798
+ def del_sbatch_option(self, key: str) -> None:
799
+ """
800
+ Delete sbatch option with given key from sbatch_options.
801
+
802
+ Parameters
803
+ ----------
804
+ key : str
805
+ The name of the sbatch option to delete.
806
+ """
807
+ del self._sbatch_options[key]
808
+ self._sbatch_options = self._sanitize_sbatch_options(self._sbatch_options)
674
809
 
675
810
  @property
676
811
  def stdfiles_removal(self) -> str:
@@ -734,10 +869,7 @@ class SlurmProcess:
734
869
  sbatch_cmd += f" --output=./{self._stdout_name(use_slurm_symbols=True)}"
735
870
  sbatch_cmd += f" --error=./{self._stderr_name(use_slurm_symbols=True)}"
736
871
  if self.time is not None:
737
- timelimit = self.time * 60
738
- timelimit_min = int(timelimit) # take only the full minutes
739
- timelimit_sec = round(60 * (timelimit - timelimit_min))
740
- timelimit_str = f"{timelimit_min}:{timelimit_sec}"
872
+ timelimit_str = self._slurm_timelimit_from_time_in_hours(self.time)
741
873
  sbatch_cmd += f" --time={timelimit_str}"
742
874
  # keep a ref to the stdin value, we need it in communicate
743
875
  self._stdin = stdin
@@ -749,6 +881,11 @@ class SlurmProcess:
749
881
  exclude_nodes = self.slurm_cluster_mediator.exclude_nodes
750
882
  if len(exclude_nodes) > 0:
751
883
  sbatch_cmd += f" --exclude={','.join(exclude_nodes)}"
884
+ for key, val in self.sbatch_options.items():
885
+ if val == "":
886
+ sbatch_cmd += f" --{key}"
887
+ else:
888
+ sbatch_cmd += f" --{key}={val}"
752
889
  sbatch_cmd += f" --parsable {self.sbatch_script}"
753
890
  logger.debug("About to execute sbatch_cmd %s.", sbatch_cmd)
754
891
  # 3 file descriptors: stdin,stdout,stderr
@@ -908,7 +1045,7 @@ class SlurmProcess:
908
1045
  RuntimeError
909
1046
  If the job has never been submitted.
910
1047
  """
911
- if self._jobid is None:
1048
+ if self.slurm_jobid is None:
912
1049
  # make sure we can only wait after submitting, otherwise we would
913
1050
  # wait indefinitively if we call wait() before submit()
914
1051
  raise RuntimeError("Can only wait for submitted SLURM jobs with "
@@ -1012,8 +1149,10 @@ class SlurmProcess:
1012
1149
  + f" and output {e.output}."
1013
1150
  ) from e
1014
1151
  # if we got until here the job is successfuly canceled....
1015
- logger.debug(f"Canceled SLURM job with jobid {self.slurm_jobid}."
1016
- + f"scancel returned {scancel_out}.")
1152
+ logger.debug("Canceled SLURM job with jobid %s. "
1153
+ "scancel returned %s.",
1154
+ self.slurm_jobid, scancel_out,
1155
+ )
1017
1156
  # remove the job from the monitoring
1018
1157
  self.slurm_cluster_mediator.monitor_remove_job(jobid=self._jobid)
1019
1158
  if (self._stdfiles_removal == "yes"
@@ -1034,6 +1173,7 @@ async def create_slurmprocess_submit(jobname: str,
1034
1173
  sbatch_script: str,
1035
1174
  workdir: str,
1036
1175
  time: typing.Optional[float] = None,
1176
+ sbatch_options: dict | None = None,
1037
1177
  stdfiles_removal: str = "success",
1038
1178
  stdin: typing.Optional[str] = None,
1039
1179
  **kwargs,
@@ -1055,6 +1195,15 @@ async def create_slurmprocess_submit(jobname: str,
1055
1195
  time : float or None
1056
1196
  Timelimit for the job in hours. None will result in using the
1057
1197
  default as either specified in the sbatch script or the partition.
1198
+ sbatch_options : dict or None
1199
+ 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": ""}.
1205
+ See the SLURM documentation for a full list of sbatch options
1206
+ (https://slurm.schedmd.com/sbatch.html).
1058
1207
  stdfiles_removal : str
1059
1208
  Whether to remove the stdout, stderr (and possibly stdin) files.
1060
1209
  Possible values are:
@@ -1078,6 +1227,7 @@ async def create_slurmprocess_submit(jobname: str,
1078
1227
  """
1079
1228
  proc = SlurmProcess(jobname=jobname, sbatch_script=sbatch_script,
1080
1229
  workdir=workdir, time=time,
1230
+ sbatch_options=sbatch_options,
1081
1231
  stdfiles_removal=stdfiles_removal,
1082
1232
  **kwargs)
1083
1233
  await proc.submit(stdin=stdin)
@@ -305,6 +305,7 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
305
305
  """
306
306
 
307
307
  def __init__(self, executable, sbatch_script,
308
+ sbatch_options: dict | None = None,
308
309
  call_kwargs: typing.Optional[dict] = None,
309
310
  load_results_func=None, **kwargs):
310
311
  """
@@ -325,6 +326,19 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
325
326
 
326
327
  - {cmd_str} : Replaced by the command to call the executable on a given trajectory.
327
328
 
329
+ sbatch_options : dict or None
330
+ Dictionary of sbatch options, keys are long names for options,
331
+ values are the correponding values. The keys/long names are given
332
+ without the dashes, e.g. to specify "--mem=1024" the dictionary
333
+ needs to be {"mem": "1024"}. To specify options without values use
334
+ keys with empty strings as values, e.g. to specify "--contiguous"
335
+ the dictionary needs to be {"contiguous": ""}.
336
+ See the SLURM documentation for a full list of sbatch options
337
+ (https://slurm.schedmd.com/sbatch.html).
338
+ Note: This argument is passed as is to the `SlurmProcess` in which
339
+ the computation is performed. Each call of the TrajectoryFunction
340
+ triggers the creation of a new `SlurmProcess` and will use the then
341
+ current `sbatch_options`.
328
342
  call_kwargs : dict, optional
329
343
  Dictionary of additional arguments to pass to the executable, they
330
344
  will be added to the call as pair ' {key} {val}', note that in case
@@ -352,6 +366,7 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
352
366
  sbatch_script = f.read()
353
367
  # (possibly) use properties to calc the id directly
354
368
  self.sbatch_script = sbatch_script
369
+ self.sbatch_options = sbatch_options
355
370
  self.executable = executable
356
371
  if call_kwargs is None:
357
372
  call_kwargs = {}
@@ -529,13 +544,14 @@ class SlurmTrajectoryFunctionWrapper(TrajectoryFunctionWrapper):
529
544
  await _SEMAPHORES["SLURM_MAX_JOB"].acquire()
530
545
  try: # this try is just to make sure we always release the semaphore
531
546
  slurm_proc = await slurm.create_slurmprocess_submit(
532
- jobname=self.slurm_jobname,
533
- sbatch_script=sbatch_fname,
534
- workdir=slurm_workdir,
535
- stdfiles_removal="success",
536
- stdin=None,
537
- # sleep 5 s between checking
538
- sleep_time=5,
547
+ jobname=self.slurm_jobname,
548
+ sbatch_script=sbatch_fname,
549
+ workdir=slurm_workdir,
550
+ sbatch_options=self.sbatch_options,
551
+ stdfiles_removal="success",
552
+ stdin=None,
553
+ # sleep 5 s between checking
554
+ sleep_time=5,
539
555
  )
540
556
  # wait for the slurm job to finish
541
557
  # also cancel the job when this future is canceled
@@ -599,14 +599,14 @@ class ConditionalTrajectoryPropagator(_TrajectoryPropagator):
599
599
  # sort out if we use max_frames or max_steps
600
600
  if max_frames is not None and max_steps is not None:
601
601
  logger.warning("Both max_steps and max_frames given. Note that "
602
- + "max_steps will take precedence.")
602
+ "max_steps will take precedence.")
603
603
  if max_steps is not None:
604
604
  self.max_steps = max_steps
605
605
  elif max_frames is not None:
606
606
  self.max_steps = max_frames * nstout
607
607
  else:
608
608
  logger.info("Neither max_frames nor max_steps given. "
609
- + "Setting max_steps to infinity.")
609
+ "Setting max_steps to infinity.")
610
610
  # this is a float but can be compared to ints
611
611
  self.max_steps = np.inf
612
612
 
@@ -629,10 +629,10 @@ class ConditionalTrajectoryPropagator(_TrajectoryPropagator):
629
629
  # and warn if it is not a corotinefunction
630
630
  logger.warning(
631
631
  "It is recommended to use coroutinefunctions for all "
632
- + "conditions. This can easily be achieved by wrapping any"
633
- + " function in a TrajectoryFunctionWrapper. All "
634
- + "non-coroutine condition functions will be blocking when"
635
- + " applied! ([c is coroutine for c in conditions] = %s)",
632
+ "conditions. This can easily be achieved by wrapping any "
633
+ "function in a TrajectoryFunctionWrapper. All "
634
+ "non-coroutine condition functions will be blocking when "
635
+ "applied! ([c is coroutine for c in conditions] = %s)",
636
636
  self._condition_func_is_coroutine
637
637
  )
638
638
  self._conditions = conditions
@@ -744,9 +744,10 @@ class ConditionalTrajectoryPropagator(_TrajectoryPropagator):
744
744
  # gets the frame with the lowest idx where any condition is True
745
745
  min_idx = np.argmin(frame_nums)
746
746
  first_condition_fullfilled = conds_fullfilled[min_idx]
747
- logger.error(f"Starting configuration ({starting_configuration}) "
748
- + "is already fullfilling the condition with idx"
749
- + f" {first_condition_fullfilled}.")
747
+ logger.error("Starting configuration (%s) is already fullfilling "
748
+ "the condition with idx %s.",
749
+ starting_configuration, first_condition_fullfilled,
750
+ )
750
751
  # we just return the starting configuration/trajectory
751
752
  trajs = [starting_configuration]
752
753
  return trajs, first_condition_fullfilled
@@ -820,9 +820,9 @@ class Trajectory:
820
820
  # default cache is set to h5py (as above)
821
821
  logger.warning("Trying to unpickle %s with cache_type "
822
822
  "'h5py' not possible without a registered "
823
- "cache. Falling back to global default type."
824
- "See 'asyncmd.config.register_h5py_cache' and"
825
- " 'asyncmd.config.set_default_cache_type'.",
823
+ "cache. Falling back to global default type. "
824
+ "See 'asyncmd.config.register_h5py_cache' and "
825
+ "'asyncmd.config.set_default_cache_type'.",
826
826
  self
827
827
  )
828
828
  self.cache_type = None # this calls _setup_cache
@@ -949,9 +949,9 @@ class TrajectoryFunctionValueCacheNPZ(collections.abc.Mapping):
949
949
  # now if the old npz did not match we should remove it
950
950
  # then we will rewrite it with the first cached CV values
951
951
  if not existing_npz_matches:
952
- logger.debug("Found existing npz file (%s) but the"
953
- " trajectory hash does not match."
954
- " Recreating the npz cache from scratch.",
952
+ logger.debug("Found existing npz file (%s) but the "
953
+ "trajectory hash does not match. "
954
+ "Recreating the npz cache from scratch.",
955
955
  self.fname_npz
956
956
  )
957
957
  os.unlink(self.fname_npz)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: asyncmd
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: asyncmd is a library to write concurrent code to run and analyze molecular dynamics simulations using pythons async/await synthax.
5
5
  Author-email: Hendrik Jung <hendrik.jung@biophys.mpg.de>
6
6
  Maintainer-email: Hendrik Jung <hendrik.jung@biophys.mpg.de>
@@ -24,6 +24,7 @@ License-File: LICENSE
24
24
  Requires-Dist: aiofiles
25
25
  Requires-Dist: mdanalysis
26
26
  Requires-Dist: numpy
27
+ Requires-Dist: scipy
27
28
  Provides-Extra: docs
28
29
  Requires-Dist: sphinx; extra == "docs"
29
30
  Provides-Extra: tests
@@ -36,6 +37,7 @@ Requires-Dist: coverage; extra == "tests-all"
36
37
  Requires-Dist: pytest-cov; extra == "tests-all"
37
38
  Provides-Extra: dev
38
39
  Requires-Dist: asyncmd[docs,tests-all]; extra == "dev"
40
+ Dynamic: license-file
39
41
 
40
42
  # asyncmd
41
43
 
@@ -50,7 +52,7 @@ This library addresses the tedious, error-prone and boring part of setting up ma
50
52
 
51
53
  ## Code Example
52
54
 
53
- Run `N` gromacs engines concurently from configurations randomly picked up along a trajectory (`traj.trr`) for `n_steps` integration steps each, drawing random Maxwell-Boltzmann velocities for each configuration on the way. Finally turn the python function `func` (which acts on `Trajectory` objects) into an asyncronous and cached function by wrapping it and apply it on all generated trajectories concurrently:
55
+ Run `N` [GROMACS] engines concurently from configurations randomly picked up along a trajectory (`traj.trr`) for `n_steps` integration steps each, drawing random Maxwell-Boltzmann velocities for each configuration on the way. Finally turn the python function `func` (which acts on `Trajectory` objects) into an asyncronous and cached function by wrapping it and apply it on all generated trajectories concurrently:
54
56
 
55
57
  ```python
56
58
  import asyncio
@@ -103,6 +105,14 @@ For an in-depth introduction see also the `examples` folder in this repository w
103
105
 
104
106
  ## Installation
105
107
 
108
+ ### pip install from PyPi
109
+
110
+ asyncmd is published on [PyPi] (since v0.3.2), installing is as easy as:
111
+
112
+ ```bash
113
+ pip install asyncmd
114
+ ```
115
+
106
116
  ### pip install directly from the repository
107
117
 
108
118
  Please note that you need to have [git-lfs] (an open source git extension) setup to get all input files needed to run the notebooks in the `examples` folder. However, no [git-lfs] is needed to get a working version of the library.
@@ -113,7 +123,7 @@ cd asyncmd
113
123
  pip install .
114
124
  ```
115
125
 
116
- ## API Reference
126
+ ## Documentation and API Reference
117
127
 
118
128
  The documentation can be build with [sphinx], use e.g. the following to build it in html format:
119
129
 
@@ -132,6 +142,9 @@ Tests use [pytest]. To run them just install asycmd with the test requirements
132
142
  git clone https://github.com/bio-phys/asyncmd.git
133
143
  cd asyncmd
134
144
  pip install .\[tests\]
145
+ # or use
146
+ pip install .\[tests-all\]
147
+ # to also install optional dependencies needed to run all tests
135
148
  ```
136
149
 
137
150
  And then run the tests (against the installed version) as
@@ -173,6 +186,8 @@ asyncmd is under the terms of the GNU general public license version 3 or later,
173
186
 
174
187
  [coverage]: https://pypi.org/project/coverage/
175
188
  [git-lfs]: https://git-lfs.com/
189
+ [GROMACS]: https://www.gromacs.org/
190
+ [PyPi]: https://pypi.org/project/asyncmd/
176
191
  [pytest]: https://docs.pytest.org/en/latest/
177
192
  [pytest-cov]: https://pypi.org/project/pytest-cov/
178
193
  [SLURM]: https://slurm.schedmd.com/documentation.html
@@ -0,0 +1,23 @@
1
+ asyncmd/__init__.py,sha256=0xkOmcT5JP7emeGl9ixPSPsdHylHtiWqvZGnzDC2GqM,768
2
+ asyncmd/_config.py,sha256=PpPYgsZyPmC5uybMc0pdAMj_6jUcm8AfME1hAvYy3DE,1140
3
+ asyncmd/_version.py,sha256=14Lpt2lrPMrGonuCi12MdigFAFaEhXYlip7X18BJQwc,2981
4
+ asyncmd/config.py,sha256=Jf_XTvXPkWdefuB9xVfCUzW4Fe3zGaUB-ZifC-oZ-yQ,7701
5
+ asyncmd/mdconfig.py,sha256=rV1eaYakFnxP8aG4_86M1uv3vtFqvLwpd7YbN3bSSQc,17482
6
+ asyncmd/mdengine.py,sha256=PPWphMBSkIoinbUpLHxbEn1yI02VHrCHGKzJlSOhYq4,4127
7
+ asyncmd/slurm.py,sha256=fHXb3B4MR8c6J97T-X_t6p-4BnWG4kANfGu8HqhmPCA,60851
8
+ asyncmd/tools.py,sha256=JS4Cn2Elm6miJmPf88ZDTaUIIFqb9bhHLlZKmJOOpC0,2554
9
+ asyncmd/utils.py,sha256=cVWgZlAa3I4eE3EacIfz82HIBznldqoxyUZ7M36hMXs,5331
10
+ asyncmd/gromacs/__init__.py,sha256=Rp_FQq_ftynX1561oD1X2Rby3Y4EBtXkgNgqa4jQk2o,726
11
+ asyncmd/gromacs/mdconfig.py,sha256=6odhq8-xw6DRwQmiZIWCt5TwheyiC_7v5iUinG2T7J8,18225
12
+ asyncmd/gromacs/mdengine.py,sha256=rV5-ugbaWtmDXiNHEq8dWlpBybMwE8zWESeikal2lHU,54276
13
+ asyncmd/gromacs/utils.py,sha256=nmpP1ZS9sYPnrJGTVam6uDBb3CjKn-s0Nn3666ynnCo,6718
14
+ asyncmd/trajectory/__init__.py,sha256=m2mP6YxMpXTpU_NQ-wID4UVEgZd7e0mkSaCCLLeo75Q,1195
15
+ asyncmd/trajectory/convert.py,sha256=3DYpt7w_AIryShT59dTPM1s9s7ND3AMqgF5z8pfUViM,26036
16
+ asyncmd/trajectory/functionwrapper.py,sha256=_atGGe2Dwu_EkyC6eNkONbc0KCDLegw0GqHfoc5MGLs,25788
17
+ asyncmd/trajectory/propagate.py,sha256=LvO3vj9JHv5EiZkuNB9PTLpHuS6qy3nzZTxge5byW20,45344
18
+ asyncmd/trajectory/trajectory.py,sha256=TouClq90m__q2qkBNNCZP2p59zWeqDa5TykdUpk7WPE,50700
19
+ asyncmd-0.3.3.dist-info/licenses/LICENSE,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
20
+ asyncmd-0.3.3.dist-info/METADATA,sha256=tQ0wPLn1GTRMOysb9cUQznSHeWCMQywbGB2aKerJmAo,8389
21
+ asyncmd-0.3.3.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
22
+ asyncmd-0.3.3.dist-info/top_level.txt,sha256=YG6cpLOyBjjelv7a8p2xYEHNVBgXSW8PvM8-9S9hMb8,8
23
+ asyncmd-0.3.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,23 +0,0 @@
1
- asyncmd/__init__.py,sha256=0xkOmcT5JP7emeGl9ixPSPsdHylHtiWqvZGnzDC2GqM,768
2
- asyncmd/_config.py,sha256=PpPYgsZyPmC5uybMc0pdAMj_6jUcm8AfME1hAvYy3DE,1140
3
- asyncmd/_version.py,sha256=14Lpt2lrPMrGonuCi12MdigFAFaEhXYlip7X18BJQwc,2981
4
- asyncmd/config.py,sha256=Jf_XTvXPkWdefuB9xVfCUzW4Fe3zGaUB-ZifC-oZ-yQ,7701
5
- asyncmd/mdconfig.py,sha256=07TL4EJs-d_34PBSINUas3creopJX8VHfcgmuW-Wd-E,17485
6
- asyncmd/mdengine.py,sha256=PPWphMBSkIoinbUpLHxbEn1yI02VHrCHGKzJlSOhYq4,4127
7
- asyncmd/slurm.py,sha256=IgnWv6Jhu-PSdHTy0GB3AIlwkFcHZ12bBZy2VggSmKs,54642
8
- asyncmd/tools.py,sha256=JS4Cn2Elm6miJmPf88ZDTaUIIFqb9bhHLlZKmJOOpC0,2554
9
- asyncmd/utils.py,sha256=cVWgZlAa3I4eE3EacIfz82HIBznldqoxyUZ7M36hMXs,5331
10
- asyncmd/gromacs/__init__.py,sha256=Rp_FQq_ftynX1561oD1X2Rby3Y4EBtXkgNgqa4jQk2o,726
11
- asyncmd/gromacs/mdconfig.py,sha256=rxhp5W3Cc0KuLN_P43ItlXI_xnAiJeh3DLeTjHp57kc,18224
12
- asyncmd/gromacs/mdengine.py,sha256=JZA3c0i2I1SK2iivNavqg_i3QFdtOOuUHbcKNlKJFho,53164
13
- asyncmd/gromacs/utils.py,sha256=TCQ3OAtUNN7cVGjJGkEdmhZF2QvnnmazvklbQrW0AB8,6716
14
- asyncmd/trajectory/__init__.py,sha256=m2mP6YxMpXTpU_NQ-wID4UVEgZd7e0mkSaCCLLeo75Q,1195
15
- asyncmd/trajectory/convert.py,sha256=3DYpt7w_AIryShT59dTPM1s9s7ND3AMqgF5z8pfUViM,26036
16
- asyncmd/trajectory/functionwrapper.py,sha256=6Oeoqi8IiffNO6egh1aRX6QSy4RyIPEYi3j64pbg-4I,24785
17
- asyncmd/trajectory/propagate.py,sha256=TSpMw4dYodRowltdPWf5styZtUBkQJ5HlSKaV38yUuw,45335
18
- asyncmd/trajectory/trajectory.py,sha256=HSPwEIdRZuxteRmxEi8lC4ZjCMxbPVy7QjpGBkSAKBI,50699
19
- asyncmd-0.3.2.dist-info/LICENSE,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
20
- asyncmd-0.3.2.dist-info/METADATA,sha256=4SlYWFxOj9lsGDF12HtYwgt8-kr2fJ9aKdwnDwX79zQ,8014
21
- asyncmd-0.3.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
22
- asyncmd-0.3.2.dist-info/top_level.txt,sha256=YG6cpLOyBjjelv7a8p2xYEHNVBgXSW8PvM8-9S9hMb8,8
23
- asyncmd-0.3.2.dist-info/RECORD,,