2 # This file is part of the GROMACS molecular simulation package.
4 # Copyright (c) 2019, by the GROMACS development team, led by
5 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6 # and including many others, as listed in the AUTHORS file in the
7 # top-level source directory and at http://www.gromacs.org.
9 # GROMACS is free software; you can redistribute it and/or
10 # modify it under the terms of the GNU Lesser General Public License
11 # as published by the Free Software Foundation; either version 2.1
12 # of the License, or (at your option) any later version.
14 # GROMACS is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 # Lesser General Public License for more details.
19 # You should have received a copy of the GNU Lesser General Public
20 # License along with GROMACS; if not, see
21 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
22 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 # If you want to redistribute modifications to GROMACS, please
25 # consider that scientific software is very special. Version
26 # control is crucial - bugs must be traceable. We will be happy to
27 # consider code for inclusion in the official distribution, but
28 # derived work must not be called official GROMACS. Details are found
29 # in the README & COPYING files - if they are missing, get the
30 # official version at http://www.gromacs.org.
32 # To help us fund GROMACS development, we humbly ask that you cite
33 # the research papers on the package. Check out http://www.gromacs.org.
35 # This file is based on the Kasson Lab gmxapi project release 0.0.7.4.
36 # https://github.com/kassonlab/gmxapi/blob/v0.0.7.4/src/gmx/workflow.py
37 # # https://github.com/kassonlab/gmxapi/blob/v0.0.7.4/LICENSE
39 Provide workflow-level utilities and classes
40 ============================================
42 Supports the implementation of operations in the gmxapi.simulation module.
45 __all__ = ['from_tpr', 'WorkSpec', 'WorkElement']
50 from typing import Text, Iterable, Set
53 from gmxapi import exceptions
56 logger = gmx.logger.getChild('simulation.workflow')
57 logger.info('Importing gmx.workflow')
59 # Work specification version string.
60 workspec_version = "gmxapi_workspec_0_1"
61 logger.info("Using schema version {}.".format(workspec_version))
64 def to_utf8(input) -> bytes:
65 """Return a utf8 encoded byte sequence of the Unicode ``input`` or its string representation.
68 :py:bytes byte sequence.
70 if isinstance(input, str):
71 value = input.encode('utf-8')
72 elif isinstance(input, bytes):
77 value = string.encode('utf-8')
78 except Exception as e:
79 raise exceptions.ValueError("Input cannot be interpreted as a UTF-8 compatible string.") from e
83 def to_string(input) -> str:
84 """Return a Unicode string representation of ``input``.
86 If ``input`` or its string representation is not already a Unicode object, attempt to decode as utf-8.
88 Returns a native string, decoding utf-8 encoded byte sequences if necessary.
90 if isinstance(input, str):
94 value = input.decode('utf-8')
98 except Exception as e:
99 raise exceptions.ValueError("Cannot find a string representation of input.") from e
104 """Utility/compatibility class to ensure consistent keys.
106 Internally, converts all keys to native str for the current interpreter.
109 for key in dict.keys(self):
110 if not isinstance(key, str):
111 raise exceptions.ApiError('Invalid key type found: {} {}'.format(key, type(key)))
114 def __getitem__(self, key):
115 return super(GmxMap, self).__getitem__(str(key))
117 def __setitem__(self, key, item):
118 super(GmxMap, self).__setitem__(str(key), item)
120 def __delitem__(self, key):
121 super(GmxMap, self).__delitem__(str(key))
124 class WorkSpec(object):
126 Container of workflow elements with data dependency
127 information and requirements for execution.
129 An element cannot be added to a WorkSpec if it has dependencies that are not
132 Work is added to the specification by passing a WorkElement object to
133 :py:func:`WorkSpec.add_element()`.
134 Any dependencies in the WorkElement must already be specified in the target WorkSpec.
136 When iterated over, a WorkSpec object returns WorkElement objects.
137 WorkElement objects are yielded in a valid order to keep dependencies
138 satisfied, but not necessarily the same order in which add_element()
139 calls were originally made. In other words, the WorkSpec is a directed
140 acyclic dependency graph, and its iterator returns nodes in an arbitrary
141 but topologically correct order.
143 The string representation of a WorkSpec object is a valid JSON serialized data object.
145 The schema for version 0.1 of the specification allows data structures like
150 'version': 'gmxapi_workspec_0_1',
155 'namespace': 'gromacs',
156 'operation': 'load_tpr',
157 'params': {'input': ['tpr_filename1', 'tpr_filename2']}
161 'namespace': 'gmxapi',
162 'operation': 'open_global_data_with_barrier',
163 'params': ['data_filename']
167 'namespace': 'myplugin',
168 'operation': 'create_mdmodule',
170 'depends': ['mydata']
174 'namespace': 'gmxapi',
176 'depends': ['myinput', 'mypotential']
181 The first mapping (``version``) is required as shown. The ``elements`` map
182 contains uniquely named elements specifying an operation, the operation's
183 namespace, and parameters and dependencies of the operation for this element.
184 ``depends`` is a sequence of string names of elements that are also in the
185 work spec. ``params`` is a key-value map with string keys and values that
186 are valid JSON data. ``namespace`` and ``operation`` are strings that the
187 :py:class:`Context <gmx.context.Context>` can map to directors it uses to
188 construct the session. Namespace ``gmxapi`` is reserved for operations
189 specified by the API. Namespace ``gromacs`` is reserved for operations
190 implemented as GROMACS adapters (versioned separately from gmxapi). The
191 period character (".") has special meaning and should not be used in naming
192 elements, namespaces, or operations.
196 self.version = workspec_version
197 self.elements = GmxMap()
198 self.__context_weak_ref = None
203 if self.__context_weak_ref is not None:
204 referent = self.__context_weak_ref()
208 def _context(self, context):
209 # We're moving towards having the context own the work, so the work should
210 # not own the context.
211 self.__context_weak_ref = weakref.ref(context)
213 def __chase_deps(self, source_set: Set[str], name_list: Iterable[Text]):
214 """Helper to recursively generate dependencies before dependents.
216 Given a set of WorkElement objects and a list of element names, generate WorkElements for
217 the members of name_list plus their dependencies in an order such that dependencies are
218 guaranteed to occur before their dependent elements.
220 For example, to sequence an entire work specification into a reasonable order for instantiation, use
222 >>> workspec.__chase_deps(set(workspec.elements.keys()), list(workspec.elements.keys()))
224 Note: as a member function of WorkSpec, we have access to the full WorkSpec elements data at all
225 times, giving us extra flexibility in implementation and arguments.
228 source_set: a (super)set of element names from the current work spec (will be consumed)
229 name_list: subset of *sources* to be sequenced
232 Sequence of WorkElement objects drawn from the names in *source_set*
234 Requires that WorkElements named in *name_list* and any elements on which
235 they depend are all named in *source_list* and available in the current
238 Warning: *source_set* is a reference to an object that is modified arbitrarily.
239 The caller should not re-use the object after calling _chase_deps().
240 (Make a copy first, if needed.)
242 TODO: Separate out DAG topology operations from here and Context.__enter__()
243 Our needs are simple enough that we probably don't need an external dependency
246 # Recursively (depth-first) generate a topologically valid serialized DAG from source_set.
247 assert isinstance(source_set, set)
248 if isinstance(name_list, (str, bytes)):
249 warnings.warn('name_list appears to be a single name. Disambiguate a string by passing a list or tuple.')
250 assert isinstance(name_list, collections.abc.Iterable)
252 # Make a copy of name_list in case the input reference is being used elsewhere during
253 # iteration, such as for source_set, which is modified during the loop.
254 for name in tuple(name_list):
255 assert isinstance(name, str)
256 if name in source_set:
257 source_set.remove(name)
258 element = WorkElement.deserialize(self.elements[name], name=name, workspec=self)
259 dependencies = element.depends
260 # items in element.depends are either element names or ensembles of element names.
261 for item in dependencies:
262 if isinstance(item, (list, tuple, set)):
263 dependency_list = item
265 if not isinstance(item, str):
266 raise exceptions.ValueError(
267 'Dependencies should be a string or sequence of strings. Got {}'.format(type(item)))
268 dependency_list = [item]
269 for dependency in dependency_list:
270 for recursive_dep in self.__chase_deps(source_set, (dependency,)):
274 # Note: The user is responsible for ensuring that source_set is complete.
275 # Otherwise, we would need to maintain a list of elements previously yielded.
280 source_set = set(self.elements.keys())
281 for element in self.__chase_deps(source_set, source_set):
285 """Uniquely identify this work specification.
287 Allows the spec to be used as a dictionary key in Python. Note that this hash is possibly dependent on the
288 Python implementation. It is not part of the gmxapi specification and should not be used outside of a single
289 invocation of a script.
291 # Hash the serialized elements, concatenated as a single string. Note that the order of elements and their
292 # contents is not guaranteed, but should be consistent within a script invocation.
293 return hash(to_string(self.serialize()))
295 def add_element(self, element):
296 """Add an element to a work specification if possible.
298 Adding an element to a WorkSpec must preserve the validity of the workspec, which involves several checks.
299 We do not yet check for element uniqueness beyond a string name.
301 If an element is added that was previously in another WorkSpec, it must first be removed from the
304 if hasattr(element, "namespace") and hasattr(element, "operation") and hasattr(element, "serialize"):
305 if not hasattr(element, "name") or element.name is None or len(str(element.name)) < 1:
306 raise exceptions.UsageError("Only named elements may be added to a WorkSpec.")
307 if element.name in self.elements:
308 raise exceptions.UsageError("Elements in WorkSpec must be uniquely identifiable.")
309 if hasattr(element, "depends"):
310 for dependency in element.depends:
311 if not dependency in self.elements:
312 raise exceptions.UsageError(
313 "Element dependencies must already be specified before an Element may be added.")
314 # Okay, it looks like we have an element we can add
315 if hasattr(element, "workspec") and element.workspec is not None and element.workspec is not self:
316 raise exceptions.Error(
317 "Element must be removed from its current WorkSpec to be added to this WorkSpec, but element "
318 "removal is not yet implemented.")
319 self.elements[element.name] = element.serialize()
320 element.workspec = self
322 raise exceptions.ValueError(
323 "Provided object does not appear to be compatible with gmx.workflow.WorkElement.")
324 logger.info("Added element {} to workspec.".format(element.name))
327 """Serialize the work specification in a form suitable to pass to any Context implementation.
329 Serialization is performed with the JSON data serialization module.
331 To simplify unique identification of work specifications, this function will also impose rules for reproducibility.
333 1. All key-value maps are sorted alphanumerically by their string keys.
334 2. Strings must consist of valid ASCII characters.
335 3. Output is a byte sequence of the utf-8 encoded densely formatted JSON document.
338 ``unicode`` object in Python 2, ``bytes`` object in Python 3
340 Output of serialize() should be explicitly converted to a string before passing to a JSON deserializer.
342 >>> my_object = my_workspec.serialize()
343 >>> my_data_structure = json.loads(my_object.decode('utf-8'))
345 >>> my_data_structure = json.loads(my_object, encoding='utf-8')
349 # Build the normalized dictionary
350 dict_representation = {'version': self.version,
353 for name, element in [(e, json.loads(to_string(self.elements[e]))) for e in sorted(self.elements.keys())]:
354 dict_representation['elements'][str(name)] = element
355 serialization = json.dumps(dict_representation, ensure_ascii=True, sort_keys=True, separators=(',', ':'))
356 return serialization.encode('utf-8')
359 def deserialize(serialized):
361 workspec = WorkSpec()
362 dict_representation = json.loads(to_string(serialized))
363 ver_in = dict_representation['version']
364 ver_out = workspec.version
365 if ver_in != ver_out:
366 message = "Expected work spec version {}. Got work spec version {}.".format(ver_out, ver_in)
367 raise exceptions.ValueError(message)
368 for element in dict_representation['elements']:
369 workspec.elements[element] = dict_representation['elements'][element]
373 """Get a unique identifier for this work specification.
378 Generate a cryptographic hash of this work specification that is guaranteed to match that of another equivalent
379 work specification. The returned string is a 64-character hexadecimal encoded SHA-256 hash digest of the
382 The definition of equivalence is likely to evolve, but currently means a work spec of the
383 same version with the same named elements containing the same operations, dependencies, and parameters, as
384 represented in the serialized version of the work specification. Note that this does not include checks on the
385 actual contents of input files or anything that does not appear in the work specification directly. Also, the
386 hash is lossy, so it is remotely conceivable that two specs could have the same hash. The work specs
387 should be compared before making any expensive decisions based on work spec equivalence, such as with hash(workspec).
389 Element names probably shouldn't be included in the unique identifying information (so that we can optimize out
390 duplicated artifacts), but they are. A future API specification may add unique identification to the elements...
392 # Get an alphanumeric string of the checksum of the serialized work spec. SHA-256 should require about 43 characters
393 # of base64 to represent, which seems reasonable. We need to replace some of the base64 characters to make them
394 # filesystem friendly, though. Hexadecimal may be more friendly, but would require 64 characters.
396 data = to_utf8(self.serialize())
397 result = hashlib.sha256(data)
398 return result.hexdigest()
401 """Generate string representation for str() or print().
403 The string output should look like the abstract schema for gmxapi_workspec_1_0, but the exact
404 format is unspecified and may change in future versions.
406 For consistent JSON output, use WorkSpec.serialize().
409 string = to_string(self.serialize())
410 data = json.loads(string)
411 reserialized = json.dumps(data, indent=4, sort_keys=True)
412 return str(reserialized)
415 """Generate Pythonic representation for repr(workspec)."""
416 return 'gmx.workflow.WorkSpec()'
418 # A possible alternative name for WorkElement would be Operator, since there is a one-to-one
419 # mapping between WorkElements and applications of "operation"s. We need to keep in mind the
420 # sensible distinction between the WorkElement abstraction and the API objects and DAG nodes.
421 class WorkElement(object):
422 """Encapsulate an element of a work specification."""
423 def __init__(self, namespace="gmxapi", operation=None, params=None, depends=()):
424 self._namespace = str(to_string(namespace))
425 # We can add an operations submodule to validate these. E.g. self.operation = gmx.workflow.operations.normalize(operation)
426 if operation is not None:
427 self._operation = str(to_string(operation))
429 raise exceptions.UsageError("Invalid argument type for operation.")
431 # Note: Nothing currently prevents attribute updates by assignment after adding the element to a workspec,
432 # but this protocol will be clarified with https://github.com/kassonlab/gmxapi/issues/92
434 self.params = GmxMap()
435 elif isinstance(params, dict):
436 self.params = GmxMap({to_string(name): params[name] for name in params})
438 raise exceptions.UsageError("If provided, params must be a dictionary of keyword arguments")
441 if isinstance(d, (list, tuple)):
442 self.depends.append([str(name) for name in d])
444 self.depends.append(str(d))
446 # The Python class for work elements keeps a strong reference to a WorkSpec object containing its description
448 self._workspec = None
452 assert isinstance(self._namespace, str)
453 return self._namespace
457 assert isinstance(self._operation, str)
458 return self._operation
462 assert isinstance(self._name, (str, type(None)))
466 def name(self, new_name):
467 self._name = str(to_string(new_name))
471 return self._workspec
474 def workspec(self, input):
475 self._workspec = input
477 def add_dependency(self, element):
478 """Add another element as a dependency.
480 First move the provided element to the same WorkSpec, if not already here.
481 Then, add to ``depends`` and update the WorkSpec.
483 def check_element(element):
484 if element.workspec is None:
485 self.workspec.add_element(element)
486 assert element.workspec is self.workspec
487 assert element.name in self.workspec.elements
488 elif element.workspec is not self.workspec:
489 raise exceptions.ApiError("Element will need to be moved to the same workspec.")
492 if hasattr(element, 'workspec') and hasattr(element, 'name'):
493 check_element(element)
494 self.depends.append(element.name)
496 assert isinstance(element, (list, tuple))
497 self.depends.append(tuple([item.name for item in element if check_element(item)]))
499 self.workspec.elements[self.name] = self.serialize()
502 """Create a byte sequence representation of the work element.
504 The WorkElement class exists just to provide convenient handles in Python. The WorkSpec is not actually a
505 container of WorkElement objects.
508 Byte sequence of utf-8 encoded JSON document. May need to be decoded if needed as a (Unicode) string.
512 output_dict = {'namespace': self.namespace,
513 'operation': self.operation,
514 'params': self.params,
515 'depends': self.depends
517 serialization = json.dumps(output_dict)
518 return to_utf8(serialization)
521 def deserialize(cls, input, name=None, workspec=None):
522 """Create a new WorkElement object from a serialized representation.
525 input: a serialized WorkElement
526 name: new element name (optional) (deprecated)
527 workspec: an existing workspec to attach this element to (optional)
529 When subclasses become distinct, this factory function will need to do additional dispatching to create an object of the correct type.
530 Alternatively, instead of subclassing, a slightly heavier single class may suffice, or more flexible duck typing might be better.
533 input_string = to_string(input)
534 args = json.loads(input_string)
535 element = cls(namespace=args['namespace'], operation=args['operation'], params=args['params'], depends=args['depends'])
538 # This conditional is nested because we can only add named elements to a WorkSpec.
539 if workspec is not None:
540 element.workspec = workspec
541 if element.name not in workspec.elements:
542 workspec.add_element(element)
545 class SharedDataElement(WorkElement):
546 """Work element with MD-specific extensions.
548 The schema may not need to be changed, but the API object may be expected to provide additional functionality.
550 def __init__(self, params, name=None):
551 """Create a blank SharedDataElement representation.
553 It may be appropriate to insist on creating objects of this type via helpers or factories, particularly if
554 creation requires additional parameters.
556 self.args = params['args']
557 self.kwargs = params['kwargs']
558 super(SharedDataElement, self).__init__(namespace="gmxapi",
559 operation="global_data",
560 params={'args': self.args, 'kwargs': self.kwargs})
564 def get_source_elements(workspec):
565 """Get an iterator of the starting nodes in the work spec.
567 Source elements have no dependencies and can be processed immediately. Elements with dependencies
568 cannot be processed, instantiated, or added to a work spec until after their dependencies have been.
571 workspec : an existing work specification to analyze, such as by a Context implementation preparing to schedule work.
574 iterator of gmx.workflow.WorkElement objects that may be processed without dependencies.
576 This function is provided in the API to allow flexibility in how source elements are determined.
578 for name in workspec.elements:
579 element_data = workspec.elements[name]
580 element = WorkElement.deserialize(element_data)
581 if len(element.depends) == 0:
583 element.workspec = workspec
587 def from_tpr(input=None, **kwargs):
588 """Create a WorkSpec from a (list of) tpr file(s).
590 Generates a work specification based on the provided simulation input and returns a handle to the
591 MD simulation element of the workflow. Key word arguments can override simulation behavior from
594 If the MD operation discovers artifacts from a previous simulation that was launched from the same input,
595 the simulation resumes from the last checkpointed step. If ``append_output`` is set ``False``, existing
596 artifacts are kept separate from new output with the standard file naming convention,
597 and new output begins from the last checkpointed step, if any.
599 Setting ``end_time`` redefines the end point of the simulation trajectory from what was provided in
600 ``input``. It is equivalent to changing the number of steps requested in the MDP (or TPR) input, but
601 the time is provided as picoseconds instead of a number of time steps.
603 .. deprecated:: 0.0.7
604 If ``steps=N`` is provided and N is an integer
605 greater than or equal to 1, the MD operation advances the trajectory by ``N`` steps, regardless of the number
606 of simulation steps specified in ``input`` or ``end_time``. For convenience, setting ``steps=None`` does not override
608 Note that when it is not ``None``, ``steps`` takes precedence over ``end_time`` and ``input``, but can still be
609 superceded by a signal, such as if an MD plugin or other code has a simulation completion condition that occurs
610 before ``N`` additional steps have run.
612 Where key word arguments correspond to ``gmx mdrun`` command line options, the corresponding flags are noted below.
615 input (str): *Required* string or list of strings giving the filename(s) of simulation input
616 append_output (bool): Append output for continuous trajectories if True, truncate existing output data if False. (default True)
617 end_time (float): Specify the final time in the simulation trajectory, overriding input read from TPR.
618 grid (tuple): Domain decomposition grid divisions (nx, ny, nz). (-dd)
619 max_hours (float): Terminate after 0.99 times this many hours if simulation is still running. (-maxh)
620 pme_ranks (int): number of separate ranks to be used for PME electrostatics. (-npme)
621 threads_per_pme_rank (int): Number of OpenMP threads per PME rank. (-ntomp_pme)
622 steps (int): Override input files and run for this many steps. (-nsteps; deprecated)
623 threads (int): Total number of threads to start. (-nt)
624 threads_per_rank (int): number of OpenMP threads to start per MPI rank. (-ntomp)
625 tmpi (int): number of thread-MPI ranks to start. (-ntmpi)
627 .. versionchanged:: 0.1
628 *pme_threads_per_rank* renamed to *threads_per_pme_rank*.
631 simulation member of a gmx.workflow.WorkSpec object
633 Produces a WorkSpec with the following data::
635 version: gmxapi_workspec_0_1
640 params: {'input': ['tpr_filename1', 'tpr_filename2', ...]}
644 depends: ['tpr_input']
645 params: {'kw1': arg1, 'kw2': arg2, ...}
648 * There is not a way to programatically check the current step number on disk.
649 See https://github.com/kassonlab/gmxapi/issues/56 and https://github.com/kassonlab/gmxapi/issues/85
653 usage = "argument to from_tpr() should be a valid filename or list of filenames, followed by optional key word arguments."
655 # Normalize to tuple input type.
656 if isinstance(input, list) or isinstance(input, tuple):
657 tpr_list = tuple([to_string(element) for element in input])
660 tpr_list = (to_string(input),)
662 raise exceptions.UsageError(usage)
664 # Check for valid filenames
666 if not (os.path.exists(arg) and os.path.isfile(arg)):
667 arg_path = os.path.abspath(arg)
668 raise exceptions.UsageError(usage + " Got {}".format(arg_path))
670 # Note: These are runner parameters, not MD parameters, and should be in the call to gmx.run() instead of here.
671 # Reference https://github.com/kassonlab/gmxapi/issues/95
673 for arg_key in kwargs:
674 if arg_key == 'grid' or arg_key == 'dd':
675 params['grid'] = tuple(kwargs[arg_key])
676 elif arg_key == 'pme_ranks' or arg_key == 'npme':
677 params['pme_ranks'] = int(kwargs[arg_key])
678 elif arg_key == 'threads' or arg_key == 'nt':
679 params['threads'] = int(kwargs[arg_key])
680 elif arg_key == 'tmpi' or arg_key == 'ntmpi':
681 params['tmpi'] = int(kwargs[arg_key])
682 elif arg_key == 'threads_per_rank' or arg_key == 'ntomp':
683 params['threads_per_rank'] = int(kwargs[arg_key])
684 elif arg_key == 'pme_threads_per_rank' or arg_key == 'threads_per_pme_rank' or arg_key == 'ntomp_pme':
685 # TODO: Remove this temporary accommodation.
686 assert not gmx.version.api_is_at_least(0, 2)
687 if arg_key == 'pme_threads_per_rank':
688 warnings.warn("Key word pme_threads_per_rank has been renamed to threads_per_pme_rank.",
690 params['threads_per_pme_rank'] = int(kwargs[arg_key])
691 elif arg_key == 'steps' or arg_key == 'nsteps':
692 if kwargs[arg_key] is None:
693 # None means "don't override the input" which is indicated by a parameter value of -2 in GROMACS 2019
696 # Otherwise we require steps to be a positive integer
698 steps = int(kwargs[arg_key])
700 raise exceptions.ValueError('steps to run must be at least 1')
701 except (TypeError, ValueError) as e:
702 # steps is not an integer.
703 raise exceptions.TypeError('"steps" could not be interpreted as an integer.')
704 # The "nsteps" command line flag will be removed in GROMACS 2020
705 # and so "steps" is deprecated in gmxapi 0.0.7
706 warnings.warn("`steps` keyword argument is deprecated. Consider `end_time` instead.",
708 params['steps'] = steps
709 elif arg_key == 'max_hours' or arg_key == 'maxh':
710 params['max_hours'] = float(kwargs[arg_key])
711 elif arg_key == 'append_output':
712 # Try not to encourage confusion with the `mdrun` `-noappend` flag, which would be a confusing double negative if represented as a bool.
713 params['append_output'] = bool(kwargs[arg_key])
714 elif arg_key == 'end_time':
715 params[arg_key] = float(kwargs[arg_key])
717 raise exceptions.UsageError("Invalid key word argument: {}. {}".format(arg_key, usage))
719 # Create an empty WorkSpec
720 workspec = WorkSpec()
722 # Create and add the Element for the tpr file(s)
723 inputelement = WorkElement(namespace='gromacs', operation='load_tpr', params={'input': tpr_list})
724 inputelement.name = 'tpr_input'
725 if inputelement.name not in workspec.elements:
726 # Operations such as this need to be replaced with accessors or properties that can check the validity of the WorkSpec
727 workspec.elements[inputelement.name] = inputelement.serialize()
728 inputelement.workspec = workspec
730 # Create and add the simulation element
731 # We can add smarter handling of the `depends` argument, but it is only critical to check when adding the element
733 mdelement = WorkElement(operation='md', depends=[inputelement.name], params=params)
734 mdelement.name = 'md_sim'
735 # Check that the element has not already been added, but that its dependency has.
736 workspec.add_element(mdelement)