Adjust hooke.curve.Data so .info is picklable.
[hooke.git] / hooke / curve.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Lesser General Public License for more details.
14 #
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/>.
18
19 """The `curve` module provides :class:`Curve` and :class:`Data` for
20 storing force curves.
21 """
22
23 import os.path
24 import numpy
25
26
27 class NotRecognized (ValueError):
28     def __init__(self, curve):
29         self.__setstate__(curve)
30
31     def __getstate__(self):
32         return self.curve
33
34     def __setstate__(self, data):
35         if isinstance(data, Curve):
36             msg = 'Not a recognizable curve format: %s' % data.path
37             super(NotRecognized, self).__init__(msg)
38             self.curve = data
39
40 class Data (numpy.ndarray):
41     """Stores a single, continuous data set.
42
43     Adds :attr:`info` :class:`dict` to the standard :class:`numpy.ndarray`.
44
45     See :mod:`numpy.doc.subclassing` for the peculiarities of
46     subclassing :class:`numpy.ndarray`.
47
48     Examples
49     --------
50
51     >>> d = Data(shape=(3,2), info={'columns':['distance (m)', 'force (N)']})
52     >>> type(d)
53     <class 'hooke.curve.Data'>
54     >>> for i in range(3): # initialize d
55     ...    for j in range(2):
56     ...        d[i,j] = i*10 + j
57     >>> d
58     Data([[  0.,   1.],
59            [ 10.,  11.],
60            [ 20.,  21.]])
61     >>> d.info
62     {'columns': ['distance (m)', 'force (N)']}
63     >>> row_a = d[:,0]
64     >>> row_a
65     Data([  0.,  10.,  20.])
66     >>> row_a.info
67     {'columns': ['distance (m)', 'force (N)']}
68     """
69     def __new__(subtype, shape, dtype=numpy.float, buffer=None, offset=0,
70                 strides=None, order=None, info=None):
71         """Create the ndarray instance of our type, given the usual
72         input arguments.  This will call the standard ndarray
73         constructor, but return an object of our type.
74         """
75         obj = numpy.ndarray.__new__(
76             subtype, shape, dtype, buffer, offset, strides, order)
77         # add the new attribute to the created instance
78         if info == None:
79             info = {}
80         obj.info = info
81         # Finally, we must return the newly created object:
82         return obj
83
84     def __array_finalize__(self, obj):
85         """Set any extra attributes from the original object when
86         creating a new view object."""
87         # reset the attribute from passed original object
88         self.info = getattr(obj, 'info', {})
89         # We do not need to return anything
90
91     def __reduce__(self):
92         base_class_state = list(numpy.ndarray.__reduce__(self))
93         own_state = (self.info,)
94         return (base_class_state, own_state)
95
96     def __setstate__(self,state):
97         base_class_state,own_state = state
98         numpy.ndarray.__setstate__(self, base_class_state)
99         self.info, = own_state
100
101
102 class Curve (object):
103     """A grouped set of :class:`Data` runs from the same file with metadata.
104
105     For an approach/retract force spectroscopy experiment, the group
106     would consist of the approach data and the retract data.  Metadata
107     would be the temperature, cantilever spring constant, etc.
108
109     Two important :attr:`info` settings are `filetype` and
110     `experiment`.  These are two strings that can be used by Hooke
111     commands/plugins to understand what they are looking at.
112
113     * `.info['filetype']` should contain the name of the exact
114       filetype defined by the driver (so that filetype-speciofic
115       commands can know if they're dealing with the correct filetype).
116     * `.info['experiment']` should contain an instance of a
117       :class:`hooke.experiment.Experiment` subclass to identify the
118       experiment type.  For example, various
119       :class:`hooke.driver.Driver`\s can read in force-clamp data, but
120       Hooke commands could like to know if they're looking at force
121       clamp data, regardless of their origin.
122     """
123     def __init__(self, path, info=None):
124         #the data dictionary contains: {name of data: list of data sets [{[x], [y]}]
125         self.path = path
126         self.driver = None
127         self.data = None
128         if info == None:
129             info = {}
130         self.info = info
131         self.name = os.path.basename(path)
132
133     def identify(self, drivers):
134         """Identify the appropriate :class:`hooke.driver.Driver` for
135         the curve file (`.path`).
136         """
137         for driver in drivers:
138             if driver.is_me(self.path):
139                 self.driver = driver # remember the working driver
140                 return
141         raise NotRecognized(self)
142
143     def load(self):
144         """Use the driver to read the curve into memory.
145         """
146         data,info = self.driver.read(self.path)
147         self.data = data
148         for key,value in info.items():
149             self.info[key] = value
150
151     def unload(self):
152         """Release memory intensive :attr:`.data`.
153         """
154         self.data = None