test/data/vclamp_jpk/README: Document sample versions
[hooke.git] / hooke / curve.py
1 # Copyright (C) 2010-2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
8 # later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
17
18 """The `curve` module provides :class:`Curve` and :class:`Data` for
19 storing force curves.
20 """
21
22 from copy_reg import dispatch_table
23 from copy import _reconstruct, _copy_dispatch
24 import logging
25 import os.path
26
27 import numpy
28
29 from .command_stack import CommandStack
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     The data-type is also YAMLable (see :mod:`hooke.util.yaml`).
92
93     >>> import yaml
94     >>> s = yaml.dump(d)
95     >>> print(s)
96     !hooke.curve.DataInfo
97     columns: [distance (m), force (N)]
98     <BLANKLINE>
99     >>> z = yaml.load(s)
100     >>> z
101     Data([], shape=(0, 0), dtype=float32)
102     """
103     def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
104                 strides=None, order=None, info=None):
105         """Create the ndarray instance of our type, given the usual
106         input arguments.  This will call the standard ndarray
107         constructor, but return an object of our type.
108         """
109         obj = numpy.ndarray.__new__(
110             subtype, shape, dtype, buffer, offset, strides, order)
111         # add the new attribute to the created instance
112         if info == None:
113             info = {}
114         obj.info = info
115         # Finally, we must return the newly created object:
116         return obj
117
118     def __array_finalize__(self, obj):
119         """Set any extra attributes from the original object when
120         creating a new view object."""
121         # reset the attribute from passed original object
122         self.info = getattr(obj, 'info', {})
123         # We do not need to return anything
124
125     def __reduce__(self):
126         """Collapse an instance for pickling.
127
128         Returns
129         -------
130         reconstruct : callable
131             Called to create the initial version of the object.
132         args : tuple
133             A tuple of arguments for `reconstruct`
134         state : (optional)
135             The state to be passed to __setstate__, if present.
136         iter : iterator (optional)
137             Yielded items will be appended to the reconstructed
138             object.
139         dict : iterator (optional)
140             Yielded (key,value) tuples pushed back onto the
141             reconstructed object.
142         """
143         base_reduce = list(numpy.ndarray.__reduce__(self))
144         # tack our stuff onto ndarray's setstate portion.
145         base_reduce[2] = (base_reduce[2], (self.info,))
146         return tuple(base_reduce)
147
148     def __setstate__(self, state):
149         base_class_state,own_state = state
150         numpy.ndarray.__setstate__(self, base_class_state)
151         self.info, = own_state
152
153
154 class Curve (object):
155     """A grouped set of :class:`Data` runs from the same file with metadata.
156
157     For an approach/retract force spectroscopy experiment, the group
158     would consist of the approach data and the retract data.  Metadata
159     would be the temperature, cantilever spring constant, etc.
160
161     Each :class:`Data` block in :attr:`data` must contain an
162     :attr:`info['name']` setting with a unique (for the parent
163     curve) name identifying the data block.  This allows plugins 
164     and commands to access individual blocks.
165
166     Each curve maintiains a :class:`~hooke.command_stack.CommandStack`
167     (:attr:`command_stack`) listing the commands that have been
168     applied to the `Curve` since loading.
169
170     The data-type is pickleable, to ensure we can move it between
171     processes with :class:`multiprocessing.Queue`\s.
172
173     >>> import pickle
174     >>> import yaml
175     >>> from .engine import CommandMessage
176     >>> c = Curve(path='some/path')
177
178     We add a recursive reference to `c` as you would get from
179     :meth:`hooke.plugin.curve.CurveCommand._add_to_command_stack`.
180
181     >>> c.command_stack.append(CommandMessage('curve info', {'curve':c}))
182
183     >>> s = pickle.dumps(c)
184     >>> z = pickle.loads(s)
185     >>> z
186     <Curve path>
187     >>> z.command_stack
188     [<CommandMessage curve info {curve: <Curve path>}>]
189     >>> z.command_stack[-1].arguments['curve'] == z
190     True
191     >>> print(yaml.dump(c))  # doctest: +REPORT_UDIFF
192     &id001 !!python/object:hooke.curve.Curve
193     command_stack: !!python/object/new:hooke.command_stack.CommandStack
194       listitems:
195       - !!python/object:hooke.engine.CommandMessage
196         arguments:
197           curve: *id001
198         command: curve info
199         explicit_user_call: true
200     name: path
201     path: some/path
202     <BLANKLINE>
203
204     However, if we try and serialize the command stack first, we run
205     into `Python issue 1062277`_.
206
207     .. _Python issue 1062277: http://bugs.python.org/issue1062277
208
209     >>> pickle.dumps(c.command_stack)
210     Traceback (most recent call last):
211       ...
212         assert id(obj) not in self.memo
213     AssertionError
214
215     YAML still works, though.
216
217     >>> print(yaml.dump(c.command_stack))  # doctest: +REPORT_UDIFF
218     &id001 !!python/object/new:hooke.command_stack.CommandStack
219     listitems:
220     - !!python/object:hooke.engine.CommandMessage
221       arguments:
222         curve: !!python/object:hooke.curve.Curve
223           command_stack: *id001
224           name: path
225           path: some/path
226       command: curve info
227       explicit_user_call: true
228     <BLANKLINE>
229     """
230     def __init__(self, path, info=None):
231         self.__setstate__({'path':path, 'info':info})
232
233     def __str__(self):
234         return str(self.__unicode__())
235
236     def __unicode__(self):
237         return u'<%s %s>' % (self.__class__.__name__, self.name)
238
239     def __repr__(self):
240         return self.__str__()
241
242     def set_path(self, path):
243         if path != None:
244             path = os.path.expanduser(path)
245         self.path = path
246         if self.name == None and path != None:
247             self.name = os.path.basename(path)
248
249     def _setup_default_attrs(self):
250         # .data contains: {name of data: list of data sets [{[x], [y]}]
251         # ._hooke contains a Hooke instance for Curve.load()
252         self._default_attrs = {
253             '_hooke': None,
254             'command_stack': [],
255             'data': None,
256             'driver': None,
257             'info': {},
258             'name': None,
259             'path': None,
260             }
261
262     def __getstate__(self):
263         state = dict(self.__dict__)  # make a copy of the attribute dict.
264         del(state['_hooke'])
265         return state
266
267     def __setstate__(self, state):
268         self._setup_default_attrs()
269         self.__dict__.update(self._default_attrs)
270         if state == True:
271             return
272         self.__dict__.update(state)
273         self.set_path(getattr(self, 'path', None))
274         if self.info in [None, {}]:
275             self.info = {}
276         if type(self.command_stack) == list:
277             self.command_stack = CommandStack()
278
279     def __copy__(self):
280         """Set copy to preserve :attr:`_hooke`.
281
282         :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
283         :mod:`yaml` output, but it should be preserved (but not
284         duplicated) during copies.
285
286         >>> import copy
287         >>> class Hooke (object):
288         ...     pass
289         >>> h = Hooke()
290         >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
291         >>> for i in range(3): # initialize d
292         ...    for j in range(2):
293         ...        d[i,j] = i*10 + j
294         >>> c = Curve(None)
295         >>> c.data = [d]
296         >>> c.set_hooke(h)
297         >>> c._hooke  # doctest: +ELLIPSIS
298         <hooke.curve.Hooke object at 0x...>
299         >>> c._hooke == h
300         True
301         >>> c2 = copy.copy(c)
302         >>> c2._hooke  # doctest: +ELLIPSIS
303         <hooke.curve.Hooke object at 0x...>
304         >>> c2._hooke == h
305         True
306         >>> c2.data
307         [Data([[  0.,   1.],
308                [ 10.,  11.],
309                [ 20.,  21.]])]
310         >>> d.info
311         {'columns': ['distance (m)', 'force (N)']}
312         >>> id(c2.data[0]) == id(d)
313         True
314         """
315         copier = _copy_dispatch.get(type(self))
316         if copier:
317             return copier(self)
318         reductor = dispatch_table.get(type(self))
319         if reductor:
320             rv = reductor(self)
321         else:
322             # :class:`object` implements __reduce_ex__, see :pep:`307`.
323             rv = self.__reduce_ex__(2)
324         y = _reconstruct(self, rv, 0)
325         y.set_hooke(self._hooke)
326         return y
327
328     def __deepcopy__(self, memo):
329         """Set deepcopy to preserve :attr:`_hooke`.
330
331         :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
332         :mod:`yaml` output, but it should be preserved (but not
333         duplicated) during copies.
334
335         >>> import copy
336         >>> class Hooke (object):
337         ...     pass
338         >>> h = Hooke()
339         >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
340         >>> for i in range(3): # initialize d
341         ...    for j in range(2):
342         ...        d[i,j] = i*10 + j
343         >>> c = Curve(None)
344         >>> c.data = [d]
345         >>> c.set_hooke(h)
346         >>> c._hooke  # doctest: +ELLIPSIS
347         <hooke.curve.Hooke object at 0x...>
348         >>> c._hooke == h
349         True
350         >>> c2 = copy.deepcopy(c)
351         >>> c2._hooke  # doctest: +ELLIPSIS
352         <hooke.curve.Hooke object at 0x...>
353         >>> c2._hooke == h
354         True
355         >>> c2.data
356         [Data([[  0.,   1.],
357                [ 10.,  11.],
358                [ 20.,  21.]])]
359         >>> d.info
360         {'columns': ['distance (m)', 'force (N)']}
361         >>> id(c2.data[0]) == id(d)
362         False
363         """
364         reductor = dispatch_table.get(type(self))
365         if reductor:
366             rv = reductor(self)
367         else:
368             # :class:`object` implements __reduce_ex__, see :pep:`307`.
369             rv = self.__reduce_ex__(2)
370         y = _reconstruct(self, rv, 1, memo)
371         y.set_hooke(self._hooke)
372         return y
373
374     def set_hooke(self, hooke=None):
375         if hooke != None:
376             self._hooke = hooke
377
378     def identify(self, drivers):
379         """Identify the appropriate :class:`hooke.driver.Driver` for
380         the curve file (`.path`).
381         """
382         if 'filetype' in self.info:
383             driver = [d for d in drivers if d.name == self.info['filetype']]
384             if len(driver) == 1:
385                 driver = driver[0]
386                 if driver.is_me(self.path):
387                     self.driver = driver
388                     return
389         for driver in drivers:
390             if driver.is_me(self.path):
391                 self.driver = driver # remember the working driver
392                 return
393         raise NotRecognized(self)
394
395     def load(self, hooke=None):
396         """Use the driver to read the curve into memory.
397
398         Also runs any commands in :attr:`command_stack`.  All
399         arguments are passed through to
400         :meth:`hooke.command_stack.CommandStack.execute`.
401         """
402         self.set_hooke(hooke)
403         log = logging.getLogger('hooke')
404         log.debug('loading curve %s with driver %s' % (self.name, self.driver))
405         data,info = self.driver.read(self.path, self.info)
406         self.data = data
407         for key,value in info.items():
408             self.info[key] = value
409         if self._hooke != None:
410             self.command_stack.execute(self._hooke)
411         elif len(self.command_stack) > 0:
412             log.warn(
413                 'could not execute command stack for %s without Hooke instance'
414                 % self.name)
415
416     def unload(self):
417         """Release memory intensive :attr:`.data`.
418         """
419         log = logging.getLogger('hooke')
420         log.debug('unloading curve %s' % self.name)
421         self.data = None