1 from .Configuration
import Configuration
12 Provides information on the data that have been generated by OpenMPCD.
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:
18 A dictionary, with each key being a configuration setting name, and the
19 value being the corresponding value;
21 A list of all path components that the generator request be added to the
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
32 Returns the path at which the configuration file for this class is
36 return os.path.expanduser(
"~/.OpenMPCD/config/DataManager.yaml")
39 def __init__(self, dataPaths = None):
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.
52 Alternatively, the list of data paths may be supplied as the `dataPaths`
53 variable, which takes over the configuration file.
56 A list of data paths, or `None` for the default (see function
60 if dataPaths
is not None:
61 if not isinstance(dataPaths, list):
67 config = yaml.safe_load(open(configPath,
"r"))
70 dataPathsPreGlob = config[
"dataPaths"]
72 dataPathsPreGlob = dataPaths
74 dataPathsPreGlob = [os.path.expanduser(x)
for x
in dataPathsPreGlob]
76 for path
in dataPathsPreGlob:
77 dataPaths += glob.glob(path)
80 for path
in dataPaths:
86 if "pathTranslators" in config:
87 for translator
in config[
"pathTranslators"]:
93 lambda x: x.replace(translator[
"server"], translator[
"local"]))
95 self.
projects = config[
"projects"]
if "projects" in config
else []
99 if "cluster" in config:
100 self.
cluster = config[
"cluster"]
102 originalNodeList = copy.deepcopy(self.
cluster[
"nodes"])
105 for nodeSpecification
in originalNodeList:
107 for name
in nodeNames:
108 newNode = copy.deepcopy(nodeSpecification)
109 newNode[
"name"] = name
110 self.
cluster[
"nodes"].append(newNode)
115 Returns all configured projects.
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
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`.
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"])
144 ret[key][
"SlurmNodeList"] = \
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
161 if run.hasParentRun():
164 if run.getState() != Run.RunState.Submitted:
167 batchSize = run.getJobBatchSize()
168 if batchSize
not in ret:
171 ret[batchSize].append(run)
178 Selects the project of the given `name` as the currently active one.
182 if project[
"name"] == name:
185 jobDirectoryBasePathOnLocalMachine = \
186 project[
"jobDirectoryBasePathOnLocalMachine"]
187 jobDirectoryBasePathOnServer = \
188 project[
"jobDirectoryBasePathOnServer"]
192 localroot = jobDirectoryBasePathOnLocalMachine,
193 serverroot = jobDirectoryBasePathOnServer):
194 if path.startswith(localroot):
195 path = path.replace(localroot, serverroot, 1)
199 "jobDirectoryBasePathOnLocalMachine":
200 jobDirectoryBasePathOnLocalMachine,
201 "jobDirectoryBasePathOnServer":
202 jobDirectoryBasePathOnServer,
203 "pathTranslator": pathTranslator,
204 "Configuration": Configuration}
206 project[
"targetDataSpecificationFilePath"], execfileScope)
207 self.
project[
"baseConfig"] = execfileScope[
'baseConfig']
208 self.
project[
"configPartGenerators"] = \
209 execfileScope[
'generators']
212 raise Exception(
"Unknown project: " + name)
217 Returns the currently selected project, or `None` if none has been
226 Returns a list of `Run` instances, corresponding to the run directories
227 that have been found.
230 if self.
runs is None:
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`.
247 if not isinstance(run, Run):
250 return run.getNumberOfCompletedSweeps()
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
261 for rundir
in rundirs:
269 Returns a list of runs that match the given criteria.
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).
278 if not isinstance(filters, list):
284 config = run.getConfiguration()
287 for filter_
in filters:
288 if not filter_(config):
299 self, configuration, pathTranslators = []):
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`.
305 @param[in] pathTranslators
306 This argument will be passed as the `pathTranslators`
307 argument to `Configuration.isEquivalent`.
315 run.getConfiguration().isEquivalent(
316 configuration, pathTranslators = pathTranslators)
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.
341 for generators
in self.
project[
"configPartGenerators"]:
342 for element
in itertools.product(*generators):
343 config = copy.deepcopy(self.
project[
"baseConfig"])
345 targetSweepCount =
None
346 for configPart
in element:
347 for name, value
in configPart[
"settings"].items():
348 if isinstance(value, ConfigPartSpecialAction):
351 elif value.isCreateGroup():
352 config.createGroup(name)
354 raise RuntimeError(
"Unknown action.")
358 if configPart[
"pathComponents"]
is not None:
359 pathComponents += configPart[
"pathComponents"]
361 if configPart[
"targetSweepCount"]
is not None:
362 tmp = configPart[
"targetSweepCount"]
363 if targetSweepCount
is None or tmp > targetSweepCount:
364 targetSweepCount = tmp
368 "targetSweepCount": targetSweepCount,
369 "pathComponents": pathComponents})
375 self, specification, defaultTargetSweepCount = None):
377 For the given target data `specification`, returns:
379 if the specification has achieved its target sweep count,
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.
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.
396 targetSweepCount = specification[
"targetSweepCount"]
397 if targetSweepCount
is None:
398 if defaultTargetSweepCount
is None:
400 targetSweepCount = defaultTargetSweepCount
402 if sweepCount >= targetSweepCount:
406 pendingStates = [Run.RunState.Running, Run.RunState.Submitted]
408 if run.getState()
in pendingStates:
419 self, defaultTargetSweepCount = None):
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.
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.
442 for specification
in specifications:
445 specification, defaultTargetSweepCount)
447 ret[status].append(specification)
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.
460 raise Exception(
"You need to select a project.")
462 basePath = self.
project[
"jobDirectoryBasePathOnLocalMachine"] +
"/jobs"
464 if not os.path.isdir(basePath):
465 raise ValueError(
"Base path does not exist: " + basePath)
467 config = specification[
"config"]
469 pathComponents =
None
470 if "rundirNamePrefix" in self.
project:
471 pathComponents = self.
project[
"rundirNamePrefix"]
473 for pathComponent
in specification[
"pathComponents"]:
475 pathComponents +=
"---"
476 pathComponents += pathComponent
479 for x
in range(0,
pow(10, numberOfDigits)):
482 pathComponents +
"---" + \
483 (
"{:0>" + str(numberOfDigits) +
"}").format(x)
485 if not os.path.exists(path):
487 with open(path +
"/config.txt",
"w")
as f:
493 self, rundirs, executablePath, executableOptions,
494 slurmOptions = {}, srunOptions = {},
495 chunkSize = 1, excludedNodes = None,
496 pathTranslator = None):
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
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
511 `executableOptions` is a string that contains all options that are
512 passed to the executable upon invocation, as if specified in `bash`.
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.
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.
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
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.
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
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.
545 The function returns a list of server paths to the created jobfiles.
548 if len(rundirs) % chunkSize != 0:
551 if excludedNodes
is not None:
552 for x
in [
"-w",
"--nodelist",
"-x",
"--exclude"]:
553 if x
in slurmOptions:
555 "Cannot have Slurm option " + x +
" if " + \
556 "`nodeSpecification` is given")
558 for x
in [
"-D",
"--chdir"]:
560 raise ValueError(
"Cannot have option " + x +
" in srunOptions")
562 if not pathTranslator:
563 pathTranslator =
lambda x: x
569 rundirs[first:first + chunkSize]
570 for first
in range(0, len(rundirs), chunkSize)
575 script =
"#!/bin/bash" +
"\n"
577 jobsInChunk = len(chunk)
578 jobfileRundirPath = os.path.abspath(chunk[0])
579 jobfilePath = jobfileRundirPath +
"/input/job.slrm"
580 mySlurmOptions = slurmOptions.copy()
582 if not os.path.isdir(jobfileRundirPath +
"/input"):
583 os.mkdir(jobfileRundirPath +
"/input")
585 if excludedNodes
is not None:
586 mySlurmOptions[
"--exclude"] = excludedNodes
588 for key, value
in mySlurmOptions.items():
589 script +=
"#SBATCH " + key
596 value.replace(
"$RUNDIR", pathTranslator(jobfileRundirPath))
597 value = value.replace(
"$JOBS_PER_NODE", str(jobsInChunk))
604 if not os.path.isdir(rundir +
"/input"):
605 os.mkdir(rundir +
"/input")
607 if rundir != jobfileRundirPath:
608 parentJobPath = rundir +
"/input/parent-job-path.txt"
609 with open(parentJobPath,
"w")
as f:
610 f.write(pathTranslator(jobfileRundirPath))
612 absolutePath = pathTranslator(os.path.abspath(rundir))
615 script +=
" --chdir='" + absolutePath +
"/input'"
617 for key, value
in srunOptions.items():
619 if value
is not None:
624 script += value.replace(
"$RUNDIR", absolutePath)
626 script +=
" '" + executablePath +
"'"
627 script +=
" " + executableOptions
628 script +=
" --rundir '" + absolutePath +
"'"
631 script +=
"wait" +
"\n"
633 with open(jobfilePath,
"w")
as jobfile:
634 jobfile.write(script)
637 jobfiles.append(pathTranslator(jobfilePath))
642 def __deprecated__createRundirs(
643 self, baseConfig, basePath, generators, rundirNamePrefix = ""):
645 Creates new rundirs, and configuration files therein.
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`.
654 `rundirNamePrefix` is a prefix that is prepended to all rundir directory
657 The function returns a list of all created run directories.
660 if not isinstance(generators, list):
661 generators = [generators]
663 if not os.path.isdir(basePath):
664 raise ValueError(
"Base path does not exist: " + basePath)
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
678 if pathComponent
is not None:
679 pathComponents +=
"---" + pathComponent
682 for x
in range(0,
pow(10, numberOfDigits)):
685 pathComponents +
"---" + \
686 (
"{:0>" + str(numberOfDigits) +
"}").format(x)
687 if not os.path.exists(path):
690 with open(path +
"/config.txt",
"w")
as f:
698 def __deprecated__createSlurmJobScripts(
699 self, rundirs, executablePath, executableOptions,
700 slurmOptions = {}, srunOptions = {},
701 chunkSize = 1, largeChunkNodeSpecification = None,
702 pathTranslator = None):
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
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
716 `executableOptions` is a string that contains all options that are
717 passed to the executable upon invocation, as if specified in `bash`.
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.
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.
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.
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
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
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.
756 The function returns a list of paths to the created jobfiles.
759 if largeChunkNodeSpecification
is not None and chunkSize != 1:
760 for x
in [
"-w",
"--nodelist",
"-x",
"--exclude"]:
761 if x
in slurmOptions:
763 "Cannot have Slurm option " + x +
" if " + \
764 "`chunkSize != 1` and " + \
765 "`largeChunkNodeSpecification` is given")
767 for x
in [
"-D",
"--chdir"]:
769 raise ValueError(
"Cannot have option " + x +
" in srunOptions")
771 if not pathTranslator:
772 pathTranslator =
lambda x: x
778 rundirs[first:first + chunkSize]
779 for first
in range(0, len(rundirs), chunkSize)
782 if len(chunks[-1]) < chunkSize:
783 popped = chunks.pop()
788 script =
"#!/bin/bash" +
"\n"
790 jobsInChunk = len(chunk)
791 jobfileRundirPath = os.path.abspath(chunk[0])
792 jobfilePath = jobfileRundirPath +
"/input/job.slrm"
793 mySlurmOptions = slurmOptions.copy()
795 if not os.path.isdir(jobfileRundirPath +
"/input"):
796 os.mkdir(jobfileRundirPath +
"/input")
798 if largeChunkNodeSpecification
is not None:
799 if jobsInChunk < chunkSize
or chunkSize == 1:
800 mySlurmOptions[
"--exclude"] = largeChunkNodeSpecification
802 for key, value
in mySlurmOptions.items():
803 script +=
"#SBATCH " + key
810 value.replace(
"$RUNDIR", pathTranslator(jobfileRundirPath))
811 value = value.replace(
"$JOBS_PER_NODE", str(jobsInChunk))
818 if not os.path.isdir(rundir +
"/input"):
819 os.mkdir(rundir +
"/input")
821 if rundir != jobfileRundirPath:
822 parentJobPath = rundir +
"/input/parent-job-path.txt"
823 with open(parentJobPath,
"w")
as f:
824 f.write(pathTranslator(jobfileRundirPath))
826 absolutePath = pathTranslator(os.path.abspath(rundir))
829 script +=
" --chdir='" + absolutePath +
"/input'"
831 for key, value
in srunOptions.items():
833 if value
is not None:
838 script += value.replace(
"$RUNDIR", absolutePath)
840 script +=
" '" + executablePath +
"'"
841 script +=
" " + executableOptions
842 script +=
" --rundir '" + absolutePath +
"'"
845 script +=
"wait" +
"\n"
847 with open(jobfilePath,
"w")
as jobfile:
848 jobfile.write(script)
851 jobfiles.append(pathTranslator(jobfilePath))
856 def _makeSlurmNodeList(self, nodes):
858 Returns a `Slurm`-compatible node-specification string.
870 def _parseSlrumNodeList(self, nodeList):
872 Returns a list of node names, corresponding to the `Slurm` node list.
878 parts = nodeList.split(
",")
885 regex =
r"\[([0-9]+)-([0-9]+)\]"
886 parts = re.split(regex, nodeList)
890 start = int(parts[1])
892 for number
in range(start, end + 1):
898 def _execfile(self, path, myGlobals = None, myLocals = None):
900 Executes the file's contents in the current context.
903 with open(path)
as f:
904 exec(compile(f.read(), path,
'exec'), myGlobals, myLocals)
909 Class that represents a special action to be performed in config part
910 generators, like deleting settings or creating setting groups.
918 Throws if `action` is not a `str`.
920 Throws if `action` has an illegal value.
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
929 if not isinstance(action, str):
932 if action
not in [
"delete",
"createGroup"]:
933 raise ValueError(action)
940 Returns whether the action is to delete the associated setting.
943 return self.
_action ==
"delete"
948 Returns whether the action is to create the associated setting group.
951 return self.
_action ==
"createGroup"