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 modify it
8 # under the terms of the GNU Lesser General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # Hooke is distributed in the hope that it will be useful, but WITHOUT
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
15 # 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.
31 from .. import curve as curve # this module defines data containers.
32 from .. import experiment as experiment # this module defines expt. types
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):
45 if os.path.isdir(path):
51 return header[2:17] == 'Force file list'
53 def read(self, path, info=None):
54 info = self._read_header_path(path)
55 self._check_version(info)
56 data = self._read_data_path(path, info)
57 info['filetype'] = self.name
58 info['experiment'] = experiment.VelocityClamp
61 def _read_header_path(self, path):
62 """Read curve information from the PicoForce file at `path`.
64 See :meth:`._read_header_file`.
66 return self._read_header_file(file(path, 'rb'))
68 def _read_header_file(self, file):
69 r"""Read curve information from a PicoForce file.
71 Return a dict of dicts representing the information. If a
72 field is repeated multiple times, it's value is replaced by a
73 list of the values for each occurence.
80 >>> f = StringIO.StringIO('\r\n'.join([
81 ... '\*Force file list',
82 ... '\Version: 0x06120002',
83 ... '\Date: 04:42:34 PM Tue Sep 11 2007',
84 ... '\Start context: FOL2',
85 ... '\Data length: 40960',
87 ... '\*Equipment list',
88 ... '\Description: Extended PicoForce',
89 ... '\Controller: IIIA',
90 ... '\*Ciao force image list',
91 ... '\Data offset: 40960',
92 ... '\Data length: 8192',
93 ... '\*Ciao force image list',
94 ... '\Data offset: 49152',
95 ... '\Data length: 8192',
96 ... '\*Ciao force image list',
97 ... '\Data offset: 57344',
98 ... '\Data length: 8192',
100 >>> p = PicoForceDriver()
101 >>> d = p._read_header_file(f)
102 >>> pprint.pprint(d, width=60)
103 {'Ciao force image list': [{'Data length': '8192',
104 'Data offset': '40960'},
105 {'Data length': '8192',
106 'Data offset': '49152'},
107 {'Data length': '8192',
108 'Data offset': '57344'}],
109 'Equipment list': {'Controller': 'IIIA',
110 'Description': 'Extended PicoForce'},
111 'Force file list': {'Data length': '40960',
112 'Date': '04:42:34 PM Tue Sep 11 2007',
113 'Start context': 'FOL2',
115 'Version': '0x06120002'}}
121 if line.startswith('\*File list end'):
123 if line.startswith(r'\*'):
124 header_field = line[len(r'\*'):]
125 if header_field in info:
126 if isinstance(info[header_field], list):
127 info[header_field].append({}) # >=3rd appearance
128 else: # Second appearance
129 info[header_field] = [info[header_field], {}]
130 else: # First appearance
131 info[header_field] = {}
133 assert line.startswith('\\'), line
134 fields = line[len('\\'):].split(': ', 1)
136 if len(fields) == 1: # fields = [key]
138 else: # fields = [key, value]
140 if isinstance(info[header_field], list): # >=2nd header_field
141 target_dict = info[header_field][-1]
142 else: # first appearance of header_field
143 target_dict = info[header_field]
144 if key in target_dict and target_dict[key] != value:
145 raise NotImplementedError(
146 'Overwriting %s: %s -> %s'
147 % (key, target_dict[key], value))
148 target_dict[key] = value
151 def _check_version(self, info):
152 """Ensure the input file is a version we understand.
154 Otherwise, raise `ValueError`.
156 version = info['Force file list'].get('Version', None)
157 if version not in ['0x06120002', '0x06130001', '0x07200000']:
158 raise NotImplementedError(
159 '%s file version %s not supported (yet!)\n%s'
160 % (self.name, version,
161 pprint.pformat(info['Force file list'])))
163 def _read_data_path(self, path, info):
164 """Read curve data from the PicoForce file at `path`.
166 See :meth:`._read_data_file`.
169 data = self._read_data_file(f, info)
173 def _read_data_file(self, file, info):
175 traces = self._extract_traces(buffer(file.read()), info)
176 self._validate_traces(
177 traces['Z sensor'], traces['Deflection'])
178 L = len(traces['Deflection'])
179 approach = self._extract_block(
180 info, traces['Z sensor'], traces['Deflection'], 0, L/2, 'approach')
181 retract = self._extract_block(
182 info, traces['Z sensor'], traces['Deflection'], L/2, L, 'retract')
183 data = [approach, retract]
186 def _extract_traces(self, buffer, info):
187 """Extract each of the three vector blocks in a PicoForce file.
189 The blocks are (in variable order):
191 * Z piezo sensor input
195 And their headers are marked with 'Ciao force image list'.
198 version = info['Force file list']['Version']
199 type_re = re.compile('S \[(\w*)\] "([\w\s.]*)"')
200 for image in info['Ciao force image list']:
201 offset = int(image['Data offset'])
202 length = int(image['Data length'])
203 sample_size = int(image['Bytes/pixel'])
205 raise NotImplementedError('Size: %s' % sample_size)
206 rows = length / sample_size
214 if version in ['0x06120002', '0x06130001']:
215 match = type_re.match(image['@4:Image Data'])
216 assert match != None, 'Bad regexp for %s, %s' \
217 % ('@4:Image Data', image['@4:Image Data'])
218 if version == '0x06130001' and match.group(1) == 'ZLowVoltage':
219 assert match.group(2) == 'Low Voltage Z', \
220 'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
222 assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
223 'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
224 tname = match.group(2)
226 assert version == '0x07200000', version
227 match = type_re.match(image['@4:Image Data'])
228 assert match != None, 'Bad regexp for %s, %s' \
229 % ('@4:Image Data', image['@4:Image Data'])
230 if match.group(1) == 'PulseFreq1':
231 assert match.group(2) == 'Freq. 1', match.group(2)
233 assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
234 'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
235 tname = match.group(2)
236 if tname == 'Freq. 1': # Normalize trace names between versions
238 elif tname == 'Deflection Error':
241 #d.tofile('%s-2.dat' % tname, sep='\n')
242 tname = self._replace_name(tname, d, traces, info)
244 continue # Don't replace anything
246 #d.tofile('%s.dat' % tname, sep='\n')
251 def _validate_traces(self, z_piezo, deflection):
252 if len(z_piezo) != len(deflection):
253 raise ValueError('Trace length missmatch: %d != %d'
254 % (len(z_piezo), len(deflection)))
256 def _extract_block(self, info, z_piezo, deflection, start, stop, name):
258 shape=(stop-start, 2),
260 block[:,0] = z_piezo[start:stop]
261 block[:,1] = deflection[start:stop]
262 block.info = self._translate_block_info(
263 info, z_piezo.info, deflection.info, name)
264 block.info['columns'] = ['z piezo (m)', 'deflection (m)']
265 block = self._scale_block(block)
268 def _replace_name(self, trace_name, trace, traces, info):
269 """Determine if a duplicate trace name should replace an earlier trace.
271 Return the target trace name if it should be replaced by the
272 new trace, or `None` if the new trace should be dropped.
275 #target = traces[trace_name]
277 ## Compare the info dictionaries for each trace
278 #ik = set(trace.info.keys())
279 #ok = set(traces[trace_name].info.keys())
280 #if ik != ok: # Ensure we have the same set of keys for both traces
281 # msg.append('extra keys: %s, missing keys %s' % (ik-ok, ok-ik))
283 # # List keys we *require* to change between traces
284 # variable_keys = ['Data offset', 'X data type'] # TODO: What is X data type?
285 # for key in trace.info.keys():
286 # if key in variable_keys:
287 # if target.info[key] == trace.info[key]:
288 # msg.append('constant %s (%s == %s)'
289 # % (key, target.info[key], trace.info[key]))
291 # if target.info[key] != trace.info[key]:
292 # msg.append('variable %s (%s != %s)'
293 # % (key, target.info[key], trace.info[key]))
295 #if not (traces[trace_name] == trace).all():
296 # msg.append('data difference')
298 # raise NotImplementedError(
299 # 'Missmatched duplicate traces for %s: %s'
300 # % (trace_name, ', '.join(msg)))
302 log = logging.getLogger('hooke')
303 for name,t in traces.items():
304 if (t == trace).all():
305 log.debug('replace %s with %s-2' % (name, trace_name))
306 return name # Replace this identical dataset.
307 log.debug('store %s-2 as Other' % (trace_name))
311 def _translate_block_info(self, info, z_piezo_info, deflection_info, name):
312 version = info['Force file list']['Version']
316 'raw z piezo info': z_piezo_info,
317 'raw deflection info': deflection_info,
318 'spring constant (N/m)': float(z_piezo_info['Spring Constant']),
321 t = info['Force file list']['Date'] # 04:42:34 PM Tue Sep 11 2007
322 ret['time'] = time.strptime(t, '%I:%M:%S %p %a %b %d %Y')
324 volt_re = re.compile(
325 'V \[Sens. ([\w\s.]*)\] \(([.0-9]*) V/LSB\) (-?[.0-9]*) V')
327 'V \[Sens. ([\w\s.]*)\] \(([.0-9]*) kHz/LSB\) (-?[.0-9]*) kHz')
328 if version in ['0x06120002', '0x06130001']:
329 match = volt_re.match(z_piezo_info['@4:Z scale'])
330 assert match != None, 'Bad regexp for %s, %s' \
331 % ('@4:Z scale', z_piezo_info['@4:Z scale'])
332 assert match.group(1) == 'ZSensorSens', z_piezo_info['@4:Z scale']
334 assert version == '0x07200000', version
335 match = hz_re.match(z_piezo_info['@4:Z scale'])
336 assert match != None, 'Bad regexp for %s, %s' \
337 % ('@4:Z scale', z_piezo_info['@4:Z scale'])
338 assert match.group(1) == 'Freq. 1', z_piezo_info['@4:Z scale']
339 ret['z piezo sensitivity (V/bit)'] = float(match.group(2))
340 ret['z piezo range (V)'] = float(match.group(3))
341 ret['z piezo offset (V)'] = 0.0
342 # offset assumed if raw data is signed...
344 match = volt_re.match(deflection_info['@4:Z scale'])
345 assert match != None, 'Bad regexp for %s, %s' \
346 % ('@4:Z scale', deflection_info['@4:Z scale'])
347 assert match.group(1) == 'DeflSens', z_piezo_info['@4:Z scale']
348 ret['deflection sensitivity (V/bit)'] = float(match.group(2))
349 ret['deflection range (V)'] = float(match.group(3))
350 ret['deflection offset (V)'] = 0.0
351 # offset assumed if raw data is signed...
353 nm_sens_re = re.compile('V ([.0-9]*) nm/V')
354 if version in ['0x06120002', '0x06130001']:
355 match = nm_sens_re.match(info['Ciao scan list']['@Sens. ZSensorSens'])
356 assert match != None, 'Bad regexp for %s/%s, %s' \
357 % ('Ciao scan list', '@Sens. ZSensorSens',
358 info['Ciao scan list']['@Sens. ZSensorSens'])
360 assert version == '0x07200000', version
361 match = nm_sens_re.match(info['Ciao scan list']['@Sens. ZsensSens'])
362 assert match != None, 'Bad regexp for %s/%s, %s' \
363 % ('Ciao scan list', '@Sens. ZsensSens',
364 info['Ciao scan list']['@Sens. ZsensSens'])
365 ret['z piezo sensitivity (m/V)'] = float(match.group(1))*1e-9
367 match = nm_sens_re.match(info['Ciao scan list']['@Sens. DeflSens'])
368 assert match != None, 'Bad regexp for %s/%s, %s' \
369 % ('Ciao scan list', '@Sens. DeflSens', info['Ciao scan list']['@Sens. DeflSens'])
370 ret['deflection sensitivity (m/V)'] = float(match.group(1))*1e-9
372 match = volt_re.match(info['Ciao force list']['@Z scan start'])
373 assert match != None, 'Bad regexp for %s/%s, %s' \
374 % ('Ciao force list', '@Z scan start', info['Ciao force list']['@Z scan start'])
375 ret['z piezo scan (V/bit)'] = float(match.group(2))
376 ret['z piezo scan start (V)'] = float(match.group(3))
378 match = volt_re.match(info['Ciao force list']['@Z scan size'])
379 assert match != None, 'Bad regexp for %s/%s, %s' \
380 % ('Ciao force list', '@Z scan size', info['Ciao force list']['@Z scan size'])
381 ret['z piezo scan size (V)'] = float(match.group(3))
383 const_re = re.compile('C \[([:\w\s]*)\] ([.0-9]*)')
384 match = const_re.match(z_piezo_info['@Z magnify'])
385 assert match != None, 'Bad regexp for %s, %s' \
386 % ('@Z magnify', info['@Z magnify'])
387 assert match.group(1) == '4:Z scale', match.group(1)
388 ret['z piezo gain'] = float(match.group(2))
390 if version in ['0x06120002', '0x06130001']:
391 match = volt_re.match(z_piezo_info['@4:Z scale'])
392 assert match != None, 'Bad regexp for %s, %s' \
393 % ('@4:Z scale', info['@4:Z scale'])
394 assert match.group(1) == 'ZSensorSens', match.group(1)
395 ret['z piezo sensitivity (V/bit)'] = float(match.group(2))
396 ret['z piezo range (V)'] = float(match.group(3))
398 assert version == '0x07200000', version
401 match = volt_re.match(z_piezo_info['@4:Ramp size'])
402 assert match != None, 'Bad regexp for %s, %s' \
403 % ('@4:Ramp size', info['@4:Ramp size'])
404 assert match.group(1) == 'Zsens', match.group(1)
405 ret['z piezo ramp size (V/bit)'] = float(match.group(2))
406 ret['z piezo ramp size (V)'] = float(match.group(3))
408 match = volt_re.match(z_piezo_info['@4:Ramp offset'])
409 assert match != None, 'Bad regexp for %s, %s' \
410 % ('@4:Ramp offset', info['@4:Ramp offset'])
411 assert match.group(1) == 'Zsens', match.group(1)
412 ret['z piezo ramp offset (V/bit)'] = float(match.group(2))
413 ret['z piezo ramp offset (V)'] = float(match.group(3))
420 def _scale_block(self, data):
421 """Convert the block from its native format to a `numpy.float`
430 ret.info['raw data'] = data # store the raw data
431 data.info = {} # break circular reference info <-> data
433 z_col = info['columns'].index('z piezo (m)')
434 d_col = info['columns'].index('deflection (m)')
436 # Leading '-' because Veeco's z increases towards the surface
437 # (positive indentation), but it makes more sense to me to
438 # have it increase away from the surface (positive
441 (data[:,z_col].astype(ret.dtype)
442 * info['z piezo sensitivity (V/bit)']
443 - info['z piezo offset (V)'])
444 * info['z piezo gain']
445 * info['z piezo sensitivity (m/V)']
448 # Leading '-' because deflection voltage increases as the tip
449 # moves away from the surface, but it makes more sense to me
450 # to have it increase as it moves toward the surface (positive
451 # tension on the protein chain).
454 * info['deflection sensitivity (V/bit)']
455 - info['deflection offset (V)'])
456 * info['deflection sensitivity (m/V)']