1 # Copyright (C) 2006-2010 Alberto Gomez-Casado
2 # Massimo Sandal <devicerandom@gmail.com>
3 # W. Trevor King <wking@drexel.edu>
5 # This file is part of Hooke.
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.
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.
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/>.
21 """Driver for Veeco PicoForce force spectroscopy files.
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
36 __version__='0.0.0.20100516'
38 class PicoForceDriver (Driver):
39 """Handle Veeco Picoforce force spectroscopy files.
42 super(PicoForceDriver, self).__init__(name='picoforce')
44 def is_me(self, path):
49 return header[2:17] == 'Force file list'
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
59 def _read_header_path(self, path):
60 """Read curve information from the PicoForce file at `path`.
62 See :meth:`._read_header_file`.
64 return self._read_header_file(file(path, 'rb'))
66 def _read_header_file(self, file):
67 r"""Read curve information from a PicoForce file.
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.
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',
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',
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',
113 'Version': '0x06120002'}}
119 if line.startswith('\*File list end'):
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] = {}
131 assert line.startswith('\\'), line
132 fields = line[len('\\'):].split(': ', 1)
134 if len(fields) == 1: # fields = [key]
136 else: # fields = [key, value]
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
149 def _check_version(self, info):
150 """Ensure the input file is a version we understand.
152 Otherwise, raise `ValueError`.
154 version = info['Force file list'].get('Version', None)
155 if version not in ['0x06120002', '0x06130001', '0x07200000']:
156 raise NotImplementedError(
157 '%s file version %s not supported (yet!)\n%s'
158 % (self.name, version,
159 pprint.pformat(info['Force file list'])))
161 def _read_data_path(self, path, info):
162 """Read curve data from the PicoForce file at `path`.
164 See :meth:`._read_data_file`.
167 data = self._read_data_file(f, info)
171 def _read_data_file(self, file, info):
173 traces = self._extract_traces(buffer(file.read()), info)
174 self._validate_traces(
175 traces['Z sensor'], traces['Deflection'])
176 L = len(traces['Deflection'])
177 approach = self._extract_block(
178 info, traces['Z sensor'], traces['Deflection'], 0, L/2, 'approach')
179 retract = self._extract_block(
180 info, traces['Z sensor'], traces['Deflection'], L/2, L, 'retract')
181 data = [approach, retract]
184 def _extract_traces(self, buffer, info):
185 """Extract each of the three vector blocks in a PicoForce file.
187 The blocks are (in variable order):
189 * Z piezo sensor input
193 And their headers are marked with 'Ciao force image list'.
196 version = info['Force file list']['Version']
197 type_re = re.compile('S \[(\w*)\] "([\w\s.]*)"')
198 for image in info['Ciao force image list']:
199 offset = int(image['Data offset'])
200 length = int(image['Data length'])
201 sample_size = int(image['Bytes/pixel'])
203 raise NotImplementedError('Size: %s' % sample_size)
204 rows = length / sample_size
212 if version in ['0x06120002', '0x06130001']:
213 match = type_re.match(image['@4:Image Data'])
214 assert match != None, 'Bad regexp for %s, %s' \
215 % ('@4:Image Data', image['@4:Image Data'])
216 assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
217 'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
218 tname = match.group(2)
220 assert version == '0x07200000', version
221 match = type_re.match(image['@4:Image Data'])
222 assert match != None, 'Bad regexp for %s, %s' \
223 % ('@4:Image Data', image['@4:Image Data'])
224 if match.group(1) == 'PulseFreq1':
225 assert match.group(2) == 'Freq. 1', match.group(2)
227 assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
228 'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
229 tname = match.group(2)
230 if tname == 'Freq. 1': # Normalize trace names between versions
232 elif tname == 'Deflection Error':
237 if (traces[tname].info != image):
238 ik = set(image.keys())
239 ok = set(traces[tname].info.keys())
241 mmsg = 'extra keys: %s, missing keys %s' % (ik-ok, ok-ik)
244 for key in image.keys():
245 if image[key] != traces[tname].info[key]:
248 % (key, image[key], traces[tname].info[key]))
249 mmsg = ', '.join(mmsg)
250 msg = 'info difference: %s' % mmsg
251 elif not (traces[tname] == d).all():
252 msg = 'data difference'
254 raise NotImplementedError(
255 'Missmatched duplicate traces for %s: %s'
260 def _validate_traces(self, z_piezo, deflection):
261 if len(z_piezo) != len(deflection):
262 raise ValueError('Trace length missmatch: %d != %d'
263 % (len(z_piezo), len(deflection)))
265 def _extract_block(self, info, z_piezo, deflection, start, stop, name):
267 shape=(stop-start, 2),
269 block[:,0] = z_piezo[start:stop]
270 block[:,1] = deflection[start:stop]
271 block.info = self._translate_block_info(
272 info, z_piezo.info, deflection.info, name)
273 block.info['columns'] = ['z piezo (m)', 'deflection (m)']
274 block = self._scale_block(block)
277 def _translate_block_info(self, info, z_piezo_info, deflection_info, name):
278 version = info['Force file list']['Version']
282 'raw z piezo info': z_piezo_info,
283 'raw deflection info': deflection_info,
284 'spring constant (N/m)': float(z_piezo_info['Spring Constant']),
287 t = info['Force file list']['Date'] # 04:42:34 PM Tue Sep 11 2007
288 ret['time'] = time.strptime(t, '%I:%M:%S %p %a %b %d %Y')
290 volt_re = re.compile(
291 'V \[Sens. ([\w\s.]*)\] \(([.0-9]*) V/LSB\) (-?[.0-9]*) V')
293 'V \[Sens. ([\w\s.]*)\] \(([.0-9]*) kHz/LSB\) (-?[.0-9]*) kHz')
294 if version in ['0x06120002', '0x06130001']:
295 match = volt_re.match(z_piezo_info['@4:Z scale'])
296 assert match != None, 'Bad regexp for %s, %s' \
297 % ('@4:Z scale', z_piezo_info['@4:Z scale'])
298 assert match.group(1) == 'ZSensorSens', z_piezo_info['@4:Z scale']
300 assert version == '0x07200000', version
301 match = hz_re.match(z_piezo_info['@4:Z scale'])
302 assert match != None, 'Bad regexp for %s, %s' \
303 % ('@4:Z scale', z_piezo_info['@4:Z scale'])
304 assert match.group(1) == 'Freq. 1', z_piezo_info['@4:Z scale']
305 ret['z piezo sensitivity (V/bit)'] = float(match.group(2))
306 ret['z piezo range (V)'] = float(match.group(3))
307 ret['z piezo offset (V)'] = 0.0
308 # offset assumed if raw data is signed...
310 match = volt_re.match(deflection_info['@4:Z scale'])
311 assert match != None, 'Bad regexp for %s, %s' \
312 % ('@4:Z scale', deflection_info['@4:Z scale'])
313 assert match.group(1) == 'DeflSens', z_piezo_info['@4:Z scale']
314 ret['deflection sensitivity (V/bit)'] = float(match.group(2))
315 ret['deflection range (V)'] = float(match.group(3))
316 ret['deflection offset (V)'] = 0.0
317 # offset assumed if raw data is signed...
319 nm_sens_re = re.compile('V ([.0-9]*) nm/V')
320 match = nm_sens_re.match(info['Scanner list']['@Sens. Zsens'])
321 assert match != None, 'Bad regexp for %s/%s, %s' \
322 % ('Scanner list', '@Sens. Zsens', info['Scanner list']['@4:Z scale'])
323 ret['z piezo sensitivity (m/V)'] = float(match.group(1))*1e-9
325 match = nm_sens_re.match(info['Ciao scan list']['@Sens. DeflSens'])
326 assert match != None, 'Bad regexp for %s/%s, %s' \
327 % ('Ciao scan list', '@Sens. DeflSens', info['Ciao scan list']['@Sens. DeflSens'])
328 ret['deflection sensitivity (m/V)'] = float(match.group(1))*1e-9
330 match = volt_re.match(info['Ciao force list']['@Z scan start'])
331 assert match != None, 'Bad regexp for %s/%s, %s' \
332 % ('Ciao force list', '@Z scan start', info['Ciao force list']['@Z scan start'])
333 ret['z piezo scan (V/bit)'] = float(match.group(2))
334 ret['z piezo scan start (V)'] = float(match.group(3))
336 match = volt_re.match(info['Ciao force list']['@Z scan size'])
337 assert match != None, 'Bad regexp for %s/%s, %s' \
338 % ('Ciao force list', '@Z scan size', info['Ciao force list']['@Z scan size'])
339 ret['z piezo scan size (V)'] = float(match.group(3))
341 const_re = re.compile('C \[([:\w\s]*)\] ([.0-9]*)')
342 match = const_re.match(z_piezo_info['@Z magnify'])
343 assert match != None, 'Bad regexp for %s, %s' \
344 % ('@Z magnify', info['@Z magnify'])
345 assert match.group(1) == '4:Z scale', match.group(1)
346 ret['z piezo gain'] = float(match.group(2))
348 if version in ['0x06120002', '0x06130001']:
349 match = volt_re.match(z_piezo_info['@4:Z scale'])
350 assert match != None, 'Bad regexp for %s, %s' \
351 % ('@4:Z scale', info['@4:Z scale'])
352 assert match.group(1) == 'ZSensorSens', match.group(1)
353 ret['z piezo sensitivity (V/bit)'] = float(match.group(2))
354 ret['z piezo range (V)'] = float(match.group(3))
356 assert version == '0x07200000', version
359 match = volt_re.match(z_piezo_info['@4:Ramp size'])
360 assert match != None, 'Bad regexp for %s, %s' \
361 % ('@4:Ramp size', info['@4:Ramp size'])
362 assert match.group(1) == 'Zsens', match.group(1)
363 ret['z piezo ramp size (V/bit)'] = float(match.group(2))
364 ret['z piezo ramp size (V)'] = float(match.group(3))
366 match = volt_re.match(z_piezo_info['@4:Ramp offset'])
367 assert match != None, 'Bad regexp for %s, %s' \
368 % ('@4:Ramp offset', info['@4:Ramp offset'])
369 assert match.group(1) == 'Zsens', match.group(1)
370 ret['z piezo ramp offset (V/bit)'] = float(match.group(2))
371 ret['z piezo ramp offset (V)'] = float(match.group(3))
378 def _scale_block(self, data):
379 """Convert the block from its native format to a `numpy.float`
388 ret.info['raw data'] = data # store the raw data
389 data.info = {} # break circular reference info <-> data
391 z_col = info['columns'].index('z piezo (m)')
392 d_col = info['columns'].index('deflection (m)')
394 # Leading '-' because Veeco's z increases towards the surface
395 # (positive indentation), but it makes more sense to me to
396 # have it increase away from the surface (positive
399 (data[:,z_col].astype(ret.dtype)
400 * info['z piezo sensitivity (V/bit)']
401 - info['z piezo offset (V)'])
402 * info['z piezo gain']
403 * info['z piezo sensitivity (m/V)']
406 # Leading '-' because deflection voltage increases as the tip
407 # moves away from the surface, but it makes more sense to me
408 # to have it increase as it moves toward the surface (positive
409 # tension on the protein chain).
412 * info['deflection sensitivity (V/bit)']
413 - info['deflection offset (V)'])
414 * info['deflection sensitivity (m/V)']