OpenMPCD
DataManager.py
1 from .Configuration import Configuration
2 from .Run import Run
3 
4 import copy
5 import glob
6 import os.path
7 import pylibconfig2
8 import yaml
9 
10 class DataManager:
11  """
12  Provides information on the data that have been generated by OpenMPCD.
13 
14  Throughout this class, the a "config part generator" is understood to be a
15  generator in the Python sense (c.f. the `yield` keyword), which yields a
16  list; each of the elements of this list is a dictionary, containing:
17  - `settings`:
18  A dictionary, with each key being a configuration setting name, and the
19  value being the corresponding value;
20  - `pathComponents`:
21  A list of all path components that the generator request be added to the
22  run directory name.
23  - `targetSweepCount`:
24  The minimum number of sweeps this configuration must be simulated for,
25  possibly across multiple runs. Set to `None` if no such minimum is
26  desired.
27  """
28 
29  @staticmethod
31  """
32  Returns the path at which the configuration file for this class is
33  expected.
34  """
35 
36  return os.path.expanduser("~/.OpenMPCD/config/DataManager.yaml")
37 
38 
39  def __init__(self, dataPaths = None):
40  """
41  The constructor.
42 
43  This will require the file returned by `getConfigurationPath`
44  to be readable, and contain in `dataPaths` a list of OpenMPCD run
45  directories. Each entry in the list will be processed through Python's
46  `glob.glob` function, and as such, special tokens such as '*' may be
47  used to match a larger number of directories. If any of the matching
48  directories is found to not be a OpenMPCD run directory, it is ignored.
49  Furthermore, each entry in `dataPaths` may contain an initial '~'
50  character, which will be expanded to the user's home directory.
51 
52  Alternatively, the list of data paths may be supplied as the `dataPaths`
53  variable, which takes over the configuration file.
54 
55  @param[in] dataPaths
56  A list of data paths, or `None` for the default (see function
57  description).
58  """
59 
60  if dataPaths is not None:
61  if not isinstance(dataPaths, list):
62  raise TypeError()
63 
64 
65  configPath = self.getConfigurationPath()
66 
67  config = yaml.safe_load(open(configPath, "r"))
68 
69  if dataPaths is None:
70  dataPathsPreGlob = config["dataPaths"]
71  else:
72  dataPathsPreGlob = dataPaths
73 
74  dataPathsPreGlob = [os.path.expanduser(x) for x in dataPathsPreGlob]
75  dataPaths = []
76  for path in dataPathsPreGlob:
77  dataPaths += glob.glob(path)
78 
79  self.rundirs = []
80  for path in dataPaths:
81  self.rundirs.append(path)
82 
83  self.runs = None
84 
85  self.pathTranslators = []
86  if "pathTranslators" in config:
87  for translator in config["pathTranslators"]:
88  self.pathTranslators.append(translator)
89 
91  for translator in self.pathTranslators:
92  self.pathTranslatorsServerToLocal.append(
93  lambda x: x.replace(translator["server"], translator["local"]))
94 
95  self.projects = config["projects"] if "projects" in config else []
96  self.project = None
97 
98  self.cluster = None
99  if "cluster" in config:
100  self.cluster = config["cluster"]
101 
102  originalNodeList = copy.deepcopy(self.cluster["nodes"])
103  self.cluster["nodes"] = []
104 
105  for nodeSpecification in originalNodeList:
106  nodeNames = self._parseSlrumNodeList(nodeSpecification["name"])
107  for name in nodeNames:
108  newNode = copy.deepcopy(nodeSpecification)
109  newNode["name"] = name
110  self.cluster["nodes"].append(newNode)
111 
112 
113  def getProjects(self):
114  """
115  Returns all configured projects.
116  """
117 
118  return self.projects
119 
120 
122  """
123  Returns a dictionary, with each key corresponding the number of GPUs
124  installed on the individual systems grouped in the corresponding
125  dictionary value. Each value is a dictionary, with the following
126  entries:
127  * "nodes": A list of nodes that fall into that category
128  * "SlurmNodeList": A string, compatible with the `Slurm` scheduler,
129  that collects all the nodes in entry `nodes`.
130  """
131 
132  ret = {}
133 
134  if self.cluster is None:
135  return ret
136 
137  for node in self.cluster["nodes"]:
138  GPUCount = node["GPUCount"]
139  if GPUCount not in ret:
140  ret[GPUCount] = {"nodes": []}
141  ret[GPUCount]["nodes"].append(node["name"])
142 
143  for key in ret:
144  ret[key]["SlurmNodeList"] = \
145  self._makeSlurmNodeList(ret[key]["nodes"])
146 
147  return ret
148 
149 
151  """
152  Returns a dictionary, with each key being a number of jobs executed in
153  parallel on one node, and the value being the list of job batches
154  having exactly this many jobs, which are submitted and pending
155  execution.
156  """
157 
158  ret = {}
159 
160  for run in self.getRuns():
161  if run.hasParentRun():
162  continue
163 
164  if run.getState() != Run.RunState.Submitted:
165  continue
166 
167  batchSize = run.getJobBatchSize()
168  if batchSize not in ret:
169  ret[batchSize] = []
170 
171  ret[batchSize].append(run)
172 
173  return ret
174 
175 
176  def selectProject(self, name):
177  """
178  Selects the project of the given `name` as the currently active one.
179  """
180 
181  for project in self.projects:
182  if project["name"] == name:
183  self.project = project
184 
185  jobDirectoryBasePathOnLocalMachine = \
186  project["jobDirectoryBasePathOnLocalMachine"]
187  jobDirectoryBasePathOnServer = \
188  project["jobDirectoryBasePathOnServer"]
189 
190  def pathTranslator(
191  path,
192  localroot = jobDirectoryBasePathOnLocalMachine,
193  serverroot = jobDirectoryBasePathOnServer):
194  if path.startswith(localroot):
195  path = path.replace(localroot, serverroot, 1)
196  return path
197 
198  execfileScope = {
199  "jobDirectoryBasePathOnLocalMachine":
200  jobDirectoryBasePathOnLocalMachine,
201  "jobDirectoryBasePathOnServer":
202  jobDirectoryBasePathOnServer,
203  "pathTranslator": pathTranslator,
204  "Configuration": Configuration}
205  self._execfile(
206  project["targetDataSpecificationFilePath"], execfileScope)
207  self.project["baseConfig"] = execfileScope['baseConfig']
208  self.project["configPartGenerators"] = \
209  execfileScope['generators']
210  return
211 
212  raise Exception("Unknown project: " + name)
213 
214 
215  def getProject(self):
216  """
217  Returns the currently selected project, or `None` if none has been
218  selected.
219  """
220 
221  return self.project
222 
223 
224  def getRuns(self):
225  """
226  Returns a list of `Run` instances, corresponding to the run directories
227  that have been found.
228  """
229 
230  if self.runs is None:
231  self.runs = [
232  Run(rundir, self.pathTranslatorsServerToLocal)
233  for rundir in self.rundirs
234  ]
235 
236  return self.runs
237 
238 
239  def getNumberOfSweepsByRun(self, run):
240  """
241  Returns the number of completed sweeps in the given `run`, which may be
242  an instance of `Run`, or be a string pointing to a run directory.
243  The returned value corresponds to `run.getNumberOfCompletedSweeps()`,
244  pretending that `run` is indeed an instance of `Run`.
245  """
246 
247  if not isinstance(run, Run):
248  run = Run(run)
249 
250  return run.getNumberOfCompletedSweeps()
251 
252 
253  def getNumberOfSweepsByRuns(self, rundirs):
254  """
255  Returns the sum of the number of completed sweeps in all of the given
256  `runs`, which may be instances of `Run`, or strings pointing to run
257  directories.
258  """
259 
260  ret = 0
261  for rundir in rundirs:
262  ret += self.getNumberOfSweepsByRun(rundir)
263 
264  return ret
265 
266 
267  def getRunsFilteredByConfig(self, filters):
268  """
269  Returns a list of runs that match the given criteria.
270 
271  The argument `filters` is expected to be a function, or a list of
272  functions, each taking an object that represents the configuration for a
273  particular run, returning `False` if it should be filtered out of the
274  result set, or `True` otherwise (filters applied later might still
275  remove that configuration from the result set).
276  """
277 
278  if not isinstance(filters, list):
279  filters = [filters]
280 
281  ret = []
282 
283  for run in self.getRuns():
284  config = run.getConfiguration()
285 
286  exclude = False
287  for filter_ in filters:
288  if not filter_(config):
289  exclude = True
290  break
291 
292  if not exclude:
293  ret.append(run)
294 
295  return ret
296 
297 
299  self, configuration, pathTranslators = []):
300  """
301  Returns the result of `getRuns` filtered by the condition that the run's
302  configuration must be equivalent (in the sense of
303  `Configuration.isEquivalent`) to the given `configuration`.
304 
305  @param[in] pathTranslators
306  This argument will be passed as the `pathTranslators`
307  argument to `Configuration.isEquivalent`.
308  """
309 
310  runs = self.getRuns()
311  ret = []
312 
313  for run in runs:
314  equivalent = \
315  run.getConfiguration().isEquivalent(
316  configuration, pathTranslators = pathTranslators)
317  if equivalent:
318  ret.append(run)
319 
320  return ret
321 
322 
323  def getTargetDataSpecifications(self):
324  """
325  Returns, for the currently selected project, a list of dictionaries;
326  the latter each contain a key `config`, which contains a `Configuration`
327  instance, and a key `targetSweepCount`, which contains the number of
328  sweeps that are desired to be in the data gathered with this
329  configuration, or `None` if none is specified. Furthermore, the key
330  `pathComponents` contains a list of all path components that the
331  generators request be added to the run directory name.
332  """
333 
334  if self.project is None:
335  raise Exception()
336 
337  ret = []
338 
339  import itertools
340 
341  for generators in self.project["configPartGenerators"]:
342  for element in itertools.product(*generators):
343  config = copy.deepcopy(self.project["baseConfig"])
344  pathComponents = []
345  targetSweepCount = None
346  for configPart in element:
347  for name, value in configPart["settings"].items():
348  if isinstance(value, ConfigPartSpecialAction):
349  if value.isDelete():
350  del config[name]
351  elif value.isCreateGroup():
352  config.createGroup(name)
353  else:
354  raise RuntimeError("Unknown action.")
355  else:
356  config[name] = value
357 
358  if configPart["pathComponents"] is not None:
359  pathComponents += configPart["pathComponents"]
360 
361  if configPart["targetSweepCount"] is not None:
362  tmp = configPart["targetSweepCount"]
363  if targetSweepCount is None or tmp > targetSweepCount:
364  targetSweepCount = tmp
365 
366  ret.append({
367  "config": config,
368  "targetSweepCount": targetSweepCount,
369  "pathComponents": pathComponents})
370 
371  return ret
372 
373 
375  self, specification, defaultTargetSweepCount = None):
376  """
377  For the given target data `specification`, returns:
378  - `"completed"`
379  if the specification has achieved its target sweep count,
380  - `"pending"`
381  if it is not yet completed, but has runs being executed or being
382  scheduled for execution, or
383  - `"incomplete"` in any other case.
384 
385  @param[in] defaultTargetSweepCount
386  This parameter is used as the sweep count for target data
387  specifications that do not have a target sweep count set.
388  If `defaultTargetSweepCount` parameter is `None`, all target
389  data specifications must specify a target sweep count.
390  """
391 
392  runs = \
394  specification["config"], self.pathTranslatorsServerToLocal)
395  sweepCount = self.getNumberOfSweepsByRuns(runs)
396  targetSweepCount = specification["targetSweepCount"]
397  if targetSweepCount is None:
398  if defaultTargetSweepCount is None:
399  raise Exception()
400  targetSweepCount = defaultTargetSweepCount
401 
402  if sweepCount >= targetSweepCount:
403  return "completed"
404 
405  isPending = False
406  pendingStates = [Run.RunState.Running, Run.RunState.Submitted]
407  for run in runs:
408  if run.getState() in pendingStates:
409  isPending = True
410  break
411 
412  if isPending:
413  return "pending"
414 
415  return "incomplete"
416 
417 
419  self, defaultTargetSweepCount = None):
420  """
421  Takes the values returned by `getTargetDataSpecifications`, and groups
422  them into three categories in the returned dictionary: `completed`
423  contains all the target data specifications that have achieved their
424  target sweep counts, `pending` contains all the target data
425  specifications that are not yet completed, but have runs being executed
426  or being scheduled for execution, and `incomplete` contains the rest.
427 
428  @param[in] defaultTargetSweepCount
429  This parameter is used as the sweep count for target data
430  specifications that do not have a target sweep count set.
431  If `defaultTargetSweepCount` parameter is `None`, all target
432  data specifications must specify a target sweep count.
433  """
434 
435  ret = {
436  "completed": [],
437  "pending": [],
438  "incomplete": []
439  }
440 
441  specifications = self.getTargetDataSpecifications()
442  for specification in specifications:
443  status = \
445  specification, defaultTargetSweepCount)
446 
447  ret[status].append(specification)
448 
449  return ret
450 
451 
452  def createRundirByTargetDataSpecification(self, specification):
453  """
454  Creates a new rundir, and configuration files therein, for the given
455  target data specification (c.f. `getTargetDataSpecifications`), and
456  returns the newly created path.
457  """
458 
459  if self.project is None:
460  raise Exception("You need to select a project.")
461 
462  basePath = self.project["jobDirectoryBasePathOnLocalMachine"] + "/jobs"
463 
464  if not os.path.isdir(basePath):
465  raise ValueError("Base path does not exist: " + basePath)
466 
467  config = specification["config"]
468 
469  pathComponents = None
470  if "rundirNamePrefix" in self.project:
471  pathComponents = self.project["rundirNamePrefix"]
472 
473  for pathComponent in specification["pathComponents"]:
474  if pathComponents:
475  pathComponents += "---"
476  pathComponents += pathComponent
477 
478  numberOfDigits = 4
479  for x in range(0, pow(10, numberOfDigits)):
480  path = \
481  basePath + "/" + \
482  pathComponents + "---" + \
483  ("{:0>" + str(numberOfDigits) + "}").format(x)
484 
485  if not os.path.exists(path):
486  os.mkdir(path)
487  with open(path + "/config.txt", "w") as f:
488  f.write(str(config))
489  return path
490 
491 
493  self, rundirs, executablePath, executableOptions,
494  slurmOptions = {}, srunOptions = {},
495  chunkSize = 1, excludedNodes = None,
496  pathTranslator = None):
497  """
498  For each of the given `rundirs`, creates a Slurm job script at
499  `input/job.slrm` (relative to the respective rundir) that can be used to
500  submit a job via `sbatch`, or alternatively, if the rundir is part of a
501  larger job controlled via a jobscript in another run directory, creates
502  the file `input/parent-job-path.txt`, which contains the absolute path
503  to the parent job.
504 
505  The job script will assume that the OpenMPCD executable will reside at
506  `executablePath`, which should most probably be an absolute path. The
507  `--rundir` option, with the respective rundir specification as its
508  value, will be added to the string of program arguments
509  `executableOptions`.
510 
511  `executableOptions` is a string that contains all options that are
512  passed to the executable upon invocation, as if specified in `bash`.
513 
514  `slurmOptions` is a dictionary, with each key specifying a Slurm
515  `sbatch` option (e.g. "-J" or "--job-name") and its value specifying
516  that option's value. There, the special string "$JOBS_PER_NODE" will be
517  replaced with the number of individual invocations of the given
518  executable in the current Slurm job. See `chunkSize` below.
519  Furthermore, for each value, the special string "$RUNDIR" will be
520  replaced with the absolute path to the run directory that will contain
521  the `sbatch` job script.
522 
523  `srunOptions` is a dictionary, with each key specifying a `srun` option
524  (e.g. --gres") and its value specifying that option's value, or `None`
525  if there is no value.
526  For each value, the special string "$RUNDIR" will be replaced with the
527  absolute path to the run directory.
528 
529  `chunkSize` can be used to specify that one Slurm job should contain
530  `chunkSize` many individual invocations of the executable given. If
531  the number of `rundirs` is not divisible `chunkSize`, an exception is
532  thrown.
533 
534  If `excludedNodes` is not `None`, it is a string describing,
535  in Slurm's syntax (e.g. "n25-02[1-2]"), which compude nodes
536  should be excluded from executing the jobs.
537 
538  If `pathTranslator` is not `None`, it is called on each absolute path
539  before writing its value to some file, or returning it from this
540  function.
541  This is useful if this program is run on one computer, but the resulting
542  files will be on another, where the root directory of the project (or
543  the user's home) is different.
544 
545  The function returns a list of server paths to the created jobfiles.
546  """
547 
548  if len(rundirs) % chunkSize != 0:
549  raise Exception()
550 
551  if excludedNodes is not None:
552  for x in ["-w", "--nodelist", "-x", "--exclude"]:
553  if x in slurmOptions:
554  raise ValueError(
555  "Cannot have Slurm option " + x + " if " + \
556  "`nodeSpecification` is given")
557 
558  for x in ["-D", "--chdir"]:
559  if x in srunOptions:
560  raise ValueError("Cannot have option " + x + " in srunOptions")
561 
562  if not pathTranslator:
563  pathTranslator = lambda x: x
564 
565  jobfiles = []
566 
567  chunks = \
568  [
569  rundirs[first:first + chunkSize]
570  for first in range(0, len(rundirs), chunkSize)
571  ]
572 
573 
574  for chunk in chunks:
575  script = "#!/bin/bash" + "\n"
576 
577  jobsInChunk = len(chunk)
578  jobfileRundirPath = os.path.abspath(chunk[0])
579  jobfilePath = jobfileRundirPath + "/input/job.slrm"
580  mySlurmOptions = slurmOptions.copy()
581 
582  if not os.path.isdir(jobfileRundirPath + "/input"):
583  os.mkdir(jobfileRundirPath + "/input")
584 
585  if excludedNodes is not None:
586  mySlurmOptions["--exclude"] = excludedNodes
587 
588  for key, value in mySlurmOptions.items():
589  script += "#SBATCH " + key
590  if key[1] == "-":
591  script += "="
592  else:
593  script += " "
594 
595  value = \
596  value.replace("$RUNDIR", pathTranslator(jobfileRundirPath))
597  value = value.replace("$JOBS_PER_NODE", str(jobsInChunk))
598  script += value
599  script += "\n"
600 
601  script += "\n"
602 
603  for rundir in chunk:
604  if not os.path.isdir(rundir + "/input"):
605  os.mkdir(rundir + "/input")
606 
607  if rundir != jobfileRundirPath:
608  parentJobPath = rundir + "/input/parent-job-path.txt"
609  with open(parentJobPath, "w") as f:
610  f.write(pathTranslator(jobfileRundirPath))
611 
612  absolutePath = pathTranslator(os.path.abspath(rundir))
613 
614  script += "srun"
615  script += " --chdir='" + absolutePath + "/input'"
616 
617  for key, value in srunOptions.items():
618  script += " " + key
619  if value is not None:
620  if key[1] == "-":
621  script += "="
622  else:
623  script += " "
624  script += value.replace("$RUNDIR", absolutePath)
625 
626  script += " '" + executablePath + "'"
627  script += " " + executableOptions
628  script += " --rundir '" + absolutePath + "'"
629  script += " & \n"
630 
631  script += "wait" + "\n"
632 
633  with open(jobfilePath, "w") as jobfile:
634  jobfile.write(script)
635 
636 
637  jobfiles.append(pathTranslator(jobfilePath))
638 
639  return jobfiles
640 
641 
642  def __deprecated__createRundirs(
643  self, baseConfig, basePath, generators, rundirNamePrefix = ""):
644  """
645  Creates new rundirs, and configuration files therein.
646 
647  The `generators` argument is supposed to be either a config part,
648  generator,or a list of such functions.
649  One rundir and configuration will be created per element of the
650  Cartesian product of all the config part generators. The configurations
651  will be based on the `baseConfig` template, and the rundirs will be
652  created as child directories of `basePath`.
653 
654  `rundirNamePrefix` is a prefix that is prepended to all rundir directory
655  names.
656 
657  The function returns a list of all created run directories.
658  """
659 
660  if not isinstance(generators, list):
661  generators = [generators]
662 
663  if not os.path.isdir(basePath):
664  raise ValueError("Base path does not exist: " + basePath)
665 
666  ret = []
667 
668  import itertools
669 
670  for instance in itertools.product(*generators):
671  config = copy.deepcopy(baseConfig)
672  pathComponents = rundirNamePrefix
673  for setting in instance:
674  name, value, pathComponent = setting
675 
676  config[name] = value
677 
678  if pathComponent is not None:
679  pathComponents += "---" + pathComponent
680 
681  numberOfDigits = 4
682  for x in range(0, pow(10, numberOfDigits)):
683  path = \
684  basePath + "/" + \
685  pathComponents + "---" + \
686  ("{:0>" + str(numberOfDigits) + "}").format(x)
687  if not os.path.exists(path):
688  os.mkdir(path)
689  ret.append(path)
690  with open(path + "/config.txt", "w") as f:
691  f.write(str(config))
692  break
693 
694  return ret
695 
696 
697 
698  def __deprecated__createSlurmJobScripts(
699  self, rundirs, executablePath, executableOptions,
700  slurmOptions = {}, srunOptions = {},
701  chunkSize = 1, largeChunkNodeSpecification = None,
702  pathTranslator = None):
703  """
704  For each of the given `rundirs`, creates a Slurm job script at
705  `input/job.slrm` (relative to the respective rundir) that can be used to
706  submit a job via `sbatch`, or alternatively, if the rundir is part of a
707  larger job controlled via a jobscript in another run directory, creates
708  the file `input/parent-job-path.txt`, which contains the absolute path
709  to the parent job.
710 
711  The job script will assume that the OpenMPCD executable will reside at
712  `executablePath`, which should most probably be an absolute path. The
713  `--rundir` option will be added to that list with the respective rundir
714  specification.
715 
716  `executableOptions` is a string that contains all options that are
717  passed to the executable upon invocation, as if specified in `bash`.
718 
719  `slurmOptions` is a dictionary, with each key specifying a Slurm
720  `sbatch` option (e.g. "-J" or "--job-name") and its value specifying
721  that option's value. There, the special string "$JOBS_PER_NODE" will be
722  replaced with the number of individual invocations of the given
723  executable in the current Slurm job. See `chunkSize` below.
724  Furthermore, for each value, the special string "$RUNDIR" will be
725  replaced with the absolute path to the run directory that will contain
726  the `sbatch` job script.
727 
728  `srunOptions` is a dictionary, with each key specifying a `srun` option
729  (e.g. --gres") and its value specifying that option's value, or `None`
730  if there is no value.
731  For each value, the special string "$RUNDIR" will be replaced with the
732  absolute path to the run directory.
733 
734  `chunkSize` can be used to specify that one Slurm job should contain
735  `chunkSize` many individual invocations of the executable given. If
736  the number of `rundirs` is not divisible `chunkSize`, as many chunks of
737  size `chunkSize` are produced, and the remainder of the given `rundirs`
738  are put into chunks of size `1`.
739  This is useful if one has compute nodes with multiple GPUs than can run
740  more than one job in parallel, and the scheduler is not configured to
741  allow multiple jobs to share a node.
742 
743  If `largeChunkNodeSpecification` is not `None`, it is a string
744  describing, in Slurm's syntax (e.g. "n25-02[1-2]"), which compude nodes
745  should be excluded from executing jobs that have been grouped into
746  chunks less than `chunkSize`. Also, these nodes are excluded if
747  `chunkSize == 1`.
748 
749  If `pathTranslator` is not `None`, it is called on each absolute path
750  before writing its value to some file, or returning it from this
751  function.
752  This is useful if this program is run on one computer, but the resulting
753  files will be on another, where the root directory of the project (or
754  the user's home) is different.
755 
756  The function returns a list of paths to the created jobfiles.
757  """
758 
759  if largeChunkNodeSpecification is not None and chunkSize != 1:
760  for x in ["-w", "--nodelist", "-x", "--exclude"]:
761  if x in slurmOptions:
762  raise ValueError(
763  "Cannot have Slurm option " + x + " if " + \
764  "`chunkSize != 1` and " + \
765  "`largeChunkNodeSpecification` is given")
766 
767  for x in ["-D", "--chdir"]:
768  if x in srunOptions:
769  raise ValueError("Cannot have option " + x + " in srunOptions")
770 
771  if not pathTranslator:
772  pathTranslator = lambda x: x
773 
774  jobfiles = []
775 
776  chunks = \
777  [
778  rundirs[first:first + chunkSize]
779  for first in range(0, len(rundirs), chunkSize)
780  ]
781 
782  if len(chunks[-1]) < chunkSize:
783  popped = chunks.pop()
784  for x in popped:
785  chunks.append([x])
786 
787  for chunk in chunks:
788  script = "#!/bin/bash" + "\n"
789 
790  jobsInChunk = len(chunk)
791  jobfileRundirPath = os.path.abspath(chunk[0])
792  jobfilePath = jobfileRundirPath + "/input/job.slrm"
793  mySlurmOptions = slurmOptions.copy()
794 
795  if not os.path.isdir(jobfileRundirPath + "/input"):
796  os.mkdir(jobfileRundirPath + "/input")
797 
798  if largeChunkNodeSpecification is not None:
799  if jobsInChunk < chunkSize or chunkSize == 1:
800  mySlurmOptions["--exclude"] = largeChunkNodeSpecification
801 
802  for key, value in mySlurmOptions.items():
803  script += "#SBATCH " + key
804  if key[1] == "-":
805  script += "="
806  else:
807  script += " "
808 
809  value = \
810  value.replace("$RUNDIR", pathTranslator(jobfileRundirPath))
811  value = value.replace("$JOBS_PER_NODE", str(jobsInChunk))
812  script += value
813  script += "\n"
814 
815  script += "\n"
816 
817  for rundir in chunk:
818  if not os.path.isdir(rundir + "/input"):
819  os.mkdir(rundir + "/input")
820 
821  if rundir != jobfileRundirPath:
822  parentJobPath = rundir + "/input/parent-job-path.txt"
823  with open(parentJobPath, "w") as f:
824  f.write(pathTranslator(jobfileRundirPath))
825 
826  absolutePath = pathTranslator(os.path.abspath(rundir))
827 
828  script += "srun"
829  script += " --chdir='" + absolutePath + "/input'"
830 
831  for key, value in srunOptions.items():
832  script += " " + key
833  if value is not None:
834  if key[1] == "-":
835  script += "="
836  else:
837  script += " "
838  script += value.replace("$RUNDIR", absolutePath)
839 
840  script += " '" + executablePath + "'"
841  script += " " + executableOptions
842  script += " --rundir '" + absolutePath + "'"
843  script += " & \n"
844 
845  script += "wait" + "\n"
846 
847  with open(jobfilePath, "w") as jobfile:
848  jobfile.write(script)
849 
850 
851  jobfiles.append(pathTranslator(jobfilePath))
852 
853  return jobfiles
854 
855 
856  def _makeSlurmNodeList(self, nodes):
857  """
858  Returns a `Slurm`-compatible node-specification string.
859  """
860 
861  ret = ""
862  for node in nodes:
863  if ret:
864  ret += ","
865  ret += node
866 
867  return ret
868 
869 
870  def _parseSlrumNodeList(self, nodeList):
871  """
872  Returns a list of node names, corresponding to the `Slurm` node list.
873  """
874 
875  ret = []
876 
877  if "," in nodeList:
878  parts = nodeList.split(",")
879  for part in parts:
880  ret += self._parseSlrumNodeList(part)
881  return ret
882 
883  import re
884 
885  regex = r"\[([0-9]+)-([0-9]+)\]"
886  parts = re.split(regex, nodeList)
887  if len(parts) > 1:
888  prefix = parts[0]
889  suffix = parts[3]
890  start = int(parts[1])
891  end = int(parts[2])
892  for number in range(start, end + 1):
893  ret += self._parseSlrumNodeList(prefix + str(number) + suffix)
894  return ret
895 
896  return [nodeList]
897 
898  def _execfile(self, path, myGlobals = None, myLocals = None):
899  """
900  Executes the file's contents in the current context.
901  """
902 
903  with open(path) as f:
904  exec(compile(f.read(), path, 'exec'), myGlobals, myLocals)
905 
906 
908  """
909  Class that represents a special action to be performed in config part
910  generators, like deleting settings or creating setting groups.
911  """
912 
913  def __init__(self, action):
914  """
915  The constructor.
916 
917  @throw TypeError
918  Throws if `action` is not a `str`.
919  @throw ValueError
920  Throws if `action` has an illegal value.
921 
922  @param[in] action
923  The action to perform on the associated setting. Can be
924  `"delete"` to delete the setting (and possibly any
925  sub-settings), or `"createGroup"` to create a settings
926  group of that name.
927  """
928 
929  if not isinstance(action, str):
930  raise TypeError()
931 
932  if action not in ["delete", "createGroup"]:
933  raise ValueError(action)
934 
935  self._action = action
936 
937 
938  def isDelete(self):
939  """
940  Returns whether the action is to delete the associated setting.
941  """
942 
943  return self._action == "delete"
944 
945 
946  def isCreateGroup(self):
947  """
948  Returns whether the action is to create the associated setting group.
949  """
950 
951  return self._action == "createGroup"
MPCDAnalysis.DataManager.DataManager.getSubmittedJobBatchesGroupedByJobBatchSize
def getSubmittedJobBatchesGroupedByJobBatchSize(self)
Definition: DataManager.py:162
MPCDAnalysis.DataManager.DataManager.getTargetDataSpecificationStatus
def getTargetDataSpecificationStatus(self, specification, defaultTargetSweepCount=None)
Definition: DataManager.py:404
MPCDAnalysis.DataManager.DataManager.selectProject
def selectProject(self, name)
Definition: DataManager.py:186
MPCDAnalysis.DataManager.DataManager.pathTranslatorsServerToLocal
pathTranslatorsServerToLocal
Definition: DataManager.py:93
MPCDAnalysis.DataManager.DataManager.cluster
cluster
Definition: DataManager.py:101
MPCDAnalysis.DataManager.DataManager.getTargetDataSpecificationsGroupedByStatus
def getTargetDataSpecificationsGroupedByStatus(self, defaultTargetSweepCount=None)
Definition: DataManager.py:448
MPCDAnalysis.DataManager.DataManager.getTargetDataSpecifications
def getTargetDataSpecifications(self)
Definition: DataManager.py:346
OpenMPCD::CUDA::DeviceCode::pow
OPENMPCD_CUDA_HOST_AND_DEVICE boost::enable_if< boost::is_integral< B >, double >::type pow(const B base, const double exponent)
The power function.
Definition: Utilities.hpp:50
MPCDAnalysis.DataManager.DataManager.getProject
def getProject(self)
Definition: DataManager.py:227
MPCDAnalysis.DataManager.DataManager.createSlurmJobScripts
def createSlurmJobScripts(self, rundirs, executablePath, executableOptions, slurmOptions={}, srunOptions={}, chunkSize=1, excludedNodes=None, pathTranslator=None)
Definition: DataManager.py:560
MPCDAnalysis.DataManager.DataManager.pathTranslators
pathTranslators
Definition: DataManager.py:88
MPCDAnalysis.DataManager.ConfigPartSpecialAction.isCreateGroup
def isCreateGroup(self)
Definition: DataManager.py:976
MPCDAnalysis.DataManager.DataManager.getNumberOfSweepsByRuns
def getNumberOfSweepsByRuns(self, rundirs)
Definition: DataManager.py:269
MPCDAnalysis.DataManager.DataManager.getRunsFilteredByConfig
def getRunsFilteredByConfig(self, filters)
Definition: DataManager.py:288
MPCDAnalysis.DataManager.ConfigPartSpecialAction._action
_action
Definition: DataManager.py:960
MPCDAnalysis.DataManager.DataManager.getRuns
def getRuns(self)
Definition: DataManager.py:237
MPCDAnalysis.DataManager.DataManager.getClusterNodesGroupedByGPUCount
def getClusterNodesGroupedByGPUCount(self)
Definition: DataManager.py:135
MPCDAnalysis.Run.Run
Definition: Run.py:10
MPCDAnalysis.DataManager.DataManager.getRunsWithMatchingConfiguration
def getRunsWithMatchingConfiguration(self, configuration, pathTranslators=[])
Definition: DataManager.py:320
MPCDAnalysis.DataManager.ConfigPartSpecialAction
Definition: DataManager.py:935
MPCDAnalysis.DataManager.DataManager.project
project
Definition: DataManager.py:99
MPCDAnalysis.DataManager.DataManager.__init__
def __init__(self, dataPaths=None)
Definition: DataManager.py:61
MPCDAnalysis.DataManager.ConfigPartSpecialAction.isDelete
def isDelete(self)
Definition: DataManager.py:967
MPCDAnalysis.DataManager.DataManager._makeSlurmNodeList
def _makeSlurmNodeList(self, nodes)
Definition: DataManager.py:880
MPCDAnalysis.DataManager.DataManager.projects
projects
Definition: DataManager.py:98
MPCDAnalysis.DataManager.DataManager.runs
runs
Definition: DataManager.py:86
MPCDAnalysis.DataManager.DataManager.rundirs
rundirs
Definition: DataManager.py:82
MPCDAnalysis.DataManager.DataManager._parseSlrumNodeList
def _parseSlrumNodeList(self, nodeList)
Definition: DataManager.py:895
MPCDAnalysis.DataManager.DataManager.getNumberOfSweepsByRun
def getNumberOfSweepsByRun(self, run)
Definition: DataManager.py:255
MPCDAnalysis.DataManager.DataManager.getConfigurationPath
def getConfigurationPath()
Definition: DataManager.py:36
MPCDAnalysis.DataManager.DataManager.createRundirByTargetDataSpecification
def createRundirByTargetDataSpecification(self, specification)
Definition: DataManager.py:474
MPCDAnalysis.DataManager.DataManager.getProjects
def getProjects(self)
Definition: DataManager.py:120
MPCDAnalysis.DataManager.DataManager._execfile
def _execfile(self, path, myGlobals=None, myLocals=None)
Definition: DataManager.py:924