Package gromacs :: Package analysis :: Module core
[hide private]
[frames] | no frames]

Source Code for Module gromacs.analysis.core

  1  # $Id$ 
  2  # Copyright (c) 2009 Oliver Beckstein <orbeckst@gmail.com> 
  3  # Released under the GNU Public License 3 (or higher, your choice) 
  4  # See the file COPYING for details. 
  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 
150 151 152 -class Simulation(object):
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 # NOTE: not suitable for multiple inheritance 165
166 - def __init__(self, **kwargs):
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) # must pop from kwargs to clean it 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 # required files 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 # check existence of required files 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 #: Registry for plugins: This dict is central. 218 self.plugins = AttributeDict() 219 #: Use this plugin if none is explicitly specified. Typically set with :meth:`~Simulation.set_plugin`. 220 self.default_plugin_name = None 221 222 # XXX: Or should we simply add instances and then re-register 223 # all instances using register() ? 224 # XXX: ... this API should be cleaned up. It seems to be connected 225 # back and forth in vicious circles. -- OB 2009-07-10 226 227 228 plugins = kwargs.pop('plugins', []) 229 # list of tuples (plugin, kwargs) or just (plugin,) if no kwords required (eg if plugin is an instance) 230 for x in plugins: 231 try: 232 P, kwargs = asiterable(x) # make sure to wrap strings, especially 2-letter ones! 233 except ValueError: 234 P = x 235 kwargs = {} 236 self.add_plugin(P, **kwargs) 237 238 # convenience: if only a single plugin was registered we default to that one 239 if len(self.plugins) == 1: 240 self.set_plugin(self.plugins.keys()[0])
241 242 # Is this needed? If done properly, kwargs should be empty by now BUT 243 # because the same list is re-used for all plugins I cannot pop them in 244 # the plugins. I don't think multiple inheritance would work with this 245 # setup so let's not pretend it does: hence comment out the super-init 246 # call: 247 ## super(Simulation, self).__init__(**kwargs) 248
249 - def add_plugin(self, plugin, **kwargs):
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 # simulation=self must be provided so that plugin knows who owns it 276 277 try: 278 plugin.register(simulation=self) 279 except (TypeError, AttributeError): 280 # NOTE: this except clause can mask bugs in the plugin code!! 281 if type(plugin) is str: 282 import plugins # We should be able to import this safely now... 283 plugin = plugins.__plugin_classes__[plugin] 284 # plugin registers itself in self.plugins 285 plugin(simulation=self, **kwargs) # simulation=self is REQUIRED!
286 287
288 - def topdir(self,*args):
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
301 - def plugindir(self, plugin_name, *args):
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
309 - def check_file(self,filetype,path):
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
315 - def check_plugin_name(self,plugin_name):
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
320 - def has_plugin(self,plugin_name):
321 """Returns True if *plugin_name* is registered.""" 322 return plugin_name in self.plugins
323
324 - def set_plugin(self,plugin_name):
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
337 - def get_plugin(self,plugin_name=None):
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
347 - def current_plugin(self):
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
355 - def run_all(self,**kwargs):
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
363 - def analyze_all(self,**kwargs):
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
387 - def _apply_all(self, func, **kwargs):
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
394 - def __str__(self):
395 return 'Simulation(tpr=%(tpr)r,xtc=%(xtc)r,analysisdir=%(analysis_dir)r)' % vars(self)
396 - def __repr__(self):
397 return str(self)
398
399 400 401 # Plugin infrastructure 402 # --------------------- 403 404 # worker classes (used by the plugins) 405 406 -class Worker(FileUtils):
407 """Base class for a plugin worker.""" 408
409 - def __init__(self,**kwargs):
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 # must be supplied, non-opt kw arg 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) # eventually needed but can come after init 429 self.location = self.plugin_name # directory name under analysisdir 430 self.results = AttributeDict() # store results 431 self.parameters = AttributeDict() # container for options, filenames, etc... 432 self.parameters.filenames = AttributeDict() 433 super(Worker,self).__init__(**kwargs)
434 435 # note: We are NOT calling self._register_hook() here; subclasses do this 436 # themselves and it *must* cascade via super(cls, self)._register_hook(). 437
438 - def _register_hook(self, **kwargs):
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 # XXX: should we 451 # XXX: 'try: super(Worker, self)._register_hook(**kwargs) except AttributeError: pass' 452 # XXX: just in case? 453 if not simulation is None: 454 self.simulation = simulation
455
456 - def topdir(self, *args):
457 """Returns a directory located under the simulation top directory.""" 458 return self.simulation.topdir(*args)
459
460 - def plugindir(self, *args):
461 """Returns a directory located under the plugin top directory.""" 462 return self.topdir(self.location, *args)
463
464 - def figdir(self, *args):
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
471 - def analyze(self,**kwargs):
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
489 - def store_xvg(self, name, a, **kwargs):
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) # ignore filename 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 # plugins: 512 # registers a worker class in Simulation.plugins and adds a pointer to Simulation to worker 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 #: actual plugin :class:`gromacs.analysis.core.Worker` class (name with leading underscore) 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) # must be a Worker 569 570 self.__is_registered = False # flag so that we only register once; maybe not needed? 571 572 kwargs['simulation'] = simulation # allows access of plugin to globals 573 kwargs['plugin'] = self # tell Worker who owns it 574 #: The :class:`Worker` instance of the plugin. 575 self.worker = self.worker_class(**kwargs) # create Worker instance 576 577 #: The :class:`Simulation` instance who owns the plugin. Can be ``None`` 578 #: until a successful call to :meth:`~Plugin.register`. 579 self.simulation = simulation 580 581 if not simulation is None: # can delay registration 582 self.register(simulation)
583 584
585 - def register(self, simulation):
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 # must know who we belong to 594 assert self.__is_registered == False # only register once (necessary?) 595 596 self.simulation = simulation # update our own 597 self.worker._register_hook(simulation=simulation) # HACK!!! patch simulation into worker & do more 598 simulation.plugins[self.plugin_name] = self.worker # add the worker to simulation 599 600 # improve help by changing the worker class doc to the plugin 601 # one: the user mostly sees the worker via simulation.plugins 602 self.worker.__doc__ = self.__doc__ 603 604 self.__is_registered = True
605