a87a7fd52b776d72d397cf2ed13f48461673d173
[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.path = path
169         self.driver = None
170         self.data = None
171         if info == None:
172             info = {}
173         self.info = info
174         self.name = os.path.basename(path)
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 __getstate__(self):
188         data = dict(self.__dict__)
189         del(data['_hooke'])
190         return data
191
192     def __setstate__(self, data):
193         self._hooke = None
194         for key,value in data.items():
195             setattr(self, key, value)
196
197     def set_hooke(self, hooke=None):
198         if hooke != None:
199             self._hooke = hooke
200
201     def identify(self, drivers):
202         """Identify the appropriate :class:`hooke.driver.Driver` for
203         the curve file (`.path`).
204         """
205         if 'filetype' in self.info:
206             driver = [d for d in drivers if d.name == self.info['filetype']]
207             if len(driver) == 1:
208                 driver = driver[0]
209                 if driver.is_me(self.path):
210                     self.driver = driver
211                     return
212         for driver in drivers:
213             if driver.is_me(self.path):
214                 self.driver = driver # remember the working driver
215                 return
216         raise NotRecognized(self)
217
218     def load(self, hooke=None):
219         """Use the driver to read the curve into memory.
220
221         Also runs any commands in :attr:`command_stack`.  All
222         arguments are passed through to
223         :meth:`hooke.command_stack.CommandStack.execute`.
224         """
225         self.set_hooke(hooke)
226         log = logging.getLogger('hooke')
227         log.debug('loading curve %s with driver %s' % (self.name, self.driver))
228         data,info = self.driver.read(self.path, self.info)
229         self.data = data
230         for key,value in info.items():
231             self.info[key] = value
232         if self._hooke != None:
233             self.command_stack.execute(self._hooke)
234         elif len(self.command_stack) > 0:
235             log.warn(
236                 'could not execute command stack for %s without Hooke instance'
237                 % self.name)
238
239     def unload(self):
240         """Release memory intensive :attr:`.data`.
241         """
242         log = logging.getLogger('hooke')
243         log.debug('unloading curve %s' % self.name)
244         self.data = None