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