1
2
3
4
5
6 """
7 :mod:`analysis.core` -- Core classes for analysis of Gromacs trajectories
8 =========================================================================
9
10 This documentation is mostly of interest to programmers who want to write
11 analysis plugins.
12
13
14 Programming API for plugins
15 ---------------------------
16
17 Additional analysis capabilities are added to a
18 :class:`gromacs.analysis.Simulation` class with *plugin* classes. For
19 an example see :mod:`gromacs.analysis.plugins`.
20
21
22 API description
23 ...............
24
25 Analysis capabilities can be added with plugins to the simulation class. Each
26 plugin is registered with the simulation class and provides at a minimum
27 :meth:`~Worker.run`, :meth:`~Worker.analyze`, and :meth:`~Worker.plot` methods.
28
29 A plugin consists of a subclass of :class:`Plugin` and an associated :class:`Worker`
30 instance. The former is responsible for administrative tasks and documentation,
31 the latter implements the analysis code.
32
33 A plugin class must be derived from :class:`Plugin` and typically bears the
34 name that is used to access it. A plugin instance must be *registered* with a
35 :class:`Simulation` object. This can be done implicitly by passing the
36 :class:`Simulation` instance in the ``simulation`` keyword argument to the
37 constructor or by explicitly calling the :meth:`Plugin.register` method with
38 the simulation instance. Alternatively, one can also register a plugin via the
39 :meth:`Simulation.add_plugin` method.
40
41 Registering the plugin means that the actual worker class is added to the
42 :attr:`Simulation.plugins` dictionary.
43
44 A plugin must eventually obtain a pointer to the :class:`Simulation` class in
45 order to be able to access simulation-global parameters such as top directories
46 or input files.
47
48 See :class:`analysis.plugins.CysAccessibility` and
49 :class:`analysis.plugins._CysAccessibility` in
50 ``analysis/plugins/CysAccessibility.py`` as examples.
51
52
53 API requirements
54 ................
55
56 * Each plugin is contained in a separate module in the
57 :mod:`gromacs.analysis.plugins` package. The name of the module *must* be the
58 name of the plugin class in all lower case.
59
60 * The plugin name is registered in
61 :const:`gromacs.analysis.plugins.__plugins__`. (Together with the file naming
62 convention this allows for automatic and consistent loading.)
63
64 * The plugin itself is derived from :class:`Plugin`; the only changes are the
65 doc strings and setting the :attr:`Plugin.worker_class` class attribute to a
66 :class:`Worker` class.
67
68 * The corresponding worker class is derived from :class:`Worker` and must implement
69
70 - :meth:`Worker.__init__` which can only use keyword arguments to initialize
71 the plugin. It must ensure that init methods of super classes are also
72 called. See the existing plugins for details.
73
74 - :meth:`Worker.run` which typically generates the data by analyzing a
75 trajectory, possibly multiple times. It should store results in files.
76
77 - :meth:`Worker.analyze` analyzes the data generated by :meth:`Worker.run`.
78
79 - :meth:`Worker.plot` plots the analyzed data.
80
81 - :meth:`Worker._register_hook` (see below)
82
83 * The worker class can access parameters of the simulation via the
84 :attr:`Worker.simulation` attribute that is automatically set when the plugin
85 registers itself with :class:`Simulations`. However, the plugin should *not*
86 rely on :attr:`~Worker.simulation` being present during initialization
87 (__init__) because registration of the plugin might occur *after* init.
88
89 This also means that one cannot use the directory methods such as
90 :meth:`Worker.plugindir` because they depend on :meth:`Simulation.topdir` and
91 :meth:`Simulation.plugindir`.
92
93 Any initialization that requires access to the :class:`Simulation` instance
94 should be moved into the :meth:`Worker._register_hook` method. It is called
95 when the plugin is actually being registered. Note that the hook *must* also
96 call the hook of the super class before setting any values. The hook should
97 pop any arguments that it requires and ignore everything else.
98
99 * Parameters of the plugin are stored in :attr:`Worker.parameters` (either as
100 attributes or as key/value pairs, see the container class
101 :class:`gromacs.utilities.AttributeDict`).
102
103 * Results are stored in :attr:`Worker.results` (also a :class:`gromacs.utilities.AttributeDict`).
104
105
106 Classes
107 -------
108
109 .. autoclass:: Simulation
110 :members: add_plugin, set_plugin, get_plugin, run,
111 analyze, plot, run_all, analyze_all, _apply_all,
112 topdir, plugindir, check_file, has_plugin,
113 check_plugin_name, current_plugin
114 :show-inheritance:
115
116 .. autoclass:: Plugin
117 :members: worker_class, register
118
119 .. attribute:: Plugin.plugin_name
120
121 Name of the plugin; this must be a *unique* identifier across
122 all plugins of a :class:`Simulation` object. It should also be
123 human understandable and must be a valid python identifier as it
124 is used as a dict key.
125
126 .. attribute:: Plugin.simulation
127
128 The :class:`Simulation` instance who owns the plugin. Can be
129 ``None`` until a successful call to :meth:`~Plugin.register`.
130
131 .. attribute:: Plugin.worker
132
133 The :class:`Worker` instance of the plugin.
134
135
136 .. autoclass:: Worker
137 :members: topdir, plugindir, savefig, _register_hook
138 :show-inheritance:
139
140 """
141 __docformat__ = "restructuredtext en"
142
143 import sys
144 import os
145 import errno
146 import subprocess
147 import warnings
148
149 from gromacs.utilities import FileUtils, AttributeDict, asiterable
153 """Class that represents one simulation.
154
155 Analysis capabilities are added via plugins.
156
157 1. Set the *active plugin* with the :meth:`Simulation.set_plugin` method.
158 2. Analyze the trajectory with the active plugin by calling the
159 :meth:`Simulation.run` method.
160 3. Analyze the output from :meth:`run` with :meth:`Simulation.analyze`; results are stored
161 in the plugin's :attr:`~Worker.results` dictionary.
162 4. Plot results with :meth:`Simulation.plot`.
163 """
164
165
167 """Set up a Simulation object.
168
169 :Keywords:
170 *sim*
171 Any object that contains the attributes *tpr*, *xtc*,
172 and optionally *ndx*
173 (e.g. :class:`gromacs.cbook.Transformer`). The individual keywrods such
174 as *xtc* override the values in *sim*.
175 *tpr*
176 Gromacs tpr file (**required**)
177 *xtc*
178 Gromacs trajectory, can also be a trr (**required**)
179 *edr*
180 Gromacs energy file (only required for some plugins)
181 *ndx*
182 Gromacs index file
183 *analysisdir*
184 directory under which derived data are stored;
185 defaults to the directory containing the tpr [None]
186 *plugins* : list
187 plugin instances or tuples (*plugin class*, *kwarg dict*) or tuples
188 (*plugin_class_name*, *kwarg dict*) to be used; more can be
189 added later with :meth:`Simulation.add_plugin`.
190 """
191 sim = kwargs.pop('sim', None)
192 def getpop(attr, required=False):
193 """Return attribute from from kwargs or sim or None"""
194 val = kwargs.pop(attr, None)
195 if not val is None:
196 return val
197 try:
198 return sim.__getattribute__(attr)
199 except AttributeError:
200 if required:
201 raise TypeError("Required attribute %r not found in kwargs or sim" % attr)
202 return None
203
204
205 self.tpr = getpop('tpr', required=True)
206 self.xtc = getpop('xtc', required=True)
207
208 self.ndx = getpop('ndx')
209 self.edr = getpop('edr')
210
211
212 for v in ('tpr', 'xtc'):
213 self.check_file(v, self.__getattribute__(v))
214
215 self.analysis_dir = kwargs.pop('analysisdir', os.path.dirname(self.tpr))
216
217
218 self.plugins = AttributeDict()
219
220 self.default_plugin_name = None
221
222
223
224
225
226
227
228 plugins = kwargs.pop('plugins', [])
229
230 for x in plugins:
231 try:
232 P, kwargs = asiterable(x)
233 except ValueError:
234 P = x
235 kwargs = {}
236 self.add_plugin(P, **kwargs)
237
238
239 if len(self.plugins) == 1:
240 self.set_plugin(self.plugins.keys()[0])
241
242
243
244
245
246
247
248
250 """Add a plugin to the registry.
251
252 - If *plugin* is a :class:`Plugin` instance then the
253 instance is directly registered and any keyword arguments
254 are ignored.
255
256 - If *plugin* is a :class:`Plugin` class object or a
257 string that can be found in :mod:`gromacs.analysis.plugins`
258 then first an instance is created with the given keyword
259 arguments and then registered.
260
261 :Arguments:
262 *plugin* : class or string, or instance
263 If the parameter is a class then it should have been derived
264 from :class:`Plugin`. If it is a string then it is taken as a
265 plugin name in :mod:`gromacs.analysis.plugins` and the
266 corresponding class is added. In both cases any parameters for
267 initizlization should be provided.
268
269 If *plugin* is already a :class:`Plugin` instance then the kwargs
270 will be ignored.
271 *kwargs*
272 The kwargs are specific for the plugin and should be
273 described in its documentation.
274 """
275
276
277 try:
278 plugin.register(simulation=self)
279 except (TypeError, AttributeError):
280
281 if type(plugin) is str:
282 import plugins
283 plugin = plugins.__plugin_classes__[plugin]
284
285 plugin(simulation=self, **kwargs)
286
287
289 """Returns a path under self.analysis_dir, which is guaranteed to exist.
290
291 .. Note:: Parent dirs are created if necessary."""
292 p = os.path.join(self.analysis_dir, *args)
293 parent = os.path.dirname(p)
294 try:
295 os.makedirs(parent)
296 except OSError,err:
297 if err.errno != errno.EEXIST:
298 raise
299 return p
300
302 """Directory where the plugin creates and looks for files."""
303 return self.get_plugin(plugin_name).plugindir(*args)
304
305 - def figdir(self, plugin_name, *args):
306 """Directory where the plugin saves figures."""
307 return self.get_plugin(plugin_name).figdir(*args)
308
310 """Raise :exc:`ValueError` if path does not exist. Uses *filetype* in message."""
311 if path is None or not os.path.isfile(path):
312 raise ValueError("Missing required file %(filetype)r, got %(path)r." % vars())
313 return True
314
316 """Raises a exc:`ValueError` if *plugin_name* is not registered."""
317 if not (plugin_name is None or self.has_plugin(plugin_name)):
318 raise ValueError('plugin_name (%r) must be None or one of\n%r\n' % (plugin_name, self.plugins.keys()))
319
321 """Returns True if *plugin_name* is registered."""
322 return plugin_name in self.plugins
323
325 """Set the plugin that should be used by default.
326
327 If no *plugin_name* is supplied to :meth:`run`, :meth:`analyze` etc. then
328 this will be used.
329 """
330 if plugin_name == None:
331 self.default_plugin_name = None
332 else:
333 self.check_plugin_name(plugin_name)
334 self.default_plugin_name = plugin_name
335 return self.default_plugin_name
336
338 """Return valid plugin or the default for *plugin_name*=``None``."""
339 self.check_plugin_name(plugin_name)
340 if plugin_name is None:
341 if self.default_plugin_name is None:
342 raise ValueError('No default plugin was set.')
343 plugin_name = self.default_plugin_name
344 return self.plugins[plugin_name]
345
346 @property
348 """The currently active plugin (set with :meth:`Simulation.set_plugin`)."""
349 return self.get_plugin()
350
351 - def run(self,plugin_name=None,**kwargs):
352 """Generate data files as prerequisite to analysis."""
353 return self.get_plugin(plugin_name).run(**kwargs)
354
356 """Execute the run() method for all registered plugins."""
357 return self._apply_all(self.run, **kwargs)
358
359 - def analyze(self,plugin_name=None,**kwargs):
360 """Run analysis for the plugin."""
361 return self.get_plugin(plugin_name).analyze(**kwargs)
362
364 """Execute the analyze() method for all registered plugins."""
365 return self._apply_all(self.analyze, **kwargs)
366
367 - def plot(self,plugin_name=None,figure=False,**kwargs):
368 """Plot all data for the selected plugin::
369
370 plot(plugin_name, **kwargs)
371
372 :Arguments:
373 *plugin_name*
374 name of the plugin to plot data from
375 *figure*
376 - ``True``: plot to file with default name.
377 - string: use this filename (+extension for format)
378 - ``False``: only display
379 *kwargs*
380 arguments for plugin plot function (in many cases
381 provided by :meth:`gromacs.formats.XVG.plot` and
382 ultimately by :func:`pylab.plot`)
383 """
384 kwargs['figure'] = figure
385 return self.get_plugin(plugin_name).plot(**kwargs)
386
388 """Execute *func* for all plugins."""
389 results = {}
390 for plugin_name in self.plugins:
391 results[plugin_name] = func(plugin_name=plugin_name, **kwargs)
392 return results
393
395 return 'Simulation(tpr=%(tpr)r,xtc=%(xtc)r,analysisdir=%(analysis_dir)r)' % vars(self)
398
399
400
401
402
403
404
405
406 -class Worker(FileUtils):
407 """Base class for a plugin worker."""
408
410 """Set up Worker class.
411
412 :Keywords:
413 *plugin* : instance
414 The :class:`Plugin` instance that owns this worker. **Must be supplied.**
415 *simulation*
416 A :class:Simulation` object, required for registration,
417 but can be supplied later.
418 *kwargs*
419 All other keyword arguments are passed to the super class.
420 """
421
422 self.plugin = kwargs.pop('plugin', None)
423 """:class:`Plugin` instance that owns this Worker."""
424 assert not self.plugin is None
425 self.plugin_name = self.plugin.plugin_name
426 """Name of the plugin that this Worker belongs to."""
427
428 self.simulation = kwargs.pop('simulation',None)
429 self.location = self.plugin_name
430 self.results = AttributeDict()
431 self.parameters = AttributeDict()
432 self.parameters.filenames = AttributeDict()
433 super(Worker,self).__init__(**kwargs)
434
435
436
437
439 """Things to initialize once the :class:`Simulation` instance is known.
440
441 The hook is called from :meth:`Plugin.register`.
442
443 .. Note:: Subclasses should do all their :class:`Simulation` -
444 dependent initialization in their own :meth:`_register_hook` which
445 **must** call the super class hook via the :class:`super`
446 mechanism.
447 """
448
449 simulation = kwargs.pop('simulation', self.simulation)
450
451
452
453 if not simulation is None:
454 self.simulation = simulation
455
457 """Returns a directory located under the simulation top directory."""
458 return self.simulation.topdir(*args)
459
461 """Returns a directory located under the plugin top directory."""
462 return self.topdir(self.location, *args)
463
465 """Returns a directory under the plugin top directory to store figures in."""
466 return self.topdir('figs', *args)
467
468 - def run(self,**kwargs):
469 raise NotImplementedError
470
472 raise NotImplementedError
473
474 - def plot(self,**kwargs):
475 raise NotImplementedError
476
477 - def savefig(self, filename=None, ext='png'):
478 """Save the current figure under the default name or *filename*.
479
480 Uses the supplied format and extension *ext*.
481 """
482 import pylab
483 if filename is None:
484 filename = self.parameters.figname
485 _filename = self.filename(filename, ext=ext, use_my_ext=True)
486 pylab.savefig(_filename)
487 print "Saved figure as %(_filename)r." % vars()
488
490 """Store array *a* as :class:`~gromacs.formats.XVG` in result *name*.
491
492 kwargs are passed to :class:`gromacs.formats.XVG`.
493
494 This is a helper method that simplifies the task of storing
495 results in the form of a numpy array as a data file on disk in
496 the xmgrace format and also as a :class:`~gromacs.formats.XVG`
497 instance in the :attr:`gromacs.analysis.core.Worker.results`
498 dictionary.
499 """
500 from gromacs.formats import XVG
501 kwargs.pop('filename',None)
502 filename = self.plugindir(name+'.xvg')
503 xvg = XVG(**kwargs)
504 xvg.set(a)
505 xvg.write(filename)
506 self.results[name] = xvg
507 self.parameters.filenames[name] = filename
508 return filename
509
510
511
512
513
514 -class Plugin(object):
515 """Plugin class that can be added to a :class:`Simulation` instance.
516
517 All analysis plugins must be derived from this base class.
518
519 If a :class:`Simulation` instance is provided to the constructore in the
520 *simulation* keyword argument then the plugin instantiates and registers a
521 worker class in :attr:`Simulation.plugins` and adds the :class:`Simulation`
522 instance to the worker.
523
524 Otherwise the :meth:`Plugin.register` method must be called explicitly with
525 a :class:`Simulation` instance.
526
527 The plugin class handles the administrative tasks of interfacing with the
528 :class:`Simulation` class. The worker runs the analysis.
529
530 .. Note:: If multiple Plugin instances are added to a Simulation one *must*
531 set the *name* keyword argument to distinguish the
532 instances. Plugins are referred to by this name in all further
533 interactions with the user. There are no sanity checks:
534 A newer plugin with the same *name* simply replaces the
535 previous one.
536 """
537
538 worker_class = None
539
540 - def __init__(self,name=None,simulation=None,**kwargs):
541 """Registers the plugin with the simulation class.
542
543 Specific keyword arguments are listed below, all other kwargs
544 are passed through.
545
546 :Arguments:
547 *name* : string
548 Name of the plugin. Should differ for different
549 instances. Defaults to the class name.
550 *simulation* : Simulation instance
551 The :class:`Simulation` instance that owns this plugin instance. Can be
552 ``None`` but then the :meth:`register` method has to be called manually
553 with a simulation instance later.
554 *kwargs*
555 All other keyword arguments are passed to the Worker.
556 """
557 if name is None:
558 name = self.__class__.__name__
559 self.plugin_name = name
560 """Name of the plugin; this must be a **unique** identifier across all
561 plugins of a :class:`Simulation` object. It should also be human
562 understandable and must be a valid python identifier as it is used
563 as a dict key."""
564
565
566 print "Initializing plugin %r" % self.plugin_name
567
568 assert issubclass(self.worker_class, Worker)
569
570 self.__is_registered = False
571
572 kwargs['simulation'] = simulation
573 kwargs['plugin'] = self
574
575 self.worker = self.worker_class(**kwargs)
576
577
578
579 self.simulation = simulation
580
581 if not simulation is None:
582 self.register(simulation)
583
584
586 """Register the plugin with the :class:`Simulation` instance.
587
588 This method also ensures that the worker class knows the simulation
589 instance. This is typically required for its :meth:`~Worker.run`,
590 :meth:`~Worker.analyze`, and :meth:`~Worker.plot` methods.
591 """
592
593 assert simulation != None
594 assert self.__is_registered == False
595
596 self.simulation = simulation
597 self.worker._register_hook(simulation=simulation)
598 simulation.plugins[self.plugin_name] = self.worker
599
600
601
602 self.worker.__doc__ = self.__doc__
603
604 self.__is_registered = True
605