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