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