1 # Copyright (C) 2010-2012 W. Trevor King <wking@tremily.us>
3 # This file is part of Hooke.
5 # Hooke is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke. If not, see <http://www.gnu.org/licenses/>.
18 """The `curve` module provides :class:`Curve` and :class:`Data` for
22 from copy_reg import dispatch_table
23 from copy import _reconstruct, _copy_dispatch
29 from .command_stack import CommandStack
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 The data-type is also YAMLable (see :mod:`hooke.util.yaml`).
97 columns: [distance (m), force (N)]
101 Data([], shape=(0, 0), dtype=float32)
103 def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
104 strides=None, order=None, info=None):
105 """Create the ndarray instance of our type, given the usual
106 input arguments. This will call the standard ndarray
107 constructor, but return an object of our type.
109 obj = numpy.ndarray.__new__(
110 subtype, shape, dtype, buffer, offset, strides, order)
111 # add the new attribute to the created instance
115 # Finally, we must return the newly created object:
118 def __array_finalize__(self, obj):
119 """Set any extra attributes from the original object when
120 creating a new view object."""
121 # reset the attribute from passed original object
122 self.info = getattr(obj, 'info', {})
123 # We do not need to return anything
125 def __reduce__(self):
126 """Collapse an instance for pickling.
130 reconstruct : callable
131 Called to create the initial version of the object.
133 A tuple of arguments for `reconstruct`
135 The state to be passed to __setstate__, if present.
136 iter : iterator (optional)
137 Yielded items will be appended to the reconstructed
139 dict : iterator (optional)
140 Yielded (key,value) tuples pushed back onto the
141 reconstructed object.
143 base_reduce = list(numpy.ndarray.__reduce__(self))
144 # tack our stuff onto ndarray's setstate portion.
145 base_reduce[2] = (base_reduce[2], (self.info,))
146 return tuple(base_reduce)
148 def __setstate__(self, state):
149 base_class_state,own_state = state
150 numpy.ndarray.__setstate__(self, base_class_state)
151 self.info, = own_state
154 class Curve (object):
155 """A grouped set of :class:`Data` runs from the same file with metadata.
157 For an approach/retract force spectroscopy experiment, the group
158 would consist of the approach data and the retract data. Metadata
159 would be the temperature, cantilever spring constant, etc.
161 Each :class:`Data` block in :attr:`data` must contain an
162 :attr:`info['name']` setting with a unique (for the parent
163 curve) name identifying the data block. This allows plugins
164 and commands to access individual blocks.
166 Each curve maintiains a :class:`~hooke.command_stack.CommandStack`
167 (:attr:`command_stack`) listing the commands that have been
168 applied to the `Curve` since loading.
170 The data-type is pickleable, to ensure we can move it between
171 processes with :class:`multiprocessing.Queue`\s.
175 >>> from .engine import CommandMessage
176 >>> c = Curve(path='some/path')
178 We add a recursive reference to `c` as you would get from
179 :meth:`hooke.plugin.curve.CurveCommand._add_to_command_stack`.
181 >>> c.command_stack.append(CommandMessage('curve info', {'curve':c}))
183 >>> s = pickle.dumps(c)
184 >>> z = pickle.loads(s)
188 [<CommandMessage curve info {curve: <Curve path>}>]
189 >>> z.command_stack[-1].arguments['curve'] == z
191 >>> print yaml.dump(c) # doctest: +REPORT_UDIFF
192 &id001 !!python/object:hooke.curve.Curve
193 command_stack: !!python/object/new:hooke.command_stack.CommandStack
195 - !!python/object:hooke.engine.CommandMessage
199 explicit_user_call: true
204 However, if we try and serialize the command stack first, we run
205 into `Python issue 1062277`_.
207 .. _Python issue 1062277: http://bugs.python.org/issue1062277
209 >>> pickle.dumps(c.command_stack)
210 Traceback (most recent call last):
212 assert id(obj) not in self.memo
215 YAML still works, though.
217 >>> print yaml.dump(c.command_stack) # doctest: +REPORT_UDIFF
218 &id001 !!python/object/new:hooke.command_stack.CommandStack
220 - !!python/object:hooke.engine.CommandMessage
222 curve: !!python/object:hooke.curve.Curve
223 command_stack: *id001
227 explicit_user_call: true
230 def __init__(self, path, info=None):
231 self.__setstate__({'path':path, 'info':info})
234 return str(self.__unicode__())
236 def __unicode__(self):
237 return u'<%s %s>' % (self.__class__.__name__, self.name)
240 return self.__str__()
242 def set_path(self, path):
244 path = os.path.expanduser(path)
246 if self.name == None and path != None:
247 self.name = os.path.basename(path)
249 def _setup_default_attrs(self):
250 # .data contains: {name of data: list of data sets [{[x], [y]}]
251 # ._hooke contains a Hooke instance for Curve.load()
252 self._default_attrs = {
262 def __getstate__(self):
263 state = dict(self.__dict__) # make a copy of the attribute dict.
267 def __setstate__(self, state):
268 self._setup_default_attrs()
269 self.__dict__.update(self._default_attrs)
272 self.__dict__.update(state)
273 self.set_path(getattr(self, 'path', None))
274 if self.info in [None, {}]:
276 if type(self.command_stack) == list:
277 self.command_stack = CommandStack()
280 """Set copy to preserve :attr:`_hooke`.
282 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
283 :mod:`yaml` output, but it should be preserved (but not
284 duplicated) during copies.
287 >>> class Hooke (object):
290 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
291 >>> for i in range(3): # initialize d
292 ... for j in range(2):
293 ... d[i,j] = i*10 + j
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...>
311 {'columns': ['distance (m)', 'force (N)']}
312 >>> id(c2.data[0]) == id(d)
315 copier = _copy_dispatch.get(type(self))
318 reductor = dispatch_table.get(type(self))
322 # :class:`object` implements __reduce_ex__, see :pep:`307`.
323 rv = self.__reduce_ex__(2)
324 y = _reconstruct(self, rv, 0)
325 y.set_hooke(self._hooke)
328 def __deepcopy__(self, memo):
329 """Set deepcopy to preserve :attr:`_hooke`.
331 :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and
332 :mod:`yaml` output, but it should be preserved (but not
333 duplicated) during copies.
336 >>> class Hooke (object):
339 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
340 >>> for i in range(3): # initialize d
341 ... for j in range(2):
342 ... d[i,j] = i*10 + j
346 >>> c._hooke # doctest: +ELLIPSIS
347 <hooke.curve.Hooke object at 0x...>
350 >>> c2 = copy.deepcopy(c)
351 >>> c2._hooke # doctest: +ELLIPSIS
352 <hooke.curve.Hooke object at 0x...>
360 {'columns': ['distance (m)', 'force (N)']}
361 >>> id(c2.data[0]) == id(d)
364 reductor = dispatch_table.get(type(self))
368 # :class:`object` implements __reduce_ex__, see :pep:`307`.
369 rv = self.__reduce_ex__(2)
370 y = _reconstruct(self, rv, 1, memo)
371 y.set_hooke(self._hooke)
374 def set_hooke(self, hooke=None):
378 def identify(self, drivers):
379 """Identify the appropriate :class:`hooke.driver.Driver` for
380 the curve file (`.path`).
382 if 'filetype' in self.info:
383 driver = [d for d in drivers if d.name == self.info['filetype']]
386 if driver.is_me(self.path):
389 for driver in drivers:
390 if driver.is_me(self.path):
391 self.driver = driver # remember the working driver
393 raise NotRecognized(self)
395 def load(self, hooke=None):
396 """Use the driver to read the curve into memory.
398 Also runs any commands in :attr:`command_stack`. All
399 arguments are passed through to
400 :meth:`hooke.command_stack.CommandStack.execute`.
402 self.set_hooke(hooke)
403 log = logging.getLogger('hooke')
404 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
405 data,info = self.driver.read(self.path, self.info)
407 for key,value in info.items():
408 self.info[key] = value
409 if self._hooke != None:
410 self.command_stack.execute(self._hooke)
411 elif len(self.command_stack) > 0:
413 'could not execute command stack for %s without Hooke instance'
417 """Release memory intensive :attr:`.data`.
419 log = logging.getLogger('hooke')
420 log.debug('unloading curve %s' % self.name)