75fbf75ce2f394bb9d03dcf39e29ca69bf0419ef
[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 from . import experiment
30
31
32 class NotRecognized (ValueError):
33     def __init__(self, curve):
34         self.__setstate__(curve)
35
36     def __getstate__(self):
37         return self.curve
38
39     def __setstate__(self, data):
40         if isinstance(data, Curve):
41             msg = 'Not a recognizable curve format: %s' % data.path
42             super(NotRecognized, self).__init__(msg)
43             self.curve = data
44
45
46 class Data (numpy.ndarray):
47     """Stores a single, continuous data set.
48
49     Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
50
51     See :mod:`numpy.doc.subclassing` for the peculiarities of
52     subclassing :class:`numpy.ndarray`.
53
54     Examples
55     --------
56
57     >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
58     >>> type(d)
59     <class 'hooke.curve.Data'>
60     >>> for i in range(3): # initialize d
61     ...    for j in range(2):
62     ...        d[i,j] = i*10 + j
63     >>> d
64     Data([[  0.,   1.],
65            [ 10.,  11.],
66            [ 20.,  21.]])
67     >>> d.info
68     {'columns': ['distance (m)', 'force (N)']}
69
70     The information gets passed on to slices.
71
72     >>> row_a = d[:,0]
73     >>> row_a
74     Data([  0.,  10.,  20.])
75     >>> row_a.info
76     {'columns': ['distance (m)', 'force (N)']}
77
78     The data-type is also pickleable, to ensure we can move it between
79     processes with :class:`multiprocessing.Queue`\s.
80
81     >>> import pickle
82     >>> s = pickle.dumps(d)
83     >>> z = pickle.loads(s)
84     >>> z
85     Data([[  0.,   1.],
86            [ 10.,  11.],
87            [ 20.,  21.]])
88     >>> z.info
89     {'columns': ['distance (m)', 'force (N)']}
90     """
91     def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
92                 strides=None, order=None, info=None):
93         """Create the ndarray instance of our type, given the usual
94         input arguments.  This will call the standard ndarray
95         constructor, but return an object of our type.
96         """
97         obj = numpy.ndarray.__new__(
98             subtype, shape, dtype, buffer, offset, strides, order)
99         # add the new attribute to the created instance
100         if info == None:
101             info = {}
102         obj.info = info
103         # Finally, we must return the newly created object:
104         return obj
105
106     def __array_finalize__(self, obj):
107         """Set any extra attributes from the original object when
108         creating a new view object."""
109         # reset the attribute from passed original object
110         self.info = getattr(obj, 'info', {})
111         # We do not need to return anything
112
113     def __reduce__(self):
114         """Collapse an instance for pickling.
115
116         Returns
117         -------
118         reconstruct : callable
119             Called to create the initial version of the object.
120         args : tuple
121             A tuple of arguments for `reconstruct`
122         state : (optional)
123             The state to be passed to __setstate__, if present.
124         iter : iterator (optional)
125             Yielded items will be appended to the reconstructed
126             object.
127         dict : iterator (optional)
128             Yielded (key,value) tuples pushed back onto the
129             reconstructed object.
130         """
131         base_reduce = list(numpy.ndarray.__reduce__(self))
132         # tack our stuff onto ndarray's setstate portion.
133         base_reduce[2] = (base_reduce[2], (self.info,))
134         return tuple(base_reduce)
135
136     def __setstate__(self, state):
137         base_class_state,own_state = state
138         numpy.ndarray.__setstate__(self, base_class_state)
139         self.info, = own_state
140
141
142 class Curve (object):
143     """A grouped set of :class:`Data` runs from the same file with metadata.
144
145     For an approach/retract force spectroscopy experiment, the group
146     would consist of the approach data and the retract data.  Metadata
147     would be the temperature, cantilever spring constant, etc.
148
149     Two important :attr:`info` settings are `filetype` and
150     `experiment`.  These are two strings that can be used by Hooke
151     commands/plugins to understand what they are looking at.
152
153     * :attr:`info['filetype']` should contain the name of the exact
154       filetype defined by the driver (so that filetype-speciofic
155       commands can know if they're dealing with the correct filetype).
156     * :attr:`info['experiment']` should contain an instance of a
157       :class:`hooke.experiment.Experiment` subclass to identify the
158       experiment type.  For example, various
159       :class:`hooke.driver.Driver`\s can read in force-clamp data, but
160       Hooke commands could like to know if they're looking at force
161       clamp data, regardless of their origin.
162
163     Another important attribute is :attr:`command_stack`, which holds
164     a :class:`~hooke.command_stack.CommandStack` listing the commands
165     that have been applied to the `Curve` since loading.
166     """
167     def __init__(self, path, info=None):
168         #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
169         self.name = None
170         self.set_path(path)
171         self.driver = None
172         self.data = None
173         if info == None:
174             info = {}
175         self.info = info
176         self.command_stack = CommandStack()
177         self._hooke = None  # Hooke instance for Curve.load()
178
179     def __str__(self):
180         return str(self.__unicode__())
181
182     def __unicode__(self):
183         return u'<%s %s>' % (self.__class__.__name__, self.name)
184
185     def __repr__(self):
186         return self.__str__()
187
188     def set_path(self, path):
189         self.path = path
190         if self.name == None and path != None:
191             self.name = os.path.basename(path)
192
193     def __getstate__(self):
194         state = dict(self.__dict__)      # make a copy of the attribute dict.
195         state['info'] = dict(self.info)  # make a copy of the info dict too.
196         del(state['_hooke'])
197         dc = state['command_stack']
198         if hasattr(dc, '__getstate__'):
199             state['command_stack'] = dc.__getstate__()
200         if state['info'].get('experiment', None) != None:
201             e = state['info']['experiment']
202             assert isinstance(e, experiment.Experiment), type(e)
203             # HACK? require Experiment classes to be defined in the
204             # experiment module.
205             state['info']['experiment'] = e.__class__.__name__
206         return state
207
208     def __setstate__(self, state):
209         self.name = self._hooke = None
210         for key,value in state.items():
211             if key == 'path':
212                 self.set_path(value)
213                 continue
214             elif key == 'info':
215                 if 'experiment' not in value:
216                     value['experiment'] = None
217                 elif value['experiment'] != None:
218                     # HACK? require Experiment classes to be defined in the
219                     # experiment module.
220                     cls = getattr(experiment, value['experiment'])
221                     value['experiment'] = cls()
222             elif key == 'command_stack':
223                 v = CommandStack()
224                 v.__setstate__(value)
225                 value = v
226             setattr(self, key, value)
227
228     def set_hooke(self, hooke=None):
229         if hooke != None:
230             self._hooke = hooke
231
232     def identify(self, drivers):
233         """Identify the appropriate :class:`hooke.driver.Driver` for
234         the curve file (`.path`).
235         """
236         if 'filetype' in self.info:
237             driver = [d for d in drivers if d.name == self.info['filetype']]
238             if len(driver) == 1:
239                 driver = driver[0]
240                 if driver.is_me(self.path):
241                     self.driver = driver
242                     return
243         for driver in drivers:
244             if driver.is_me(self.path):
245                 self.driver = driver # remember the working driver
246                 return
247         raise NotRecognized(self)
248
249     def load(self, hooke=None):
250         """Use the driver to read the curve into memory.
251
252         Also runs any commands in :attr:`command_stack`.  All
253         arguments are passed through to
254         :meth:`hooke.command_stack.CommandStack.execute`.
255         """
256         self.set_hooke(hooke)
257         log = logging.getLogger('hooke')
258         log.debug('loading curve %s with driver %s' % (self.name, self.driver))
259         data,info = self.driver.read(self.path, self.info)
260         self.data = data
261         for key,value in info.items():
262             self.info[key] = value
263         if self._hooke != None:
264             self.command_stack.execute(self._hooke)
265         elif len(self.command_stack) > 0:
266             log.warn(
267                 'could not execute command stack for %s without Hooke instance'
268                 % self.name)
269
270     def unload(self):
271         """Release memory intensive :attr:`.data`.
272         """
273         log = logging.getLogger('hooke')
274         log.debug('unloading curve %s' % self.name)
275         self.data = None