Merged my unitary FFT wrappers (FFT_tools) as hooke.util.fft.
[hooke.git] / hooke / driver / picoforce.py
1 # Copyright (C) 2006-2010 Alberto Gomez-Kasai
2 #                         Massimo Sandal <devicerandom@gmail.com>
3 #                         W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of Hooke.
6 #
7 # Hooke is free software: you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation, either
10 # version 3 of the License, or (at your option) any later version.
11 #
12 # Hooke is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with Hooke.  If not, see
19 # <http://www.gnu.org/licenses/>.
20
21 """Library for interpreting Veeco PicoForce force spectroscopy files.
22 """
23
24 import pprint
25 import re
26 import time
27
28 import numpy
29
30 from .. import curve as curve # this module defines data containers.
31 from .. import experiment as experiment # this module defines expt. types
32 from ..config import Setting # configurable setting class
33 from . import Driver as Driver # this is the Driver base class
34
35
36 __version__='0.0.0.20100516'
37
38 class PicoForceDriver (Driver):
39     """Handle Veeco Picoforce force spectroscopy files.
40     """
41     def __init__(self):
42         super(PicoForceDriver, self).__init__(name='picoforce')
43
44     def is_me(self, path):
45         f = file(path, 'r')
46         header = f.read(30)
47         f.close()
48
49         return header[2:17] == 'Force file list'
50
51     def read(self, path):
52         info = self._read_header_path(path)
53         self._check_version(info)
54         data = self._read_data_path(path, info)
55         info['filetype'] = self.name
56         info['experiment'] = experiment.VelocityClamp
57         return (data, info)
58
59     def _read_header_path(self, path):
60         """Read curve information from the PicoForce file at `path`.
61
62         See :meth:`._read_header_file`.
63         """
64         return self._read_header_file(file(path, 'rb'))
65
66     def _read_header_file(self, file):
67         r"""Read curve information from a PicoForce file.
68
69         Return a dict of dicts representing the information.  If a
70         field is repeated multiple times, it's value is replaced by a
71         list of the values for each occurence.
72
73         Examples
74         --------
75
76         >>> import pprint
77         >>> import StringIO
78         >>> f = StringIO.StringIO('\r\n'.join([
79         ...             '\*Force file list',
80         ...             '\Version: 0x06120002',
81         ...             '\Date: 04:42:34 PM Tue Sep 11 2007',
82         ...             '\Start context: FOL2',
83         ...             '\Data length: 40960',
84         ...             '\Text: ',
85         ...             '\*Equipment list',
86         ...             '\Description: Extended PicoForce',
87         ...             '\Controller: IIIA',
88         ...             '\*Ciao force image list',
89         ...             '\Data offset: 40960',
90         ...             '\Data length: 8192',
91         ...             '\*Ciao force image list',
92         ...             '\Data offset: 49152',
93         ...             '\Data length: 8192',
94         ...             '\*Ciao force image list',
95         ...             '\Data offset: 57344',
96         ...             '\Data length: 8192',
97         ...             ]))
98         >>> p = PicoForceDriver()
99         >>> d = p._read_header_file(f)
100         >>> pprint.pprint(d, width=60)
101         {'Ciao force image list': [{'Data length': '8192',
102                                     'Data offset': '40960'},
103                                    {'Data length': '8192',
104                                     'Data offset': '49152'},
105                                    {'Data length': '8192',
106                                     'Data offset': '57344'}],
107          'Equipment list': {'Controller': 'IIIA',
108                             'Description': 'Extended PicoForce'},
109          'Force file list': {'Data length': '40960',
110                              'Date': '04:42:34 PM Tue Sep 11 2007',
111                              'Start context': 'FOL2',
112                              'Text:': None,
113                              'Version': '0x06120002'}}
114         """
115         info = {}
116         header_field = None
117         for line in file:
118             line = line.strip()
119             if line.startswith('\*File list end'):
120                 break
121             if line.startswith(r'\*'):
122                 header_field = line[len(r'\*'):]
123                 if header_field in info:
124                     if isinstance(info[header_field], list):
125                         info[header_field].append({}) # >=3rd appearance
126                     else: # Second appearance
127                         info[header_field] = [info[header_field], {}]
128                 else: # First appearance
129                     info[header_field] = {}
130             else:
131                 assert line.startswith('\\'), line
132                 fields = line[len('\\'):].split(': ', 1)
133                 key = fields[0]
134                 if len(fields) == 1: # fields = [key]
135                     value = None
136                 else: # fields = [key, value]
137                     value = fields[1]
138                 if isinstance(info[header_field], list): # >=2nd header_field
139                     target_dict = info[header_field][-1]
140                 else: # first appearance of header_field
141                     target_dict = info[header_field]
142                 if key in target_dict and target_dict[key] != value:
143                     raise NotImplementedError(
144                         'Overwriting %s: %s -> %s'
145                         % (key, target_dict[key], value))
146                 target_dict[key] = value
147         return (info)
148
149     def _check_version(self, info):
150         """Ensure the input file is a version we understand.
151
152         Otherwise, raise `ValueError`.
153         """
154         version = info['Force file list'].get('Version', None)
155         if version not in ['0x06120002']:
156             raise ValueError(
157                 '%s file version %s not supported (yet!)\n%s'
158                 % (self.name, version,
159                    pprint.pformat(info['Force file list'])))
160
161     def _read_data_path(self, path, info):
162         """Read curve data from the PicoForce file at `path`.
163
164         See :meth:`._read_data_file`.
165         """
166         f = file(path, 'rb')
167         data = self._read_data_file(f, info)
168         f.close()
169         return data
170
171     def _read_data_file(self, file, info):
172         file.seek(0)
173         traces = self._extract_traces(buffer(file.read()), info)
174         deflection,z_piezo,deflection_B = traces
175         self._validate_traces(z_piezo, deflection, deflection_B)
176         L = len(deflection)
177         approach = self._extract_block(
178             info, z_piezo, deflection, 0, L/2, 'approach')
179         retract = self._extract_block(
180             info, z_piezo, deflection, L/2, L, 'retract')
181         data = [approach, retract]
182         return data
183
184     def _extract_traces(self, buffer, info):
185         """Extract each of the three vector blocks in a PicoForce file.
186         
187         The blocks are:
188
189         * Deflection input
190         * Z piezo sensor input
191         * Deflection again?
192
193         And their headers are marked with 'Ciao force image list'.
194         """
195         traces = [] 
196         for image in info['Ciao force image list']:
197             offset = int(image['Data offset'])
198             length = int(image['Data length'])
199             sample_size = int(image['Bytes/pixel'])
200             rows = length / sample_size
201             if sample_size != 2:
202                 raise NotImplementedError('Size: %s' % sample_size)
203             d = curve.Data(
204                 shape=(rows),
205                 dtype=numpy.int16,
206                 buffer=buffer,
207                 offset=offset,
208                 info=image,
209                 )
210             traces.append(d)
211         return traces
212
213     def _validate_traces(self, z_piezo, deflection, deflection_B):
214         key = 'Spring Constant'
215         spring_constant = z_piezo.info[key]
216         for trace in [deflection, deflection_B]:
217             if trace.info[key] != spring_constant:
218                 raise NotImplementedError(
219                     'spring constant missmatch: %s != %s'
220                     % (spring_constant, trace.info[key]))
221         if max(abs(deflection_B[:-1]-deflection[:-1])) != 0:
222             raise NotImplementedError('trace 0 != trace 2')
223         if len(z_piezo) != len(deflection):
224             raise ValueError('Trace length missmatch: %d != %d'
225                              % (len(z_piezo), len(deflection)))
226
227     def _extract_block(self, info, z_piezo, deflection, start, stop, name):
228         block = curve.Data(
229             shape=(stop-start, 2),
230             dtype=numpy.float)
231         block[:,0] = z_piezo[start:stop]
232         block[:,1] = deflection[start:stop]
233         block.info = self._translate_block_info(
234             info, z_piezo.info, deflection.info, name)
235         block = self._scale_block(block)
236         return block
237
238     def _translate_block_info(self, info, z_piezo_info, deflection_info, name):
239         ret = {
240             'name':name,
241             'raw info':info,
242             'raw z piezo info': z_piezo_info,
243             'raw deflection info': deflection_info,
244             'spring constant (N/m)':float(z_piezo_info['Spring Constant'])
245             }
246
247         t = info['Force file list']['Date'] # 04:42:34 PM Tue Sep 11 2007
248         ret['time'] = time.strptime(t, '%I:%M:%S %p %a %b %d %Y')
249
250         type_re = re.compile('S \[(\w*)\] "([\w\s]*)"')
251         match = type_re.match(z_piezo_info['@4:Image Data'])
252         assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
253             'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
254         ret['columns'] = [match.group(2)]
255         match = type_re.match(deflection_info['@4:Image Data'])
256         assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
257             'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
258         ret['columns'].append(match.group(2))
259         assert ret['columns'] == ['Z sensor', 'Deflection'], \
260             'Unexpected columns: %s' % ret['columns']
261         ret['columns'] = ['z piezo (m)', 'deflection (m)']
262
263         volt_re = re.compile(
264             'V \[Sens. (\w*)\] \(([.0-9]*) V/LSB\) ([.0-9]*) V')
265         match = volt_re.match(z_piezo_info['@4:Z scale'])
266         assert match.group(1) == 'ZSensorSens', z_piezo_info['@4:Z scale']
267         ret['z piezo sensitivity (V/bit)'] = float(match.group(2))
268         ret['z piezo range (V)'] = float(match.group(3))
269         ret['z piezo offset (V)'] = 0.0
270         # offset assumed if raw data is signed...
271
272         match = volt_re.match(deflection_info['@4:Z scale'])
273         assert match.group(1) == 'DeflSens', z_piezo_info['@4:Z scale']
274         ret['deflection sensitivity (V/bit)'] = float(match.group(2))
275         ret['deflection range (V)'] = float(match.group(3))
276         ret['deflection offset (V)'] = 0.0
277         # offset assumed if raw data is signed...
278
279         nm_sens_re = re.compile('V ([.0-9]*) nm/V')
280         match = nm_sens_re.match(info['Scanner list']['@Sens. Zsens'])
281         ret['z piezo sensitivity (m/V)'] = float(match.group(1))*1e-9
282
283         match = nm_sens_re.match(info['Ciao scan list']['@Sens. DeflSens'])
284         ret['deflection sensitivity (m/V)'] = float(match.group(1))*1e-9
285
286         match = volt_re.match(info['Ciao force list']['@Z scan start'])
287         ret['z piezo scan (V/bit)'] = float(match.group(2))
288         ret['z piezo scan start (V)'] = float(match.group(3))
289
290         match = volt_re.match(info['Ciao force list']['@Z scan size'])
291         ret['z piezo scan size (V)'] = float(match.group(3))
292
293         const_re = re.compile('C \[([:\w\s]*)\] ([.0-9]*)')
294         match = const_re.match(z_piezo_info['@Z magnify'])
295         assert match.group(1) == '4:Z scale', match.group(1)
296         ret['z piezo magnification'] = match.group(2)
297
298         match = volt_re.match(z_piezo_info['@4:Z scale'])
299         assert match.group(1) == 'ZSensorSens', match.group(1)
300         ret['z piezo scale (V/bit)'] = float(match.group(2))
301         ret['z piezo scale (V)'] = float(match.group(3))
302
303         match = volt_re.match(z_piezo_info['@4:Ramp size'])
304         assert match.group(1) == 'Zsens', match.group(1)
305         ret['z piezo ramp size (V/bit)'] = float(match.group(2))
306         ret['z piezo ramp size (V)'] = float(match.group(3))
307
308         match = volt_re.match(z_piezo_info['@4:Ramp offset'])
309         assert match.group(1) == 'Zsens', match.group(1)
310         ret['z piezo ramp offset (V/bit)'] = float(match.group(2))
311         ret['z piezo ramp offset (V)'] = float(match.group(3))
312         
313         # Unaccounted for:
314         #   Samps*
315         
316         return ret
317
318     def _scale_block(self, data):
319         """Convert the block from its native format to a `numpy.float`
320         array in SI units.
321         """
322         ret = curve.Data(
323             shape=data.shape,
324             dtype=numpy.float,
325             )
326         info = data.info
327         ret.info = info
328         ret.info['raw-data'] = data # store the raw data
329         data.info = {} # break circular reference info <-> data
330
331         z_col = info['columns'].index('z piezo (m)')
332         d_col = info['columns'].index('deflection (m)')
333
334         # Leading '-' because Veeco's z increases towards the surface
335         # (positive indentation), but it makes more sense to me to
336         # have it inzrease away from the surface (positive
337         # separation).
338         ret[:,z_col] = -(
339             (data[:,z_col].astype(ret.dtype)
340              * info['z piezo sensitivity (V/bit)']
341              - info['z piezo offset (V)'])
342             * info['z piezo sensitivity (m/V)']
343             )
344
345         ret[:,d_col] = (
346             (data[:,d_col]
347              * info['deflection sensitivity (V/bit)']
348              - info['deflection offset (V)'])
349             * info['deflection sensitivity (m/V)']
350             )
351
352         return ret
353