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)
44 class Data (numpy.ndarray):
45 """Stores a single, continuous data set.
47 Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
49 See :mod:`numpy.doc.subclassing` for the peculiarities of
50 subclassing :class:`numpy.ndarray`.
55 >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
57 <class 'hooke.curve.Data'>
58 >>> for i in range(3): # initialize d
59 ... for j in range(2):
66 {'columns': ['distance (m)', 'force (N)']}
68 The information gets passed on to slices.
74 {'columns': ['distance (m)', 'force (N)']}
76 The data-type is also pickleable, to ensure we can move it between
77 processes with :class:`multiprocessing.Queue`\s.
80 >>> s = pickle.dumps(d)
81 >>> z = pickle.loads(s)
87 {'columns': ['distance (m)', 'force (N)']}
89 def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
90 strides=None, order=None, info=None):
91 """Create the ndarray instance of our type, given the usual
92 input arguments. This will call the standard ndarray
93 constructor, but return an object of our type.
95 obj = numpy.ndarray.__new__(
96 subtype, shape, dtype, buffer, offset, strides, order)
97 # add the new attribute to the created instance
101 # Finally, we must return the newly created object:
104 def __array_finalize__(self, obj):
105 """Set any extra attributes from the original object when
106 creating a new view object."""
107 # reset the attribute from passed original object
108 self.info = getattr(obj, 'info', {})
109 # We do not need to return anything
111 def __reduce__(self):
112 """Collapse an instance for pickling.
116 reconstruct : callable
117 Called to create the initial version of the object.
119 A tuple of arguments for `reconstruct`
121 The state to be passed to __setstate__, if present.
122 iter : iterator (optional)
123 Yielded items will be appended to the reconstructed
125 dict : iterator (optional)
126 Yielded (key,value) tuples pushed back onto the
127 reconstructed object.
129 base_reduce = list(numpy.ndarray.__reduce__(self))
130 # tack our stuff onto ndarray's setstate portion.
131 base_reduce[2] = (base_reduce[2], (self.info,))
132 return tuple(base_reduce)
134 def __setstate__(self, state):
135 base_class_state,own_state = state
136 numpy.ndarray.__setstate__(self, base_class_state)
137 self.info, = own_state
140 class Curve (object):
141 """A grouped set of :class:`Data` runs from the same file with metadata.
143 For an approach/retract force spectroscopy experiment, the group
144 would consist of the approach data and the retract data. Metadata
145 would be the temperature, cantilever spring constant, etc.
147 Two important :attr:`info` settings are `filetype` and
148 `experiment`. These are two strings that can be used by Hooke
149 commands/plugins to understand what they are looking at.
151 * :attr:`info['filetype']` should contain the name of the exact
152 filetype defined by the driver (so that filetype-speciofic
153 commands can know if they're dealing with the correct filetype).
154 * :attr:`info['experiment']` should contain an instance of a
155 :class:`hooke.experiment.Experiment` subclass to identify the
156 experiment type. For example, various
157 :class:`hooke.driver.Driver`\s can read in force-clamp data, but
158 Hooke commands could like to know if they're looking at force
159 clamp data, regardless of their origin.
161 Another important attribute is :attr:`command_stack`, which holds
162 a :class:`~hooke.command_stack.CommandStack` listing the commands
163 that have been applied to the `Curve` since loading.
165 def __init__(self, path, info=None):
166 #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
173 self.name = os.path.basename(path)
174 self.command_stack = CommandStack()
175 self._hooke = None # Hooke instance for Curve.load()
178 return str(self.__unicode__())
180 def __unicode__(self):
181 return u'<%s %s>' % (self.__class__.__name__, self.name)
184 return self.__str__()
186 def set_hooke(self, hooke=None):
190 def identify(self, drivers):
191 """Identify the appropriate :class:`hooke.driver.Driver` for
192 the curve file (`.path`).
194 if 'filetype' in self.info:
195 driver = [d for d in drivers if d.name == self.info['filetype']]
198 if driver.is_me(self.path):
201 for driver in drivers:
202 if driver.is_me(self.path):
203 self.driver = driver # remember the working driver
205 raise NotRecognized(self)
207 def load(self, hooke=None):
208 """Use the driver to read the curve into memory.
210 Also runs any commands in :attr:`command_stack`. All
211 arguments are passed through to
212 :meth:`hooke.command_stack.CommandStack.execute`.
214 self.set_hooke(hooke)
215 log = logging.getLogger('hooke')
216 log.debug('loading curve %s with driver %s' % (self.name, self.driver))
217 data,info = self.driver.read(self.path, self.info)
219 for key,value in info.items():
220 self.info[key] = value
221 if self._hooke != None:
222 self.command_stack.execute(self._hooke)
223 elif len(self.command_stack) > 0:
225 'could not execute command stack for %s without Hooke instance'
229 """Release memory intensive :attr:`.data`.
231 log = logging.getLogger('hooke')
232 log.debug('unloading curve %s' % self.name)