1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
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.
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.
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/>.
19 """The `curve` module provides :class:`Curve` and :class:`Data` for
28 from .command_stack import CommandStack
29 from . import experiment
32 class NotRecognized (ValueError):
33 def __init__(self, curve):
34 self.__setstate__(curve)
36 def __getstate__(self):
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)
46 class Data (numpy.ndarray):
47 """Stores a single, continuous data set.
49 Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
51 See :mod:`numpy.doc.subclassing` for the peculiarities of
52 subclassing :class:`numpy.ndarray`.
57 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
59 <class 'hooke.curve.Data'>
60 >>> for i in range(3): # initialize d
61 ... for j in range(2):
68 {'columns': ['distance (m)', 'force (N)']}
70 The information gets passed on to slices.
76 {'columns': ['distance (m)', 'force (N)']}
78 The data-type is also pickleable, to ensure we can move it between
79 processes with :class:`multiprocessing.Queue`\s.
82 >>> s = pickle.dumps(d)
83 >>> z = pickle.loads(s)
89 {'columns': ['distance (m)', 'force (N)']}
91 def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
92 strides=None, order=None, info=None):
93 """Create the ndarray instance of our type, given the usual
94 input arguments. This will call the standard ndarray
95 constructor, but return an object of our type.
97 obj = numpy.ndarray.__new__(
98 subtype, shape, dtype, buffer, offset, strides, order)
99 # add the new attribute to the created instance
103 # Finally, we must return the newly created object:
106 def __array_finalize__(self, obj):
107 """Set any extra attributes from the original object when
108 creating a new view object."""
109 # reset the attribute from passed original object
110 self.info = getattr(obj, 'info', {})
111 # We do not need to return anything
113 def __reduce__(self):
114 """Collapse an instance for pickling.
118 reconstruct : callable
119 Called to create the initial version of the object.
121 A tuple of arguments for `reconstruct`
123 The state to be passed to __setstate__, if present.
124 iter : iterator (optional)
125 Yielded items will be appended to the reconstructed
127 dict : iterator (optional)
128 Yielded (key,value) tuples pushed back onto the
129 reconstructed object.
131 base_reduce = list(numpy.ndarray.__reduce__(self))
132 # tack our stuff onto ndarray's setstate portion.
133 base_reduce[2] = (base_reduce[2], (self.info,))
134 return tuple(base_reduce)
136 def __setstate__(self, state):
137 base_class_state,own_state = state
138 numpy.ndarray.__setstate__(self, base_class_state)
139 self.info, = own_state
142 class Curve (object):
143 """A grouped set of :class:`Data` runs from the same file with metadata.
145 For an approach/retract force spectroscopy experiment, the group
146 would consist of the approach data and the retract data. Metadata
147 would be the temperature, cantilever spring constant, etc.
149 Two important :attr:`info` settings are `filetype` and
150 `experiment`. These are two strings that can be used by Hooke
151 commands/plugins to understand what they are looking at.
153 * :attr:`info['filetype']` should contain the name of the exact
154 filetype defined by the driver (so that filetype-speciofic
155 commands can know if they're dealing with the correct filetype).
156 * :attr:`info['experiment']` should contain an instance of a
157 :class:`hooke.experiment.Experiment` subclass to identify the
158 experiment type. For example, various
159 :class:`hooke.driver.Driver`\s can read in force-clamp data, but
160 Hooke commands could like to know if they're looking at force
161 clamp data, regardless of their origin.
163 Another important attribute is :attr:`command_stack`, which holds
164 a :class:`~hooke.command_stack.CommandStack` listing the commands
165 that have been applied to the `Curve` since loading.
167 def __init__(self, path, info=None):
168 #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
176 self.command_stack = CommandStack()
177 self._hooke = None # Hooke instance for Curve.load()
180 return str(self.__unicode__())
182 def __unicode__(self):
183 return u'<%s %s>' % (self.__class__.__name__, self.name)
186 return self.__str__()
188 def set_path(self, path):
190 if self.name == None and path != None:
191 self.name = os.path.basename(path)
193 def __getstate__(self):
194 state = dict(self.__dict__) # make a copy of the attribute dict.
195 state['info'] = dict(self.info) # make a copy of the info dict too.
197 dc = state['command_stack']
198 if hasattr(dc, '__getstate__'):
199 state['command_stack'] = dc.__getstate__()
200 if state['info'].get('experiment', None) != None:
201 e = state['info']['experiment']
202 assert isinstance(e, experiment.Experiment), type(e)
203 # HACK? require Experiment classes to be defined in the
205 state['info']['experiment'] = e.__class__.__name__
208 def __setstate__(self, state):
209 self.name = self._hooke = None
210 for key,value in state.items():
215 if 'experiment' not in value:
216 value['experiment'] = None
217 elif value['experiment'] != None:
218 # HACK? require Experiment classes to be defined in the
220 cls = getattr(experiment, value['experiment'])
221 value['experiment'] = cls()
222 elif key == 'command_stack':
224 v.__setstate__(value)
226 setattr(self, key, value)
228 def set_hooke(self, hooke=None):
232 def identify(self, drivers):
233 """Identify the appropriate :class:`hooke.driver.Driver` for
234 the curve file (`.path`).
236 if 'filetype' in self.info:
237 driver = [d for d in drivers if d.name == self.info['filetype']]
240 if driver.is_me(self.path):
243 for driver in drivers:
244 if driver.is_me(self.path):
245 self.driver = driver # remember the working driver
247 raise NotRecognized(self)
249 def load(self, hooke=None):
250 """Use the driver to read the curve into memory.
252 Also runs any commands in :attr:`command_stack`. All
253 arguments are passed through to
254 :meth:`hooke.command_stack.CommandStack.execute`.
256 self.set_hooke(hooke)
257 log = logging.getLogger('hooke')
258 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
259 data,info = self.driver.read(self.path, self.info)
261 for key,value in info.items():
262 self.info[key] = value
263 if self._hooke != None:
264 self.command_stack.execute(self._hooke)
265 elif len(self.command_stack) > 0:
267 'could not execute command stack for %s without Hooke instance'
271 """Release memory intensive :attr:`.data`.
273 log = logging.getLogger('hooke')
274 log.debug('unloading curve %s' % self.name)