Serialize hooke.experiment.Experiment instances in Curves.
[hooke.git] / hooke / curve.py
index db313182c7f95b0f138fc351cd232b8f39829bc2..af572f3cfd1a6794657dc9aca9272e9bbd094e95 100644 (file)
@@ -2,15 +2,15 @@
 #
 # This file is part of Hooke.
 #
-# Hooke is free software: you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation, either
-# version 3 of the License, or (at your option) any later version.
+# Hooke is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
 #
-# Hooke is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Lesser General Public License for more details.
+# Hooke is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
+# Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with Hooke.  If not, see
 storing force curves.
 """
 
+import logging
 import os.path
+
 import numpy
 
+from .command_stack import CommandStack
+from . import experiment
+
 
 class NotRecognized (ValueError):
     def __init__(self, curve):
@@ -37,6 +42,7 @@ class NotRecognized (ValueError):
             super(NotRecognized, self).__init__(msg)
             self.curve = data
 
+
 class Data (numpy.ndarray):
     """Stores a single, continuous data set.
 
@@ -144,25 +150,83 @@ class Curve (object):
     `experiment`.  These are two strings that can be used by Hooke
     commands/plugins to understand what they are looking at.
 
-    * `.info['filetype']` should contain the name of the exact
+    * :attr:`info['filetype']` should contain the name of the exact
       filetype defined by the driver (so that filetype-speciofic
       commands can know if they're dealing with the correct filetype).
-    * `.info['experiment']` should contain an instance of a
+    * :attr:`info['experiment']` should contain an instance of a
       :class:`hooke.experiment.Experiment` subclass to identify the
       experiment type.  For example, various
       :class:`hooke.driver.Driver`\s can read in force-clamp data, but
       Hooke commands could like to know if they're looking at force
       clamp data, regardless of their origin.
+
+    Another important attribute is :attr:`command_stack`, which holds
+    a :class:`~hooke.command_stack.CommandStack` listing the commands
+    that have been applied to the `Curve` since loading.
     """
     def __init__(self, path, info=None):
         #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
-        self.path = path
+        self.name = None
+        self.set_path(path)
         self.driver = None
         self.data = None
         if info == None:
             info = {}
         self.info = info
-        self.name = os.path.basename(path)
+        self.command_stack = CommandStack()
+        self._hooke = None  # Hooke instance for Curve.load()
+
+    def __str__(self):
+        return str(self.__unicode__())
+
+    def __unicode__(self):
+        return u'<%s %s>' % (self.__class__.__name__, self.name)
+
+    def __repr__(self):
+        return self.__str__()
+
+    def set_path(self, path):
+        self.path = path
+        if self.name == None and path != None:
+            self.name = os.path.basename(path)
+
+    def __getstate__(self):
+        state = dict(self.__dict__)
+        del(state['_hooke'])
+        dc = state['command_stack']
+        if hasattr(dc, '__getstate__'):
+            state['command_stack'] = dc.__getstate__()
+        if self.info.get('experiment', None) != None:
+            e = self.info['experiment']
+            assert isinstance(e, experiment.Experiment)
+            # HACK? require Experiment classes to be defined in the
+            # experiment module.
+            self.info['experiment'] = e.__class__.__name__
+        return state
+
+    def __setstate__(self, state):
+        self.name = self._hooke = None
+        for key,value in state.items():
+            if key == 'path':
+                self.set_path(value)
+                continue
+            elif key == 'info':
+                if 'experiment' not in value:
+                    value['experiment'] = None
+                else:
+                    # HACK? require Experiment classes to be defined in the
+                    # experiment module.
+                    cls = getattr(experiment, value['experiment'])
+                    value['experiment'] = cls()
+            elif key == 'command_stack':
+                v = CommandStack()
+                v.__setstate__(value)
+                value = v
+            setattr(self, key, value)
+
+    def set_hooke(self, hooke=None):
+        if hooke != None:
+            self._hooke = hooke
 
     def identify(self, drivers):
         """Identify the appropriate :class:`hooke.driver.Driver` for
@@ -181,15 +245,30 @@ class Curve (object):
                 return
         raise NotRecognized(self)
 
-    def load(self):
+    def load(self, hooke=None):
         """Use the driver to read the curve into memory.
+
+        Also runs any commands in :attr:`command_stack`.  All
+        arguments are passed through to
+        :meth:`hooke.command_stack.CommandStack.execute`.
         """
+        self.set_hooke(hooke)
+        log = logging.getLogger('hooke')
+        log.debug('loading curve %s with driver %s' % (self.name, self.driver))
         data,info = self.driver.read(self.path, self.info)
         self.data = data
         for key,value in info.items():
             self.info[key] = value
+        if self._hooke != None:
+            self.command_stack.execute(self._hooke)
+        elif len(self.command_stack) > 0:
+            log.warn(
+                'could not execute command stack for %s without Hooke instance'
+                % self.name)
 
     def unload(self):
         """Release memory intensive :attr:`.data`.
         """
+        log = logging.getLogger('hooke')
+        log.debug('unloading curve %s' % self.name)
         self.data = None