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