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
28 from .command_stack import CommandStack
31 class NotRecognized (ValueError):
32 def __init__(self, curve):
33 self.__setstate__(curve)
35 def __getstate__(self):
38 def __setstate__(self, data):
39 if isinstance(data, Curve):
40 msg = 'Not a recognizable curve format: %s' % data.path
41 super(NotRecognized, self).__init__(msg)
45 class Data (numpy.ndarray):
46 """Stores a single, continuous data set.
48 Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
50 See :mod:`numpy.doc.subclassing` for the peculiarities of
51 subclassing :class:`numpy.ndarray`.
56 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
58 <class 'hooke.curve.Data'>
59 >>> for i in range(3): # initialize d
60 ... for j in range(2):
67 {'columns': ['distance (m)', 'force (N)']}
69 The information gets passed on to slices.
75 {'columns': ['distance (m)', 'force (N)']}
77 The data-type is also pickleable, to ensure we can move it between
78 processes with :class:`multiprocessing.Queue`\s.
81 >>> s = pickle.dumps(d)
82 >>> z = pickle.loads(s)
88 {'columns': ['distance (m)', 'force (N)']}
90 def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
91 strides=None, order=None, info=None):
92 """Create the ndarray instance of our type, given the usual
93 input arguments. This will call the standard ndarray
94 constructor, but return an object of our type.
96 obj = numpy.ndarray.__new__(
97 subtype, shape, dtype, buffer, offset, strides, order)
98 # add the new attribute to the created instance
102 # Finally, we must return the newly created object:
105 def __array_finalize__(self, obj):
106 """Set any extra attributes from the original object when
107 creating a new view object."""
108 # reset the attribute from passed original object
109 self.info = getattr(obj, 'info', {})
110 # We do not need to return anything
112 def __reduce__(self):
113 """Collapse an instance for pickling.
117 reconstruct : callable
118 Called to create the initial version of the object.
120 A tuple of arguments for `reconstruct`
122 The state to be passed to __setstate__, if present.
123 iter : iterator (optional)
124 Yielded items will be appended to the reconstructed
126 dict : iterator (optional)
127 Yielded (key,value) tuples pushed back onto the
128 reconstructed object.
130 base_reduce = list(numpy.ndarray.__reduce__(self))
131 # tack our stuff onto ndarray's setstate portion.
132 base_reduce[2] = (base_reduce[2], (self.info,))
133 return tuple(base_reduce)
135 def __setstate__(self, state):
136 base_class_state,own_state = state
137 numpy.ndarray.__setstate__(self, base_class_state)
138 self.info, = own_state
141 class Curve (object):
142 """A grouped set of :class:`Data` runs from the same file with metadata.
144 For an approach/retract force spectroscopy experiment, the group
145 would consist of the approach data and the retract data. Metadata
146 would be the temperature, cantilever spring constant, etc.
148 Two important :attr:`info` settings are `filetype` and
149 `experiment`. These are two strings that can be used by Hooke
150 commands/plugins to understand what they are looking at.
152 * :attr:`info['filetype']` should contain the name of the exact
153 filetype defined by the driver (so that filetype-speciofic
154 commands can know if they're dealing with the correct filetype).
155 * :attr:`info['experiment']` should contain an instance of a
156 :class:`hooke.experiment.Experiment` subclass to identify the
157 experiment type. For example, various
158 :class:`hooke.driver.Driver`\s can read in force-clamp data, but
159 Hooke commands could like to know if they're looking at force
160 clamp data, regardless of their origin.
162 Another important attribute is :attr:`command_stack`, which holds
163 a :class:`~hooke.command_stack.CommandStack` listing the commands
164 that have been applied to the `Curve` since loading.
166 def __init__(self, path, info=None):
167 #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
174 self.name = os.path.basename(path)
175 self.command_stack = CommandStack()
176 self._hooke = None # Hooke instance for Curve.load()
179 return str(self.__unicode__())
181 def __unicode__(self):
182 return u'<%s %s>' % (self.__class__.__name__, self.name)
185 return self.__str__()
187 def __getstate__(self):
188 data = dict(self.__dict__)
192 def __setstate__(self, data):
194 for key,value in data.items():
195 setattr(self, key, value)
197 def set_hooke(self, hooke=None):
201 def identify(self, drivers):
202 """Identify the appropriate :class:`hooke.driver.Driver` for
203 the curve file (`.path`).
205 if 'filetype' in self.info:
206 driver = [d for d in drivers if d.name == self.info['filetype']]
209 if driver.is_me(self.path):
212 for driver in drivers:
213 if driver.is_me(self.path):
214 self.driver = driver # remember the working driver
216 raise NotRecognized(self)
218 def load(self, hooke=None):
219 """Use the driver to read the curve into memory.
221 Also runs any commands in :attr:`command_stack`. All
222 arguments are passed through to
223 :meth:`hooke.command_stack.CommandStack.execute`.
225 self.set_hooke(hooke)
226 log = logging.getLogger('hooke')
227 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
228 data,info = self.driver.read(self.path, self.info)
230 for key,value in info.items():
231 self.info[key] = value
232 if self._hooke != None:
233 self.command_stack.execute(self._hooke)
234 elif len(self.command_stack) > 0:
236 'could not execute command stack for %s without Hooke instance'
240 """Release memory intensive :attr:`.data`.
242 log = logging.getLogger('hooke')
243 log.debug('unloading curve %s' % self.name)