test/data/vclamp_jpk/README: Document sample versions
[hooke.git] / hooke / curve.py
index af572f3cfd1a6794657dc9aca9272e9bbd094e95..53670151685a92bbc16a373e5b396854ae0e7fc3 100644 (file)
@@ -1,32 +1,32 @@
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2010-2012 W. Trevor King <wking@tremily.us>
 #
 # This file is part of Hooke.
 #
 #
 # 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
-# <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU Lesser General Public License
+# along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
 
 """The `curve` module provides :class:`Curve` and :class:`Data` for
 storing force curves.
 """
 
 
 """The `curve` module provides :class:`Curve` and :class:`Data` for
 storing force curves.
 """
 
+from copy_reg import dispatch_table
+from copy import _reconstruct, _copy_dispatch
 import logging
 import os.path
 
 import numpy
 
 from .command_stack import CommandStack
 import logging
 import os.path
 
 import numpy
 
 from .command_stack import CommandStack
-from . import experiment
 
 
 class NotRecognized (ValueError):
 
 
 class NotRecognized (ValueError):
@@ -87,6 +87,18 @@ class Data (numpy.ndarray):
            [ 20.,  21.]])
     >>> z.info
     {'columns': ['distance (m)', 'force (N)']}
            [ 20.,  21.]])
     >>> z.info
     {'columns': ['distance (m)', 'force (N)']}
+
+    The data-type is also YAMLable (see :mod:`hooke.util.yaml`).
+
+    >>> import yaml
+    >>> s = yaml.dump(d)
+    >>> print(s)
+    !hooke.curve.DataInfo
+    columns: [distance (m), force (N)]
+    <BLANKLINE>
+    >>> z = yaml.load(s)
+    >>> z
+    Data([], shape=(0, 0), dtype=float32)
     """
     def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
                 strides=None, order=None, info=None):
     """
     def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
                 strides=None, order=None, info=None):
@@ -146,35 +158,77 @@ class Curve (object):
     would consist of the approach data and the retract data.  Metadata
     would be the temperature, cantilever spring constant, etc.
 
     would consist of the approach data and the retract data.  Metadata
     would be the temperature, cantilever spring constant, etc.
 
-    Two important :attr:`info` settings are `filetype` and
-    `experiment`.  These are two strings that can be used by Hooke
-    commands/plugins to understand what they are looking at.
-
-    * :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).
-    * :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.
+    Each :class:`Data` block in :attr:`data` must contain an
+    :attr:`info['name']` setting with a unique (for the parent
+    curve) name identifying the data block.  This allows plugins 
+    and commands to access individual blocks.
+
+    Each curve maintiains a :class:`~hooke.command_stack.CommandStack`
+    (:attr:`command_stack`) listing the commands that have been
+    applied to the `Curve` since loading.
+
+    The data-type is pickleable, to ensure we can move it between
+    processes with :class:`multiprocessing.Queue`\s.
+
+    >>> import pickle
+    >>> import yaml
+    >>> from .engine import CommandMessage
+    >>> c = Curve(path='some/path')
+
+    We add a recursive reference to `c` as you would get from
+    :meth:`hooke.plugin.curve.CurveCommand._add_to_command_stack`.
+
+    >>> c.command_stack.append(CommandMessage('curve info', {'curve':c}))
+
+    >>> s = pickle.dumps(c)
+    >>> z = pickle.loads(s)
+    >>> z
+    <Curve path>
+    >>> z.command_stack
+    [<CommandMessage curve info {curve: <Curve path>}>]
+    >>> z.command_stack[-1].arguments['curve'] == z
+    True
+    >>> print(yaml.dump(c))  # doctest: +REPORT_UDIFF
+    &id001 !!python/object:hooke.curve.Curve
+    command_stack: !!python/object/new:hooke.command_stack.CommandStack
+      listitems:
+      - !!python/object:hooke.engine.CommandMessage
+        arguments:
+          curve: *id001
+        command: curve info
+        explicit_user_call: true
+    name: path
+    path: some/path
+    <BLANKLINE>
+
+    However, if we try and serialize the command stack first, we run
+    into `Python issue 1062277`_.
+
+    .. _Python issue 1062277: http://bugs.python.org/issue1062277
+
+    >>> pickle.dumps(c.command_stack)
+    Traceback (most recent call last):
+      ...
+        assert id(obj) not in self.memo
+    AssertionError
+
+    YAML still works, though.
+
+    >>> print(yaml.dump(c.command_stack))  # doctest: +REPORT_UDIFF
+    &id001 !!python/object/new:hooke.command_stack.CommandStack
+    listitems:
+    - !!python/object:hooke.engine.CommandMessage
+      arguments:
+        curve: !!python/object:hooke.curve.Curve
+          command_stack: *id001
+          name: path
+          path: some/path
+      command: curve info
+      explicit_user_call: true
+    <BLANKLINE>
     """
     def __init__(self, path, info=None):
     """
     def __init__(self, path, info=None):
-        #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
-        self.name = None
-        self.set_path(path)
-        self.driver = None
-        self.data = None
-        if info == None:
-            info = {}
-        self.info = info
-        self.command_stack = CommandStack()
-        self._hooke = None  # Hooke instance for Curve.load()
+        self.__setstate__({'path':path, 'info':info})
 
     def __str__(self):
         return str(self.__unicode__())
 
     def __str__(self):
         return str(self.__unicode__())
@@ -186,43 +240,136 @@ class Curve (object):
         return self.__str__()
 
     def set_path(self, path):
         return self.__str__()
 
     def set_path(self, path):
+        if path != None:
+            path = os.path.expanduser(path)
         self.path = path
         if self.name == None and path != None:
             self.name = os.path.basename(path)
 
         self.path = path
         if self.name == None and path != None:
             self.name = os.path.basename(path)
 
+    def _setup_default_attrs(self):
+        # .data contains: {name of data: list of data sets [{[x], [y]}]
+        # ._hooke contains a Hooke instance for Curve.load()
+        self._default_attrs = {
+            '_hooke': None,
+            'command_stack': [],
+            'data': None,
+            'driver': None,
+            'info': {},
+            'name': None,
+            'path': None,
+            }
+
     def __getstate__(self):
     def __getstate__(self):
-        state = dict(self.__dict__)
+        state = dict(self.__dict__)  # make a copy of the attribute dict.
         del(state['_hooke'])
         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):
         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)
+        self._setup_default_attrs()
+        self.__dict__.update(self._default_attrs)
+        if state == True:
+            return
+        self.__dict__.update(state)
+        self.set_path(getattr(self, 'path', None))
+        if self.info in [None, {}]:
+            self.info = {}
+        if type(self.command_stack) == list:
+            self.command_stack = CommandStack()
+
+    def __copy__(self):
+        """Set copy to preserve :attr:`_hooke`.
+
+        :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
+        :mod:`yaml` output, but it should be preserved (but not
+        duplicated) during copies.
+
+        >>> import copy
+        >>> class Hooke (object):
+        ...     pass
+        >>> h = Hooke()
+        >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
+        >>> for i in range(3): # initialize d
+        ...    for j in range(2):
+        ...        d[i,j] = i*10 + j
+        >>> c = Curve(None)
+        >>> c.data = [d]
+        >>> c.set_hooke(h)
+        >>> c._hooke  # doctest: +ELLIPSIS
+        <hooke.curve.Hooke object at 0x...>
+        >>> c._hooke == h
+        True
+        >>> c2 = copy.copy(c)
+        >>> c2._hooke  # doctest: +ELLIPSIS
+        <hooke.curve.Hooke object at 0x...>
+        >>> c2._hooke == h
+        True
+        >>> c2.data
+        [Data([[  0.,   1.],
+               [ 10.,  11.],
+               [ 20.,  21.]])]
+        >>> d.info
+        {'columns': ['distance (m)', 'force (N)']}
+        >>> id(c2.data[0]) == id(d)
+        True
+        """
+        copier = _copy_dispatch.get(type(self))
+        if copier:
+            return copier(self)
+        reductor = dispatch_table.get(type(self))
+        if reductor:
+            rv = reductor(self)
+        else:
+            # :class:`object` implements __reduce_ex__, see :pep:`307`.
+            rv = self.__reduce_ex__(2)
+        y = _reconstruct(self, rv, 0)
+        y.set_hooke(self._hooke)
+        return y
+
+    def __deepcopy__(self, memo):
+        """Set deepcopy to preserve :attr:`_hooke`.
+
+        :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
+        :mod:`yaml` output, but it should be preserved (but not
+        duplicated) during copies.
+
+        >>> import copy
+        >>> class Hooke (object):
+        ...     pass
+        >>> h = Hooke()
+        >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
+        >>> for i in range(3): # initialize d
+        ...    for j in range(2):
+        ...        d[i,j] = i*10 + j
+        >>> c = Curve(None)
+        >>> c.data = [d]
+        >>> c.set_hooke(h)
+        >>> c._hooke  # doctest: +ELLIPSIS
+        <hooke.curve.Hooke object at 0x...>
+        >>> c._hooke == h
+        True
+        >>> c2 = copy.deepcopy(c)
+        >>> c2._hooke  # doctest: +ELLIPSIS
+        <hooke.curve.Hooke object at 0x...>
+        >>> c2._hooke == h
+        True
+        >>> c2.data
+        [Data([[  0.,   1.],
+               [ 10.,  11.],
+               [ 20.,  21.]])]
+        >>> d.info
+        {'columns': ['distance (m)', 'force (N)']}
+        >>> id(c2.data[0]) == id(d)
+        False
+        """
+        reductor = dispatch_table.get(type(self))
+        if reductor:
+            rv = reductor(self)
+        else:
+            # :class:`object` implements __reduce_ex__, see :pep:`307`.
+            rv = self.__reduce_ex__(2)
+        y = _reconstruct(self, rv, 1, memo)
+        y.set_hooke(self._hooke)
+        return y
 
     def set_hooke(self, hooke=None):
         if hooke != None:
 
     def set_hooke(self, hooke=None):
         if hooke != None: