1 # Copyright (C) 2010-2012 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`).
98 columns: [distance (m), force (N)]
102 Data([], shape=(0, 0), dtype=float32)
104 def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
105 strides=None, order=None, info=None):
106 """Create the ndarray instance of our type, given the usual
107 input arguments. This will call the standard ndarray
108 constructor, but return an object of our type.
110 obj = numpy.ndarray.__new__(
111 subtype, shape, dtype, buffer, offset, strides, order)
112 # add the new attribute to the created instance
116 # Finally, we must return the newly created object:
119 def __array_finalize__(self, obj):
120 """Set any extra attributes from the original object when
121 creating a new view object."""
122 # reset the attribute from passed original object
123 self.info = getattr(obj, 'info', {})
124 # We do not need to return anything
126 def __reduce__(self):
127 """Collapse an instance for pickling.
131 reconstruct : callable
132 Called to create the initial version of the object.
134 A tuple of arguments for `reconstruct`
136 The state to be passed to __setstate__, if present.
137 iter : iterator (optional)
138 Yielded items will be appended to the reconstructed
140 dict : iterator (optional)
141 Yielded (key,value) tuples pushed back onto the
142 reconstructed object.
144 base_reduce = list(numpy.ndarray.__reduce__(self))
145 # tack our stuff onto ndarray's setstate portion.
146 base_reduce[2] = (base_reduce[2], (self.info,))
147 return tuple(base_reduce)
149 def __setstate__(self, state):
150 base_class_state,own_state = state
151 numpy.ndarray.__setstate__(self, base_class_state)
152 self.info, = own_state
155 class Curve (object):
156 """A grouped set of :class:`Data` runs from the same file with metadata.
158 For an approach/retract force spectroscopy experiment, the group
159 would consist of the approach data and the retract data. Metadata
160 would be the temperature, cantilever spring constant, etc.
162 Each :class:`Data` block in :attr:`data` must contain an
163 :attr:`info['name']` setting with a unique (for the parent
164 curve) name identifying the data block. This allows plugins
165 and commands to access individual blocks.
167 Each curve maintiains a :class:`~hooke.command_stack.CommandStack`
168 (:attr:`command_stack`) listing the commands that have been
169 applied to the `Curve` since loading.
171 The data-type is pickleable, to ensure we can move it between
172 processes with :class:`multiprocessing.Queue`\s.
176 >>> from .engine import CommandMessage
177 >>> c = Curve(path='some/path')
179 We add a recursive reference to `c` as you would get from
180 :meth:`hooke.plugin.curve.CurveCommand._add_to_command_stack`.
182 >>> c.command_stack.append(CommandMessage('curve info', {'curve':c}))
184 >>> s = pickle.dumps(c)
185 >>> z = pickle.loads(s)
189 [<CommandMessage curve info {curve: <Curve path>}>]
190 >>> z.command_stack[-1].arguments['curve'] == z
192 >>> print yaml.dump(c) # doctest: +REPORT_UDIFF
193 &id001 !!python/object:hooke.curve.Curve
194 command_stack: !!python/object/new:hooke.command_stack.CommandStack
196 - !!python/object:hooke.engine.CommandMessage
200 explicit_user_call: true
205 However, if we try and serialize the command stack first, we run
206 into `Python issue 1062277`_.
208 .. _Python issue 1062277: http://bugs.python.org/issue1062277
210 >>> pickle.dumps(c.command_stack)
211 Traceback (most recent call last):
213 assert id(obj) not in self.memo
216 YAML still works, though.
218 >>> print yaml.dump(c.command_stack) # doctest: +REPORT_UDIFF
219 &id001 !!python/object/new:hooke.command_stack.CommandStack
221 - !!python/object:hooke.engine.CommandMessage
223 curve: !!python/object:hooke.curve.Curve
224 command_stack: *id001
228 explicit_user_call: true
231 def __init__(self, path, info=None):
232 self.__setstate__({'path':path, 'info':info})
235 return str(self.__unicode__())
237 def __unicode__(self):
238 return u'<%s %s>' % (self.__class__.__name__, self.name)
241 return self.__str__()
243 def set_path(self, path):
245 path = os.path.expanduser(path)
247 if self.name == None and path != None:
248 self.name = os.path.basename(path)
250 def _setup_default_attrs(self):
251 # .data contains: {name of data: list of data sets [{[x], [y]}]
252 # ._hooke contains a Hooke instance for Curve.load()
253 self._default_attrs = {
263 def __getstate__(self):
264 state = dict(self.__dict__) # make a copy of the attribute dict.
268 def __setstate__(self, state):
269 self._setup_default_attrs()
270 self.__dict__.update(self._default_attrs)
273 self.__dict__.update(state)
274 self.set_path(getattr(self, 'path', None))
275 if self.info in [None, {}]:
277 if type(self.command_stack) == list:
278 self.command_stack = CommandStack()
281 """Set copy to preserve :attr:`_hooke`.
283 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
284 :mod:`yaml` output, but it should be preserved (but not
285 duplicated) during copies.
288 >>> class Hooke (object):
291 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
292 >>> for i in range(3): # initialize d
293 ... for j in range(2):
294 ... d[i,j] = i*10 + j
298 >>> c._hooke # doctest: +ELLIPSIS
299 <hooke.curve.Hooke object at 0x...>
302 >>> c2 = copy.copy(c)
303 >>> c2._hooke # doctest: +ELLIPSIS
304 <hooke.curve.Hooke object at 0x...>
312 {'columns': ['distance (m)', 'force (N)']}
313 >>> id(c2.data[0]) == id(d)
316 copier = _copy_dispatch.get(type(self))
319 reductor = dispatch_table.get(type(self))
323 # :class:`object` implements __reduce_ex__, see :pep:`307`.
324 rv = self.__reduce_ex__(2)
325 y = _reconstruct(self, rv, 0)
326 y.set_hooke(self._hooke)
329 def __deepcopy__(self, memo):
330 """Set deepcopy to preserve :attr:`_hooke`.
332 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
333 :mod:`yaml` output, but it should be preserved (but not
334 duplicated) during copies.
337 >>> class Hooke (object):
340 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
341 >>> for i in range(3): # initialize d
342 ... for j in range(2):
343 ... d[i,j] = i*10 + j
347 >>> c._hooke # doctest: +ELLIPSIS
348 <hooke.curve.Hooke object at 0x...>
351 >>> c2 = copy.deepcopy(c)
352 >>> c2._hooke # doctest: +ELLIPSIS
353 <hooke.curve.Hooke object at 0x...>
361 {'columns': ['distance (m)', 'force (N)']}
362 >>> id(c2.data[0]) == id(d)
365 reductor = dispatch_table.get(type(self))
369 # :class:`object` implements __reduce_ex__, see :pep:`307`.
370 rv = self.__reduce_ex__(2)
371 y = _reconstruct(self, rv, 1, memo)
372 y.set_hooke(self._hooke)
375 def set_hooke(self, hooke=None):
379 def identify(self, drivers):
380 """Identify the appropriate :class:`hooke.driver.Driver` for
381 the curve file (`.path`).
383 if 'filetype' in self.info:
384 driver = [d for d in drivers if d.name == self.info['filetype']]
387 if driver.is_me(self.path):
390 for driver in drivers:
391 if driver.is_me(self.path):
392 self.driver = driver # remember the working driver
394 raise NotRecognized(self)
396 def load(self, hooke=None):
397 """Use the driver to read the curve into memory.
399 Also runs any commands in :attr:`command_stack`. All
400 arguments are passed through to
401 :meth:`hooke.command_stack.CommandStack.execute`.
403 self.set_hooke(hooke)
404 log = logging.getLogger('hooke')
405 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
406 data,info = self.driver.read(self.path, self.info)
408 for key,value in info.items():
409 self.info[key] = value
410 if self._hooke != None:
411 self.command_stack.execute(self._hooke)
412 elif len(self.command_stack) > 0:
414 'could not execute command stack for %s without Hooke instance'
418 """Release memory intensive :attr:`.data`.
420 log = logging.getLogger('hooke')
421 log.debug('unloading curve %s' % self.name)