Added command stack saving and loading.
[hooke.git] / hooke / curve.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """The `curve` module provides :class:`Curve` and :class:`Data` for
20 storing force curves.
21 """
22
23 import logging
24 import os.path
25
26 import numpy
27
28 from .command_stack import CommandStack
29
30
31 class NotRecognized (ValueError):
32     def __init__(self, curve):
33         self.__setstate__(curve)
34
35     def __getstate__(self):
36         return self.curve
37
38     def __setstate__(self, data):
39         if isinstance(data, Curve):
40             msg = 'Not a recognizable curve format: %s' % data.path
41             super(NotRecognized, self).__init__(msg)
42             self.curve = data
43
44 class Data (numpy.ndarray):
45     """Stores a single, continuous data set.
46
47     Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
48
49     See :mod:`numpy.doc.subclassing` for the peculiarities of
50     subclassing :class:`numpy.ndarray`.
51
52     Examples
53     --------
54
55     >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
56     >>> type(d)
57     <class 'hooke.curve.Data'>
58     >>> for i in range(3): # initialize d
59     ...    for j in range(2):
60     ...        d[i,j] = i*10 + j
61     >>> d
62     Data([[  0.,   1.],
63            [ 10.,  11.],
64            [ 20.,  21.]])
65     >>> d.info
66     {'columns': ['distance (m)', 'force (N)']}
67
68     The information gets passed on to slices.
69
70     >>> row_a = d[:,0]
71     >>> row_a
72     Data([  0.,  10.,  20.])
73     >>> row_a.info
74     {'columns': ['distance (m)', 'force (N)']}
75
76     The data-type is also pickleable, to ensure we can move it between
77     processes with :class:`multiprocessing.Queue`\s.
78
79     >>> import pickle
80     >>> s = pickle.dumps(d)
81     >>> z = pickle.loads(s)
82     >>> z
83     Data([[  0.,   1.],
84            [ 10.,  11.],
85            [ 20.,  21.]])
86     >>> z.info
87     {'columns': ['distance (m)', 'force (N)']}
88     """
89     def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
90                 strides=None, order=None, info=None):
91         """Create the ndarray instance of our type, given the usual
92         input arguments.  This will call the standard ndarray
93         constructor, but return an object of our type.
94         """
95         obj = numpy.ndarray.__new__(
96             subtype, shape, dtype, buffer, offset, strides, order)
97         # add the new attribute to the created instance
98         if info == None:
99             info = {}
100         obj.info = info
101         # Finally, we must return the newly created object:
102         return obj
103
104     def __array_finalize__(self, obj):
105         """Set any extra attributes from the original object when
106         creating a new view object."""
107         # reset the attribute from passed original object
108         self.info = getattr(obj, 'info', {})
109         # We do not need to return anything
110
111     def __reduce__(self):
112         """Collapse an instance for pickling.
113
114         Returns
115         -------
116         reconstruct : callable
117             Called to create the initial version of the object.
118         args : tuple
119             A tuple of arguments for `reconstruct`
120         state : (optional)
121             The state to be passed to __setstate__, if present.
122         iter : iterator (optional)
123             Yielded items will be appended to the reconstructed
124             object.
125         dict : iterator (optional)
126             Yielded (key,value) tuples pushed back onto the
127             reconstructed object.
128         """
129         base_reduce = list(numpy.ndarray.__reduce__(self))
130         # tack our stuff onto ndarray's setstate portion.
131         base_reduce[2] = (base_reduce[2], (self.info,))
132         return tuple(base_reduce)
133
134     def __setstate__(self, state):
135         base_class_state,own_state = state
136         numpy.ndarray.__setstate__(self, base_class_state)
137         self.info, = own_state
138
139
140 class Curve (object):
141     """A grouped set of :class:`Data` runs from the same file with metadata.
142
143     For an approach/retract force spectroscopy experiment, the group
144     would consist of the approach data and the retract data.  Metadata
145     would be the temperature, cantilever spring constant, etc.
146
147     Two important :attr:`info` settings are `filetype` and
148     `experiment`.  These are two strings that can be used by Hooke
149     commands/plugins to understand what they are looking at.
150
151     * :attr:`info['filetype']` should contain the name of the exact
152       filetype defined by the driver (so that filetype-speciofic
153       commands can know if they're dealing with the correct filetype).
154     * :attr:`info['experiment']` should contain an instance of a
155       :class:`hooke.experiment.Experiment` subclass to identify the
156       experiment type.  For example, various
157       :class:`hooke.driver.Driver`\s can read in force-clamp data, but
158       Hooke commands could like to know if they're looking at force
159       clamp data, regardless of their origin.
160
161     Another important attribute is :attr:`command_stack`, which holds
162     a :class:`~hooke.command_stack.CommandStack` listing the commands
163     that have been applied to the `Curve` since loading.
164     """
165     def __init__(self, path, info=None):
166         #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
167         self.path = path
168         self.driver = None
169         self.data = None
170         if info == None:
171             info = {}
172         self.info = info
173         self.name = os.path.basename(path)
174         self.command_stack = CommandStack()
175         self._hooke = None  # Hooke instance for Curve.load()
176
177     def __str__(self):
178         return str(self.__unicode__())
179
180     def __unicode__(self):
181         return u'<%s %s>' % (self.__class__.__name__, self.name)
182
183     def __repr__(self):
184         return self.__str__()
185
186     def set_hooke(self, hooke=None):
187         if hooke != None:
188             self._hooke = hooke
189
190     def identify(self, drivers):
191         """Identify the appropriate :class:`hooke.driver.Driver` for
192         the curve file (`.path`).
193         """
194         if 'filetype' in self.info:
195             driver = [d for d in drivers if d.name == self.info['filetype']]
196             if len(driver) == 1:
197                 driver = driver[0]
198                 if driver.is_me(self.path):
199                     self.driver = driver
200                     return
201         for driver in drivers:
202             if driver.is_me(self.path):
203                 self.driver = driver # remember the working driver
204                 return
205         raise NotRecognized(self)
206
207     def load(self, hooke=None):
208         """Use the driver to read the curve into memory.
209
210         Also runs any commands in :attr:`command_stack`.  All
211         arguments are passed through to
212         :meth:`hooke.command_stack.CommandStack.execute`.
213         """
214         self.set_hooke(hooke)
215         log = logging.getLogger('hooke')
216         log.debug('loading curve %s with driver %s' % (self.name, self.driver))
217         data,info = self.driver.read(self.path, self.info)
218         self.data = data
219         for key,value in info.items():
220             self.info[key] = value
221         if self._hooke != None:
222             self.command_stack.execute(self._hooke)
223         elif len(self.command_stack) > 0:
224             log.warn(
225                 'could not execute command stack for %s without Hooke instance'
226                 % self.name)
227
228     def unload(self):
229         """Release memory intensive :attr:`.data`.
230         """
231         log = logging.getLogger('hooke')
232         log.debug('unloading curve %s' % self.name)
233         self.data = None