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):
297 >>> c._hooke # doctest: +ELLIPSIS
298 <hooke.curve.Hooke object at 0x...>
301 >>> c2 = copy.copy(c)
302 >>> c2._hooke # doctest: +ELLIPSIS
303 <hooke.curve.Hooke object at 0x...>
307 copier = _copy_dispatch.get(type(self))
310 reductor = dispatch_table.get(type(self))
314 # :class:`object` implements __reduce_ex__, see :pep:`307`.
315 rv = self.__reduce_ex__(2)
316 y = _reconstruct(self, rv, 0)
317 y.set_hooke(self._hooke)
320 def __deepcopy__(self, memo):
321 """Set deepcopy to preserve :attr:`_hooke`.
323 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
324 :mod:`yaml` output, but it should be preserved (but not
325 duplicated) during copies.
328 >>> class Hooke (object):
333 >>> c._hooke # doctest: +ELLIPSIS
334 <hooke.curve.Hooke object at 0x...>
337 >>> c2 = copy.deepcopy(c)
338 >>> c2._hooke # doctest: +ELLIPSIS
339 <hooke.curve.Hooke object at 0x...>
343 reductor = dispatch_table.get(type(self))
347 # :class:`object` implements __reduce_ex__, see :pep:`307`.
348 rv = self.__reduce_ex__(2)
349 y = _reconstruct(self, rv, 1, memo)
350 y.set_hooke(self._hooke)
353 def set_hooke(self, hooke=None):
357 def identify(self, drivers):
358 """Identify the appropriate :class:`hooke.driver.Driver` for
359 the curve file (`.path`).
361 if 'filetype' in self.info:
362 driver = [d for d in drivers if d.name == self.info['filetype']]
365 if driver.is_me(self.path):
368 for driver in drivers:
369 if driver.is_me(self.path):
370 self.driver = driver # remember the working driver
372 raise NotRecognized(self)
374 def load(self, hooke=None):
375 """Use the driver to read the curve into memory.
377 Also runs any commands in :attr:`command_stack`. All
378 arguments are passed through to
379 :meth:`hooke.command_stack.CommandStack.execute`.
381 self.set_hooke(hooke)
382 log = logging.getLogger('hooke')
383 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
384 data,info = self.driver.read(self.path, self.info)
386 for key,value in info.items():
387 self.info[key] = value
388 if self._hooke != None:
389 self.command_stack.execute(self._hooke)
390 elif len(self.command_stack) > 0:
392 'could not execute command stack for %s without Hooke instance'
396 """Release memory intensive :attr:`.data`.
398 log = logging.getLogger('hooke')
399 log.debug('unloading curve %s' % self.name)