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 """read_tpr operation module
37 Provides implementation classes and user interface for gmxapi.read_tpr.
45 import gmxapi.exceptions
46 import gmxapi.operation as _op
48 from gmxapi.operation import InputCollectionDescription
49 from gmxapi.simulation.abc import ModuleObject
53 # Initialize module-level logger
54 from gmxapi import logger as root_logger
56 logger = root_logger.getChild('read_tpr')
57 logger.info('Importing {}'.format(__name__))
61 # Interface classes and internal details
64 # TODO: The output of read_tpr and modify_input should be the same and part of the
65 # simulation module specification. Such output is either a special type of output proxy
67 _output_descriptors = (
68 _op.OutputDataDescriptor('_simulation_input', str),
69 _op.OutputDataDescriptor('parameters', dict)
71 _publishing_descriptors = {desc._name: gmxapi.operation.Publisher(desc._name, desc._dtype) for desc in
73 _output = _op.OutputCollectionDescription(**{descriptor._name: descriptor._dtype for descriptor in
78 class OutputDataProxy(ModuleObject,
80 descriptors=_output_descriptors):
81 """Implement the 'output' attribute of ReadTpr operations."""
82 def __init__(self, *args, **kwargs):
83 _op.DataProxyBase.__init__(self, *args, **kwargs)
86 class PublishingDataProxy(_op.DataProxyBase,
87 descriptors=_publishing_descriptors
89 """Manage output resource updates for ReadTpr operation."""
92 _output_factory = _op.OutputFactory(output_proxy=OutputDataProxy,
93 output_description=_output,
94 publishing_data_proxy=PublishingDataProxy)
97 class SessionResources(object):
98 """Input and output run-time resources for a ReadTpr operation."""
99 def __init__(self, tpr_filename, publisher: PublishingDataProxy):
100 self.tpr_object = fileio.TprFile(filename=tpr_filename, mode='r')
101 self.output = publisher
109 _input = _op.InputCollectionDescription(
110 [('filename', inspect.Parameter('filename',
111 inspect.Parameter.POSITIONAL_OR_KEYWORD,
115 # TODO: Clarify. The actual input and output arguments passed are customized for this operation.
116 def _session_resource_factory(input: _op.InputPack, output: 'PublishingDataProxy') -> SessionResources:
117 """Translate resources from the gmxapi.operation Context to the ReadTpr implementation."""
118 filename = input.kwargs['filename']
119 return SessionResources(tpr_filename=filename, publisher=output)
122 def _standard_node_resource_factory(*args, **kwargs):
123 """Translate Python UI input to the gmxapi.operation node builder inputs."""
124 return _input.bind(*args, **kwargs)
127 def _run(resources: SessionResources):
128 """Operation implementation in the gmxapi.operation module context."""
129 # TODO: Implement for other source/target contexts. We don't always need to
130 # produce all outputs.
131 with resources.tpr_object as fh:
132 params = fh._tprFileHandle.params().extract()
133 resources.output.parameters = params
134 resources.output._simulation_input = fh.filename
141 # Note: we borrow the implementation from operation.ResourceManager for now,
142 # but in the future we want the implementations to either be decoupled or
143 # for implementations in a given context to be coupled to details that are clearly
144 # and explicitly related to that context. Right now, operation.ResourceManager
145 # is tied to the implementation of Contexts in gmxapi.operation, but that is not
146 # sufficiently clear and explicit.
147 class ResourceManager(gmxapi.operation.ResourceManager):
148 """Manage resources for the ReadTpr operation in the gmxapi.operation contexts.
150 Extends gmxapi.operation.ResourceManager to tolerate non-standard data payloads.
151 Futures managed by this resource manager may contain additional attributes.
153 def future(self, name: str, description: _op.ResultDescription):
154 tpr_future = super().future(name=name, description=description)
157 def data(self) -> OutputDataProxy:
158 return OutputDataProxy(self)
160 def update_output(self):
161 logger.debug('Updating output for {}.'.format(self.operation_id))
162 super().update_output()
163 for descriptor in _output_descriptors:
164 name = descriptor._name
165 if not self.is_done(name):
166 raise gmxapi.exceptions.ApiError('Expected output {} not updated.'.format(name))
168 # TODO: Consider making Generic in source and target context type variables,
169 # or leave unspecified and use generic function or pair of single_dispatch functions.
170 # Need to know the right home for input_description, if needed.
171 class ResourceFactory(gmxapi.abc.ResourceFactory):
172 """ReadTpr resource factory.
174 Generic class for creating resources passed to read_tpr implementation details.
175 Dispatching may occur based on the source and target Context of factory action.
177 def __init__(self, target_context, source_context):
178 """Initialize an instance to support read_tpr action.
181 source_context: The source of the resources in the calling scope.
182 target_context: The Context in which the factory product will be consumed.
185 self.source_context = source_context # Determine input form of *create* method.
186 self.target_context = target_context # Context in which resources are consumed.
188 # TODO: clean up the dispatching. What to do when input comes from multiple sources?
189 # Use a typing overload or a single-dispatch functor for clearer input/result typing.
190 def __call__(self, *args, **kwargs):
191 """Create the resource product.
193 Dispatch to appropriate factory functionality based on the *source_context*
194 and *target_context*.
196 # context is used for dispatching and annotates the Context of the other arguments.
197 # context == None implies inputs are from Python UI.
198 if self.source_context is None:
199 if isinstance(self.target_context, _op.Context):
200 return _standard_node_resource_factory(*args, **kwargs)
201 if isinstance(self.source_context, _op.Context):
202 # TODO: Check whether the consumer is a Context.NodeBuilder or an operation runner.
203 # We don't yet use this dispatcher for building nodes, so assume we are launching a session.
204 assert 'input' in kwargs
205 assert 'output' in kwargs
206 return _session_resource_factory(input=kwargs['input'], output=kwargs['output'])
207 raise gmxapi.exceptions.NotImplementedError(
208 'No translation from {} context to {}'.format(self.source_context, self.target_context))
211 def input_description(self, context: _op.Context) -> _op.InputDescription:
214 def input_description(self, context: gmxapi.abc.Context):
215 """Get an input description usable in the indicated Context.
218 context: Context for which to dispatch the generation of input description.
220 Overrides gmxapi.abc.ResourceFactory for collaboration between this
221 resource factory and a target Context.
223 # TODO: Clarify exposure and scope as gmxapi.abc.ResourceFactory is refined.
224 # The expected use case is that a Context implementation may consult the
225 # input_description when preparing input to a resource factory, or even
226 # when determining in which Context a resource should be created or an
227 # operation dispatched. It seems sensible that the ResourceFactory should
228 # be able to describe its possible inputs. Instance functions are the
229 # most common and simple Python detail, so it makes sense for this to be
230 # an instance method instead of classmethod or staticmethod, until shown
232 # Also note that ResourceFactory is likely to be a Generic class or a
233 # container for composed functionality. Customizing method signatures
234 # based on instance data is more brittle than behavior determined at the
235 # class level. The behavior of this function is determined at the class
236 # level to be a dispatcher, which may utilize instance data, but as an
237 # implementation detail that is not the business of the caller.
238 # This should probably be implemented in terms of the standard Python
239 # functools.single_dispatch generic function, but it is cleaner if the
240 # method itself is not generic beyond the level of typing overloads.
241 if isinstance(context, _op.Context):
242 return StandardInputDescription()
243 raise gmxapi.exceptions.NotImplementedError('No input description available for {} context'.format(context))
246 class StandardInputDescription(_op.InputDescription):
247 """Provide the ReadTpr input description in gmxapi.operation Contexts."""
249 # TODO: Improve fingerprinting.
250 # If _make_uid can't make sufficiently unique IDs, use additional "salt":
254 def _make_uid(input) -> str:
255 # TODO: Use input fingerprint for more useful identification.
257 # If can't make sufficiently unique IDs, use additional "salt"
258 # from a class data member. E.g.
259 # new_uid = 'read_tpr_{}_{}'.format(_next_uid, salt)
261 new_uid = 'read_tpr_{}'.format(salt)
264 def signature(self) -> InputCollectionDescription:
267 def make_uid(self, input: _op.DataEdge) -> str:
268 assert isinstance(input, _op.DataEdge)
269 return self._make_uid(input)
272 class RegisteredOperation(_op.OperationImplementation, metaclass=_op.OperationMeta):
273 """Provide the gmxapi compatible ReadTpr implementation."""
275 # This is a class method to allow the class object to be used in gmxapi.operation._make_registry_key
277 def name(self) -> str:
278 """Canonical name for the operation."""
282 def namespace(self) -> str:
283 """read_tpr is importable from the gmxapi module."""
287 def director(cls, context: gmxapi.abc.Context) -> _op.OperationDirector:
288 if isinstance(context, _op.Context):
289 return StandardDirector(context)
290 raise gmxapi.exceptions.NotImplementedError(
291 'No dispatcher for context {} of type {}'.format(context, type(context)))
294 class StandardOperationHandle(_op.AbstractOperation, ModuleObject):
295 """Handle used in Python UI or gmxapi.operation Contexts."""
296 def __init__(self, resource_manager: ResourceManager):
297 self.__resource_manager = resource_manager
300 self.__resource_manager.update_output()
303 def output(self) -> OutputDataProxy:
304 return self.__resource_manager.data()
307 class StandardDirector(gmxapi.abc.OperationDirector):
308 """Direct the instantiation of a read_tpr node in a gmxapi.operation Context.
310 .. todo:: Compose this behavior in a more generic class.
312 .. todo:: Describe where instances live.
314 def __init__(self, context: _op.Context):
315 if not isinstance(context, _op.Context):
316 raise gmxapi.exceptions.ValueError('StandardDirector requires a gmxapi.operation Context.')
317 self.context = context
319 def __call__(self, resources: _op.DataSourceCollection, label: str) -> StandardOperationHandle:
320 builder = self.context.node_builder(operation=RegisteredOperation, label=label)
322 builder.set_resource_factory(_session_resource_factory)
323 builder.set_input_description(StandardInputDescription())
324 builder.set_handle(StandardOperationHandle)
326 def runner_director(resources):
331 builder.set_runner_director(runner_director)
332 builder.set_output_factory(_output_factory)
334 # Note: we have not yet done any dispatching based on *resources*. We should
335 # translate the resources provided into the form that the Context can subscribe to
336 # using the dispatching resource_factory. In the second draft, this operation
337 # is dispatched to a SimulationModuleContext, which can be subscribed directly
338 # to sources that are either literal filenames or gmxapi.simulation sources,
339 # while standard Futures can be resolved in the standard context.
341 assert isinstance(resources, _op.DataSourceCollection)
342 assert 'filename' in resources
343 builder.add_input('filename', resources['filename'])
345 handle = builder.build()
346 assert isinstance(handle, StandardOperationHandle)
349 def handle_type(self, context: gmxapi.abc.Context):
350 return StandardOperationHandle
352 def resource_factory(self,
353 source: typing.Union[gmxapi.abc.Context, None],
354 target: gmxapi.abc.Context = None) -> ResourceFactory:
355 # Distinguish between the UIContext, in which input is in the form
356 # of function call arguments, and the StandardContext, implemented in
357 # gmxapi.operation. UIContext is probably a virtual context that is
358 # asserted by callers in order to get a factory that normalizes UI input
359 # for the StandardContext.
362 target = self.context
364 if isinstance(target, _op.Context):
365 return ResourceFactory(target_context=target, source_context=source)
366 if isinstance(source, _op.Context):
367 return ResourceFactory(target_context=target, source_context=source)
368 raise gmxapi.exceptions.ValueError('No dispatching from {} context to {}'.format(source, target))
371 def read_tpr(filename, label: str = None, context=None):
375 filename: input file name
376 label: optional human-readable label with which to tag the new node
377 context: Context in which to return a handle to the new node.
378 Use default (None) for Python scripting interface
381 Reference (handle) to the new operation instance (node).
384 handle_context = context
385 if handle_context is not None:
386 raise gmxapi.exceptions.NotImplementedError(
387 'context must be None. This factory is only for the Python UI right now.')
389 # 1. Handle node creation in the scripting interface.
390 # When *context* input is None, dispatch to the current Context. Confirm that
391 # it is a standard context from the gmxapi.operation module, translate the
392 # input into that context, and create the node.
394 # 2. Dispatch to SimulationModuleContext.
395 # Operation is not fully implemented in gmxapi.operation context. When creating
396 # a node in a gmxapi.operation context, dispatch and subscribe to a SimulationModuleContext,
399 # 3. Handle consumption in SimulationModuleContext.
400 # When a consuming operation is native to the SimulationModuleContext,
401 # detect that a richer interface is available and use it. Possible implementation:
402 # Chain of Responsibility: subscribe() is serviced by the Context "closest"
403 # to the source. subscriber is the most native compatible Context for the consuming operation
404 # but decays to the context in which the handle is being created. subscribe()
405 # can accept a callback function and an indication of the form of the message
406 # to send (a consuming Context or ResourceFactory).
408 # TODO: Other types of input, such as File placeholders.
410 target_context = _op.current_context()
411 assert isinstance(target_context, _op.Context)
412 # Get a director that will create a node in the standard context.
413 node_director = _op._get_operation_director(RegisteredOperation, context=target_context)
414 assert isinstance(node_director, StandardDirector)
415 # TODO: refine this protocol
416 assert handle_context is None
417 resource_factory = node_director.resource_factory(source=handle_context, target=target_context)
418 resources = resource_factory(filename=filename)
419 handle = node_director(resources=resources, label=label)
420 # Note: One effect of the assertions above is to help the type checker infer
421 # the return type of the handle. It is hard to convince the type checker that
422 # the return value of the node builder is up-cast. We might be able to resolve
423 # this by making both get_operation_director and ReadTprImplementation
424 # generics of the handle type, or using the handle type as the key for
425 # get_operation_director.