X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fcurve.py;h=d312be36e076447b914868f9fb2ba36d26020438;hp=ed76c8a557d08566d6495c2a7e86854f473970cc;hb=23bcfd05915a61e3a41d01babd291fd3089ba329;hpb=28a01c259df23e587352035f08e97295e01b3fe2 diff --git a/hooke/curve.py b/hooke/curve.py index ed76c8a..d312be3 100644 --- a/hooke/curve.py +++ b/hooke/curve.py @@ -1,25 +1,26 @@ -# Copyright (C) 2010 W. Trevor King +# Copyright (C) 2010-2012 W. Trevor King # # This file is part of Hooke. # -# Hooke is free software: you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. +# Hooke is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. # -# Hooke is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General -# Public License for more details. +# Hooke is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. # -# You should have received a copy of the GNU Lesser General Public -# License along with Hooke. If not, see -# . +# You should have received a copy of the GNU Lesser General Public License +# along with Hooke. If not, see . """The `curve` module provides :class:`Curve` and :class:`Data` for storing force curves. """ +from copy_reg import dispatch_table +from copy import _reconstruct, _copy_dispatch import logging import os.path @@ -41,6 +42,7 @@ class NotRecognized (ValueError): super(NotRecognized, self).__init__(msg) self.curve = data + class Data (numpy.ndarray): """Stores a single, continuous data set. @@ -85,6 +87,18 @@ class Data (numpy.ndarray): [ 20., 21.]]) >>> z.info {'columns': ['distance (m)', 'force (N)']} + + The data-type is also YAMLable (see :mod:`hooke.util.yaml`). + + >>> import yaml + >>> s = yaml.dump(d) + >>> print s + !hooke.curve.DataInfo + columns: [distance (m), force (N)] + + >>> z = yaml.load(s) + >>> z + Data([], shape=(0, 0), dtype=float32) """ def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0, strides=None, order=None, info=None): @@ -144,35 +158,77 @@ class Curve (object): would consist of the approach data and the retract data. Metadata would be the temperature, cantilever spring constant, etc. - Two important :attr:`info` settings are `filetype` and - `experiment`. These are two strings that can be used by Hooke - commands/plugins to understand what they are looking at. - - * :attr:`info['filetype']` should contain the name of the exact - filetype defined by the driver (so that filetype-speciofic - commands can know if they're dealing with the correct filetype). - * :attr:`info['experiment']` should contain an instance of a - :class:`hooke.experiment.Experiment` subclass to identify the - experiment type. For example, various - :class:`hooke.driver.Driver`\s can read in force-clamp data, but - Hooke commands could like to know if they're looking at force - clamp data, regardless of their origin. - - Another important attribute is :attr:`command_stack`, which holds - a :class:`~hooke.command_stack.CommandStack` listing the commands - that have been applied to the `Curve` since loading. + Each :class:`Data` block in :attr:`data` must contain an + :attr:`info['name']` setting with a unique (for the parent + curve) name identifying the data block. This allows plugins + and commands to access individual blocks. + + Each curve maintiains a :class:`~hooke.command_stack.CommandStack` + (:attr:`command_stack`) listing the commands that have been + applied to the `Curve` since loading. + + The data-type is pickleable, to ensure we can move it between + processes with :class:`multiprocessing.Queue`\s. + + >>> import pickle + >>> import yaml + >>> from .engine import CommandMessage + >>> c = Curve(path='some/path') + + We add a recursive reference to `c` as you would get from + :meth:`hooke.plugin.curve.CurveCommand._add_to_command_stack`. + + >>> c.command_stack.append(CommandMessage('curve info', {'curve':c})) + + >>> s = pickle.dumps(c) + >>> z = pickle.loads(s) + >>> z + + >>> z.command_stack + [}>] + >>> z.command_stack[-1].arguments['curve'] == z + True + >>> print yaml.dump(c) # doctest: +REPORT_UDIFF + &id001 !!python/object:hooke.curve.Curve + command_stack: !!python/object/new:hooke.command_stack.CommandStack + listitems: + - !!python/object:hooke.engine.CommandMessage + arguments: + curve: *id001 + command: curve info + explicit_user_call: true + name: path + path: some/path + + + However, if we try and serialize the command stack first, we run + into `Python issue 1062277`_. + + .. _Python issue 1062277: http://bugs.python.org/issue1062277 + + >>> pickle.dumps(c.command_stack) + Traceback (most recent call last): + ... + assert id(obj) not in self.memo + AssertionError + + YAML still works, though. + + >>> print yaml.dump(c.command_stack) # doctest: +REPORT_UDIFF + &id001 !!python/object/new:hooke.command_stack.CommandStack + listitems: + - !!python/object:hooke.engine.CommandMessage + arguments: + curve: !!python/object:hooke.curve.Curve + command_stack: *id001 + name: path + path: some/path + command: curve info + explicit_user_call: true + """ def __init__(self, path, info=None): - #the data dictionary contains: {name of data: list of data sets [{[x], [y]}] - self.path = path - self.driver = None - self.data = None - if info == None: - info = {} - self.info = info - self.name = os.path.basename(path) - self.command_stack = CommandStack() - self._hooke = None # Hooke instance for Curve.load() + self.__setstate__({'path':path, 'info':info}) def __str__(self): return str(self.__unicode__()) @@ -183,6 +239,138 @@ class Curve (object): def __repr__(self): return self.__str__() + def set_path(self, path): + if path != None: + path = os.path.expanduser(path) + self.path = path + if self.name == None and path != None: + self.name = os.path.basename(path) + + def _setup_default_attrs(self): + # .data contains: {name of data: list of data sets [{[x], [y]}] + # ._hooke contains a Hooke instance for Curve.load() + self._default_attrs = { + '_hooke': None, + 'command_stack': [], + 'data': None, + 'driver': None, + 'info': {}, + 'name': None, + 'path': None, + } + + def __getstate__(self): + state = dict(self.__dict__) # make a copy of the attribute dict. + del(state['_hooke']) + return state + + def __setstate__(self, state): + self._setup_default_attrs() + self.__dict__.update(self._default_attrs) + if state == True: + return + self.__dict__.update(state) + self.set_path(getattr(self, 'path', None)) + if self.info in [None, {}]: + self.info = {} + if type(self.command_stack) == list: + self.command_stack = CommandStack() + + def __copy__(self): + """Set copy to preserve :attr:`_hooke`. + + :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and + :mod:`yaml` output, but it should be preserved (but not + duplicated) during copies. + + >>> import copy + >>> class Hooke (object): + ... pass + >>> h = Hooke() + >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']}) + >>> for i in range(3): # initialize d + ... for j in range(2): + ... d[i,j] = i*10 + j + >>> c = Curve(None) + >>> c.data = [d] + >>> c.set_hooke(h) + >>> c._hooke # doctest: +ELLIPSIS + + >>> c._hooke == h + True + >>> c2 = copy.copy(c) + >>> c2._hooke # doctest: +ELLIPSIS + + >>> c2._hooke == h + True + >>> c2.data + [Data([[ 0., 1.], + [ 10., 11.], + [ 20., 21.]])] + >>> d.info + {'columns': ['distance (m)', 'force (N)']} + >>> id(c2.data[0]) == id(d) + True + """ + copier = _copy_dispatch.get(type(self)) + if copier: + return copier(self) + reductor = dispatch_table.get(type(self)) + if reductor: + rv = reductor(self) + else: + # :class:`object` implements __reduce_ex__, see :pep:`307`. + rv = self.__reduce_ex__(2) + y = _reconstruct(self, rv, 0) + y.set_hooke(self._hooke) + return y + + def __deepcopy__(self, memo): + """Set deepcopy to preserve :attr:`_hooke`. + + :meth:`getstate` drops :attr:`_hooke` for :mod:`pickle` and + :mod:`yaml` output, but it should be preserved (but not + duplicated) during copies. + + >>> import copy + >>> class Hooke (object): + ... pass + >>> h = Hooke() + >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']}) + >>> for i in range(3): # initialize d + ... for j in range(2): + ... d[i,j] = i*10 + j + >>> c = Curve(None) + >>> c.data = [d] + >>> c.set_hooke(h) + >>> c._hooke # doctest: +ELLIPSIS + + >>> c._hooke == h + True + >>> c2 = copy.deepcopy(c) + >>> c2._hooke # doctest: +ELLIPSIS + + >>> c2._hooke == h + True + >>> c2.data + [Data([[ 0., 1.], + [ 10., 11.], + [ 20., 21.]])] + >>> d.info + {'columns': ['distance (m)', 'force (N)']} + >>> id(c2.data[0]) == id(d) + False + """ + reductor = dispatch_table.get(type(self)) + if reductor: + rv = reductor(self) + else: + # :class:`object` implements __reduce_ex__, see :pep:`307`. + rv = self.__reduce_ex__(2) + y = _reconstruct(self, rv, 1, memo) + y.set_hooke(self._hooke) + return y + def set_hooke(self, hooke=None): if hooke != None: self._hooke = hooke