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
23 from copy_reg import dispatch_table
24 from copy import _reconstruct, _copy_dispatch
30 from .command_stack import CommandStack
31 from . import experiment
34 class NotRecognized (ValueError):
35 def __init__(self, curve):
36 self.__setstate__(curve)
38 def __getstate__(self):
41 def __setstate__(self, data):
42 if isinstance(data, Curve):
43 msg = 'Not a recognizable curve format: %s' % data.path
44 super(NotRecognized, self).__init__(msg)
48 class Data (numpy.ndarray):
49 """Stores a single, continuous data set.
51 Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
53 See :mod:`numpy.doc.subclassing` for the peculiarities of
54 subclassing :class:`numpy.ndarray`.
59 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
61 <class 'hooke.curve.Data'>
62 >>> for i in range(3): # initialize d
63 ... for j in range(2):
70 {'columns': ['distance (m)', 'force (N)']}
72 The information gets passed on to slices.
78 {'columns': ['distance (m)', 'force (N)']}
80 The data-type is also pickleable, to ensure we can move it between
81 processes with :class:`multiprocessing.Queue`\s.
84 >>> s = pickle.dumps(d)
85 >>> z = pickle.loads(s)
91 {'columns': ['distance (m)', 'force (N)']}
93 The data-type is also YAMLable (see :mod:`hooke.util.yaml`).
96 >>> print yaml.dump(d)
98 columns: [distance (m), force (N)]
101 def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
102 strides=None, order=None, info=None):
103 """Create the ndarray instance of our type, given the usual
104 input arguments. This will call the standard ndarray
105 constructor, but return an object of our type.
107 obj = numpy.ndarray.__new__(
108 subtype, shape, dtype, buffer, offset, strides, order)
109 # add the new attribute to the created instance
113 # Finally, we must return the newly created object:
116 def __array_finalize__(self, obj):
117 """Set any extra attributes from the original object when
118 creating a new view object."""
119 # reset the attribute from passed original object
120 self.info = getattr(obj, 'info', {})
121 # We do not need to return anything
123 def __reduce__(self):
124 """Collapse an instance for pickling.
128 reconstruct : callable
129 Called to create the initial version of the object.
131 A tuple of arguments for `reconstruct`
133 The state to be passed to __setstate__, if present.
134 iter : iterator (optional)
135 Yielded items will be appended to the reconstructed
137 dict : iterator (optional)
138 Yielded (key,value) tuples pushed back onto the
139 reconstructed object.
141 base_reduce = list(numpy.ndarray.__reduce__(self))
142 # tack our stuff onto ndarray's setstate portion.
143 base_reduce[2] = (base_reduce[2], (self.info,))
144 return tuple(base_reduce)
146 def __setstate__(self, state):
147 base_class_state,own_state = state
148 numpy.ndarray.__setstate__(self, base_class_state)
149 self.info, = own_state
152 class Curve (object):
153 """A grouped set of :class:`Data` runs from the same file with metadata.
155 For an approach/retract force spectroscopy experiment, the group
156 would consist of the approach data and the retract data. Metadata
157 would be the temperature, cantilever spring constant, etc.
159 Two important :attr:`info` settings are `filetype` and
160 `experiment`. These are two strings that can be used by Hooke
161 commands/plugins to understand what they are looking at.
163 * :attr:`info['filetype']` should contain the name of the exact
164 filetype defined by the driver (so that filetype-speciofic
165 commands can know if they're dealing with the correct filetype).
166 * :attr:`info['experiment']` should contain an instance of a
167 :class:`hooke.experiment.Experiment` subclass to identify the
168 experiment type. For example, various
169 :class:`hooke.driver.Driver`\s can read in force-clamp data, but
170 Hooke commands could like to know if they're looking at force
171 clamp data, regardless of their origin.
173 Another important attribute is :attr:`command_stack`, which holds
174 a :class:`~hooke.command_stack.CommandStack` listing the commands
175 that have been applied to the `Curve` since loading.
177 The data-type is pickleable, to ensure we can move it between
178 processes with :class:`multiprocessing.Queue`\s.
182 >>> from .engine import CommandMessage
183 >>> c = Curve(path='some/path')
185 We add a recursive reference to `c` as you would get from
186 :meth:`hooke.plugin.curve.CurveCommand._add_to_command_stack`.
188 >>> c.command_stack.append(CommandMessage('curve info', {'curve':c}))
190 >>> s = pickle.dumps(c)
191 >>> z = pickle.loads(s)
195 [<CommandMessage curve info {curve: <Curve path>}>]
196 >>> z.command_stack[-1].arguments['curve'] == z
198 >>> print yaml.dump(c) # doctest: +REPORT_UDIFF
199 &id001 !!python/object:hooke.curve.Curve
200 command_stack: !!python/object/new:hooke.command_stack.CommandStack
202 - !!python/object:hooke.engine.CommandMessage
206 explicit_user_call: true
211 However, if we try and serialize the command stack first, we run
212 into `Python issue 1062277`_.
214 .. _Python issue 1062277: http://bugs.python.org/issue1062277
216 >>> pickle.dumps(c.command_stack)
217 Traceback (most recent call last):
219 assert id(obj) not in self.memo
222 YAML still works, though.
224 >>> print yaml.dump(c.command_stack) # doctest: +REPORT_UDIFF
225 &id001 !!python/object/new:hooke.command_stack.CommandStack
227 - !!python/object:hooke.engine.CommandMessage
229 curve: !!python/object:hooke.curve.Curve
230 command_stack: *id001
234 explicit_user_call: true
237 def __init__(self, path, info=None):
238 self.__setstate__({'path':path, 'info':info})
241 return str(self.__unicode__())
243 def __unicode__(self):
244 return u'<%s %s>' % (self.__class__.__name__, self.name)
247 return self.__str__()
249 def set_path(self, path):
251 if self.name == None and path != None:
252 self.name = os.path.basename(path)
254 def _setup_default_attrs(self):
255 # .data contains: {name of data: list of data sets [{[x], [y]}]
256 # ._hooke contains a Hooke instance for Curve.load()
257 self._default_attrs = {
267 def __getstate__(self):
268 state = dict(self.__dict__) # make a copy of the attribute dict.
272 def __setstate__(self, state):
273 self._setup_default_attrs()
274 self.__dict__.update(self._default_attrs)
277 self.__dict__.update(state)
278 self.set_path(getattr(self, 'path', None))
279 if self.info in [None, {}]:
281 if type(self.command_stack) == list:
282 self.command_stack = CommandStack()
285 """Set copy to preserve :attr:`_hooke`.
287 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
288 :mod:`yaml` output, but it should be preserved (but not
289 duplicated) during copies.
292 >>> class Hooke (object):
295 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
296 >>> for i in range(3): # initialize d
297 ... for j in range(2):
298 ... d[i,j] = i*10 + j
302 >>> c._hooke # doctest: +ELLIPSIS
303 <hooke.curve.Hooke object at 0x...>
306 >>> c2 = copy.copy(c)
307 >>> c2._hooke # doctest: +ELLIPSIS
308 <hooke.curve.Hooke object at 0x...>
316 {'columns': ['distance (m)', 'force (N)']}
317 >>> id(c2.data[0]) == id(d)
320 copier = _copy_dispatch.get(type(self))
323 reductor = dispatch_table.get(type(self))
327 # :class:`object` implements __reduce_ex__, see :pep:`307`.
328 rv = self.__reduce_ex__(2)
329 y = _reconstruct(self, rv, 0)
330 y.set_hooke(self._hooke)
333 def __deepcopy__(self, memo):
334 """Set deepcopy to preserve :attr:`_hooke`.
336 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
337 :mod:`yaml` output, but it should be preserved (but not
338 duplicated) during copies.
341 >>> class Hooke (object):
344 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
345 >>> for i in range(3): # initialize d
346 ... for j in range(2):
347 ... d[i,j] = i*10 + j
351 >>> c._hooke # doctest: +ELLIPSIS
352 <hooke.curve.Hooke object at 0x...>
355 >>> c2 = copy.deepcopy(c)
356 >>> c2._hooke # doctest: +ELLIPSIS
357 <hooke.curve.Hooke object at 0x...>
365 {'columns': ['distance (m)', 'force (N)']}
366 >>> id(c2.data[0]) == id(d)
369 reductor = dispatch_table.get(type(self))
373 # :class:`object` implements __reduce_ex__, see :pep:`307`.
374 rv = self.__reduce_ex__(2)
375 y = _reconstruct(self, rv, 1, memo)
376 y.set_hooke(self._hooke)
379 def set_hooke(self, hooke=None):
383 def identify(self, drivers):
384 """Identify the appropriate :class:`hooke.driver.Driver` for
385 the curve file (`.path`).
387 if 'filetype' in self.info:
388 driver = [d for d in drivers if d.name == self.info['filetype']]
391 if driver.is_me(self.path):
394 for driver in drivers:
395 if driver.is_me(self.path):
396 self.driver = driver # remember the working driver
398 raise NotRecognized(self)
400 def load(self, hooke=None):
401 """Use the driver to read the curve into memory.
403 Also runs any commands in :attr:`command_stack`. All
404 arguments are passed through to
405 :meth:`hooke.command_stack.CommandStack.execute`.
407 self.set_hooke(hooke)
408 log = logging.getLogger('hooke')
409 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
410 data,info = self.driver.read(self.path, self.info)
412 for key,value in info.items():
413 self.info[key] = value
414 if self._hooke != None:
415 self.command_stack.execute(self._hooke)
416 elif len(self.command_stack) > 0:
418 'could not execute command stack for %s without Hooke instance'
422 """Release memory intensive :attr:`.data`.
424 log = logging.getLogger('hooke')
425 log.debug('unloading curve %s' % self.name)