X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fcurve.py;h=53670151685a92bbc16a373e5b396854ae0e7fc3;hp=552eda6abe5582c2bf3b47f1926a8899f3829e77;hb=60b12a779acf92ec32ef2d0ed9e5766b11705150;hpb=8e89eecbde44ef0be854d898455e8543a30204b7 diff --git a/hooke/curve.py b/hooke/curve.py index 552eda6..5367015 100644 --- a/hooke/curve.py +++ b/hooke/curve.py @@ -1,26 +1,29 @@ -# 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 + import numpy from .command_stack import CommandStack @@ -39,6 +42,7 @@ class NotRecognized (ValueError): super(NotRecognized, self).__init__(msg) self.curve = data + class Data (numpy.ndarray): """Stores a single, continuous data set. @@ -83,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): @@ -142,34 +158,222 @@ 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.__setstate__({'path':path, 'info':info}) + + def __str__(self): + return str(self.__unicode__()) + + def __unicode__(self): + return u'<%s %s>' % (self.__class__.__name__, self.name) + + def __repr__(self): + return self.__str__() + + def set_path(self, path): + if path != None: + path = os.path.expanduser(path) 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() + 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 def identify(self, drivers): """Identify the appropriate :class:`hooke.driver.Driver` for @@ -188,20 +392,30 @@ class Curve (object): return raise NotRecognized(self) - def load(self, *args, **kwargs): + def load(self, hooke=None): """Use the driver to read the curve into memory. Also runs any commands in :attr:`command_stack`. All arguments are passed through to :meth:`hooke.command_stack.CommandStack.execute`. """ + self.set_hooke(hooke) + log = logging.getLogger('hooke') + log.debug('loading curve %s with driver %s' % (self.name, self.driver)) data,info = self.driver.read(self.path, self.info) self.data = data for key,value in info.items(): self.info[key] = value - self.command_stack.execute(*args, **kwargs) + if self._hooke != None: + self.command_stack.execute(self._hooke) + elif len(self.command_stack) > 0: + log.warn( + 'could not execute command stack for %s without Hooke instance' + % self.name) def unload(self): """Release memory intensive :attr:`.data`. """ + log = logging.getLogger('hooke') + log.debug('unloading curve %s' % self.name) self.data = None