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
33 class NotRecognized (ValueError):
34 def __init__(self, curve):
35 self.__setstate__(curve)
37 def __getstate__(self):
40 def __setstate__(self, data):
41 if isinstance(data, Curve):
42 msg = 'Not a recognizable curve format: %s' % data.path
43 super(NotRecognized, self).__init__(msg)
47 class Data (numpy.ndarray):
48 """Stores a single, continuous data set.
50 Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
52 See :mod:`numpy.doc.subclassing` for the peculiarities of
53 subclassing :class:`numpy.ndarray`.
58 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
60 <class 'hooke.curve.Data'>
61 >>> for i in range(3): # initialize d
62 ... for j in range(2):
69 {'columns': ['distance (m)', 'force (N)']}
71 The information gets passed on to slices.
77 {'columns': ['distance (m)', 'force (N)']}
79 The data-type is also pickleable, to ensure we can move it between
80 processes with :class:`multiprocessing.Queue`\s.
83 >>> s = pickle.dumps(d)
84 >>> z = pickle.loads(s)
90 {'columns': ['distance (m)', 'force (N)']}
92 The data-type is also YAMLable (see :mod:`hooke.util.yaml`).
95 >>> print yaml.dump(d)
97 columns: [distance (m), force (N)]
100 def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
101 strides=None, order=None, info=None):
102 """Create the ndarray instance of our type, given the usual
103 input arguments. This will call the standard ndarray
104 constructor, but return an object of our type.
106 obj = numpy.ndarray.__new__(
107 subtype, shape, dtype, buffer, offset, strides, order)
108 # add the new attribute to the created instance
112 # Finally, we must return the newly created object:
115 def __array_finalize__(self, obj):
116 """Set any extra attributes from the original object when
117 creating a new view object."""
118 # reset the attribute from passed original object
119 self.info = getattr(obj, 'info', {})
120 # We do not need to return anything
122 def __reduce__(self):
123 """Collapse an instance for pickling.
127 reconstruct : callable
128 Called to create the initial version of the object.
130 A tuple of arguments for `reconstruct`
132 The state to be passed to __setstate__, if present.
133 iter : iterator (optional)
134 Yielded items will be appended to the reconstructed
136 dict : iterator (optional)
137 Yielded (key,value) tuples pushed back onto the
138 reconstructed object.
140 base_reduce = list(numpy.ndarray.__reduce__(self))
141 # tack our stuff onto ndarray's setstate portion.
142 base_reduce[2] = (base_reduce[2], (self.info,))
143 return tuple(base_reduce)
145 def __setstate__(self, state):
146 base_class_state,own_state = state
147 numpy.ndarray.__setstate__(self, base_class_state)
148 self.info, = own_state
151 class Curve (object):
152 """A grouped set of :class:`Data` runs from the same file with metadata.
154 For an approach/retract force spectroscopy experiment, the group
155 would consist of the approach data and the retract data. Metadata
156 would be the temperature, cantilever spring constant, etc.
158 Each :class:`Data` block in :attr:`data` must contain an
159 :attr:`info['name']` setting with a unique (for the parent
160 curve) name identifying the data block. This allows plugins
161 and commands to access individual blocks.
163 Each curve maintiains a :class:`~hooke.command_stack.CommandStack`
164 (:attr:`command_stack`) listing the commands that have been
165 applied to the `Curve` since loading.
167 The data-type is pickleable, to ensure we can move it between
168 processes with :class:`multiprocessing.Queue`\s.
172 >>> from .engine import CommandMessage
173 >>> c = Curve(path='some/path')
175 We add a recursive reference to `c` as you would get from
176 :meth:`hooke.plugin.curve.CurveCommand._add_to_command_stack`.
178 >>> c.command_stack.append(CommandMessage('curve info', {'curve':c}))
180 >>> s = pickle.dumps(c)
181 >>> z = pickle.loads(s)
185 [<CommandMessage curve info {curve: <Curve path>}>]
186 >>> z.command_stack[-1].arguments['curve'] == z
188 >>> print yaml.dump(c) # doctest: +REPORT_UDIFF
189 &id001 !!python/object:hooke.curve.Curve
190 command_stack: !!python/object/new:hooke.command_stack.CommandStack
192 - !!python/object:hooke.engine.CommandMessage
196 explicit_user_call: true
201 However, if we try and serialize the command stack first, we run
202 into `Python issue 1062277`_.
204 .. _Python issue 1062277: http://bugs.python.org/issue1062277
206 >>> pickle.dumps(c.command_stack)
207 Traceback (most recent call last):
209 assert id(obj) not in self.memo
212 YAML still works, though.
214 >>> print yaml.dump(c.command_stack) # doctest: +REPORT_UDIFF
215 &id001 !!python/object/new:hooke.command_stack.CommandStack
217 - !!python/object:hooke.engine.CommandMessage
219 curve: !!python/object:hooke.curve.Curve
220 command_stack: *id001
224 explicit_user_call: true
227 def __init__(self, path, info=None):
228 self.__setstate__({'path':path, 'info':info})
231 return str(self.__unicode__())
233 def __unicode__(self):
234 return u'<%s %s>' % (self.__class__.__name__, self.name)
237 return self.__str__()
239 def set_path(self, path):
241 if self.name == None and path != None:
242 self.name = os.path.basename(path)
244 def _setup_default_attrs(self):
245 # .data contains: {name of data: list of data sets [{[x], [y]}]
246 # ._hooke contains a Hooke instance for Curve.load()
247 self._default_attrs = {
257 def __getstate__(self):
258 state = dict(self.__dict__) # make a copy of the attribute dict.
262 def __setstate__(self, state):
263 self._setup_default_attrs()
264 self.__dict__.update(self._default_attrs)
267 self.__dict__.update(state)
268 self.set_path(getattr(self, 'path', None))
269 if self.info in [None, {}]:
271 if type(self.command_stack) == list:
272 self.command_stack = CommandStack()
275 """Set copy to preserve :attr:`_hooke`.
277 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
278 :mod:`yaml` output, but it should be preserved (but not
279 duplicated) during copies.
282 >>> class Hooke (object):
285 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
286 >>> for i in range(3): # initialize d
287 ... for j in range(2):
288 ... d[i,j] = i*10 + j
292 >>> c._hooke # doctest: +ELLIPSIS
293 <hooke.curve.Hooke object at 0x...>
296 >>> c2 = copy.copy(c)
297 >>> c2._hooke # doctest: +ELLIPSIS
298 <hooke.curve.Hooke object at 0x...>
306 {'columns': ['distance (m)', 'force (N)']}
307 >>> id(c2.data[0]) == id(d)
310 copier = _copy_dispatch.get(type(self))
313 reductor = dispatch_table.get(type(self))
317 # :class:`object` implements __reduce_ex__, see :pep:`307`.
318 rv = self.__reduce_ex__(2)
319 y = _reconstruct(self, rv, 0)
320 y.set_hooke(self._hooke)
323 def __deepcopy__(self, memo):
324 """Set deepcopy to preserve :attr:`_hooke`.
326 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
327 :mod:`yaml` output, but it should be preserved (but not
328 duplicated) during copies.
331 >>> class Hooke (object):
334 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
335 >>> for i in range(3): # initialize d
336 ... for j in range(2):
337 ... d[i,j] = i*10 + j
341 >>> c._hooke # doctest: +ELLIPSIS
342 <hooke.curve.Hooke object at 0x...>
345 >>> c2 = copy.deepcopy(c)
346 >>> c2._hooke # doctest: +ELLIPSIS
347 <hooke.curve.Hooke object at 0x...>
355 {'columns': ['distance (m)', 'force (N)']}
356 >>> id(c2.data[0]) == id(d)
359 reductor = dispatch_table.get(type(self))
363 # :class:`object` implements __reduce_ex__, see :pep:`307`.
364 rv = self.__reduce_ex__(2)
365 y = _reconstruct(self, rv, 1, memo)
366 y.set_hooke(self._hooke)
369 def set_hooke(self, hooke=None):
373 def identify(self, drivers):
374 """Identify the appropriate :class:`hooke.driver.Driver` for
375 the curve file (`.path`).
377 if 'filetype' in self.info:
378 driver = [d for d in drivers if d.name == self.info['filetype']]
381 if driver.is_me(self.path):
384 for driver in drivers:
385 if driver.is_me(self.path):
386 self.driver = driver # remember the working driver
388 raise NotRecognized(self)
390 def load(self, hooke=None):
391 """Use the driver to read the curve into memory.
393 Also runs any commands in :attr:`command_stack`. All
394 arguments are passed through to
395 :meth:`hooke.command_stack.CommandStack.execute`.
397 self.set_hooke(hooke)
398 log = logging.getLogger('hooke')
399 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
400 data,info = self.driver.read(self.path, self.info)
402 for key,value in info.items():
403 self.info[key] = value
404 if self._hooke != None:
405 self.command_stack.execute(self._hooke)
406 elif len(self.command_stack) > 0:
408 'could not execute command stack for %s without Hooke instance'
412 """Release memory intensive :attr:`.data`.
414 log = logging.getLogger('hooke')
415 log.debug('unloading curve %s' % self.name)