Really hideous merge of Rolf Schmidt's code.
[hooke.git] / hooke / driver / picoforce.py
1 '''
2 libpicoforce.py
3
4 Library for interpreting Picoforce force spectroscopy files.
5
6 Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
7
8 This program is released under the GNU General Public License version 2.
9 '''
10
11 import re
12 import struct
13 from scipy import arange
14
15 #from .. import libhooke as lh
16 from .. import libhookecurve as lhc
17
18
19 __version__='0.0.0.20090923'
20
21
22 class DataChunk(list):
23     #Dummy class to provide ext and ret methods to the data list.
24
25     def ext(self):
26         halflen=(len(self)/2)
27         return self[0:halflen]
28
29     def ret(self):
30         halflen=(len(self)/2)
31         return self[halflen:]
32
33 class picoforceDriver(lhc.Driver):
34
35     #Construction and other special methods
36
37     def __init__(self,filename):
38         '''
39         constructor method
40         '''
41         filename = lh.get_file_path(filename)
42         self.textfile=file(filename)
43         self.binfile=file(filename,'rb')
44
45         #The 0,1,2 data chunks are:
46         #0: D (vs T)
47         #1: Z (vs T)
48         #2: D (vs Z)
49
50
51         self.filepath=filename
52         self.debug=False
53
54         self.filetype='picoforce'
55         self.experiment='smfs'
56
57
58     #Hidden methods. These are meant to be used only by API functions. If needed, however,
59     #they can be called just like API methods.
60
61     def _get_samples_line(self):
62         '''
63         Gets the samples per line parameters in the file, to understand trigger behaviour.
64         '''
65         self.textfile.seek(0)
66
67         samps_expr=re.compile(".*Samps")
68
69         samps_values=[]
70         for line in self.textfile.readlines():
71             if samps_expr.match(line):
72                 try:
73                     samps=int(line.split()[2]) #the third word splitted is the offset (in bytes)
74                     samps_values.append(samps)
75                 except:
76                     pass
77
78                 #We raise a flag for the fact we meet an offset, otherwise we would take spurious data length arguments.
79
80         return int(samps_values[0])
81
82     def _get_chunk_coordinates(self):
83         '''
84         This method gets the coordinates (offset and length) of a data chunk in our
85         Picoforce file.
86
87         It returns a list containing two tuples:
88         the first element of each tuple is the data_offset, the second is the corresponding
89         data size.
90
91         In near future probably each chunk will get its own data structure, with
92         offset, size, type, etc.
93         '''
94         self.textfile.seek(0)
95
96         offset_expr=re.compile(".*Data offset")
97         length_expr=re.compile(".*Data length")
98
99         data_offsets=[]
100         data_sizes=[]
101         flag_offset=0
102
103         for line in self.textfile.readlines():
104
105             if offset_expr.match(line):
106                 offset=int(line.split()[2]) #the third word splitted is the offset (in bytes)
107                 data_offsets.append(offset)
108                 #We raise a flag for the fact we meet an offset, otherwise we would take spurious data length arguments.
109                 flag_offset=1
110
111             #same for the data length
112             if length_expr.match(line) and flag_offset:
113                 size=int(line.split()[2])
114                 data_sizes.append(size)
115                 #Put down the offset flag until the next offset is met.
116                 flag_offset=0
117
118         return zip(data_offsets,data_sizes)
119
120     def _get_data_chunk(self,whichchunk):
121         '''
122         reads a data chunk and converts it in 16bit signed int.
123         '''
124         offset,size=self._get_chunk_coordinates()[whichchunk]
125
126
127         self.binfile.seek(offset)
128         raw_chunk=self.binfile.read(size)
129
130         my_chunk=[]
131         for data_position in range(0,len(raw_chunk),2):
132             data_unit_bytes=raw_chunk[data_position:data_position+2]
133             #The unpack function converts 2-bytes in a signed int ('h').
134             #we use output[0] because unpack returns a 1-value tuple, and we want the number only
135             data_unit=struct.unpack('h',data_unit_bytes)[0]
136             my_chunk.append(data_unit)
137
138         return DataChunk(my_chunk)
139
140     def _get_Zscan_info(self,index):
141         '''
142         gets the Z scan informations needed to interpret the data chunk.
143         These info come from the general section, BEFORE individual chunk headers.
144
145         By itself, the function will parse for three parameters.
146         (index) that tells the function what to return when called by
147         exposed API methods.
148         index=0 : returns Zscan_V_LSB
149         index=1 : returns Zscan_V_start
150         index=2 : returns Zscan_V_size
151         '''
152         self.textfile.seek(0)
153
154         ciaoforcelist_expr=re.compile(".*Ciao force")
155         zscanstart_expr=re.compile(".*@Z scan start")
156         zscansize_expr=re.compile(".*@Z scan size")
157
158         ciaoforce_flag=0
159         theline=0
160         for line in self.textfile.readlines():
161             if ciaoforcelist_expr.match(line):
162                 ciaoforce_flag=1 #raise a flag: zscanstart and zscansize params to read are later
163
164             if ciaoforce_flag and zscanstart_expr.match(line):
165                 raw_Zscanstart_line=line.split()
166
167             if ciaoforce_flag and zscansize_expr.match(line):
168                 raw_Zscansize_line=line.split()
169
170         Zscanstart_line=[]
171         Zscansize_line=[]
172         for itemscanstart,itemscansize in zip(raw_Zscanstart_line,raw_Zscansize_line):
173             Zscanstart_line.append(itemscanstart.strip('[]()'))
174             Zscansize_line.append(itemscansize.strip('[]()'))
175
176         Zscan_V_LSB=float(Zscanstart_line[6])
177         Zscan_V_start=float(Zscanstart_line[8])
178         Zscan_V_size=float(Zscansize_line[8])
179
180         return (Zscan_V_LSB,Zscan_V_start,Zscan_V_size)[index]
181
182     def _get_Z_magnify_scale(self,whichchunk):
183         '''
184         gets Z scale and Z magnify
185         Here we get Z scale/magnify from the 'whichchunk' only.
186         whichchunk=1,2,3
187         TODO: make it coherent with data_chunks syntaxis (0,1,2)
188
189         In future, should we divide the *file* itself into chunk descriptions and gain
190         true chunk data structures?
191         '''
192         self.textfile.seek(0)
193
194         z_scale_expr=re.compile(".*@4:Z scale")
195         z_magnify_expr=re.compile(".*@Z magnify")
196
197         ramp_size_expr=re.compile(".*@4:Ramp size")
198         ramp_offset_expr=re.compile(".*@4:Ramp offset")
199
200         occurrences=0
201         found_right=0
202
203
204         for line in self.textfile.readlines():
205             if z_magnify_expr.match(line):
206                 occurrences+=1
207                 if occurrences==whichchunk:
208                     found_right=1
209                     raw_z_magnify_expression=line.split()
210                 else:
211                     found_right=0
212
213             if found_right and z_scale_expr.match(line):
214                 raw_z_scale_expression=line.split()
215             if found_right and ramp_size_expr.match(line):
216                 raw_ramp_size_expression=line.split()
217             if found_right and ramp_offset_expr.match(line):
218                 raw_ramp_offset_expression=line.split()
219
220         return float(raw_z_magnify_expression[5]),float(raw_z_scale_expression[7]), float(raw_ramp_size_expression[7]), float(raw_ramp_offset_expression[7]), float(raw_z_scale_expression[5][1:])
221
222
223     #Exposed APIs.
224     #These are the methods that are meant to be called from external apps.
225
226     def LSB_to_volt(self,chunknum,voltrange=20):
227         '''
228         Converts the LSB data of a given chunk (chunknum=0,1,2) in volts.
229         First step to get the deflection and the force.
230
231         SYNTAXIS:
232         item.LSB_to_volt(chunknum, [voltrange])
233
234         The voltrange is by default set to 20 V.
235         '''
236         return DataChunk([((float(lsb)/65535)*voltrange) for lsb in self.data_chunks[chunknum]])
237
238     def LSB_to_deflection(self,chunknum,deflsensitivity=None,voltrange=20):
239         '''
240         Converts the LSB data in deflection (meters).
241
242         SYNTAXIS:
243         item.LSB_to_deflection(chunknum, [deflection sensitivity], [voltrange])
244
245         chunknum is the chunk you want to parse (0,1,2)
246
247         The deflection sensitivity by default is the one parsed from the file.
248         The voltrange is by default set to 20 V.
249         '''
250         if deflsensitivity is None:
251             deflsensitivity=self.get_deflection_sensitivity()
252
253         lsbvolt=self.LSB_to_volt(chunknum)
254         return DataChunk([volt*deflsensitivity for volt in lsbvolt])
255
256     def deflection(self):
257         '''
258         Get the actual force curve deflection.
259         '''
260         deflchunk= self.LSB_to_deflection(2)
261         return deflchunk.ext(),deflchunk.ret()
262
263     def LSB_to_force(self,chunknum=2,Kspring=None,voltrange=20):
264         '''
265         Converts the LSB data (of deflection) in force (newtons).
266
267         SYNTAXIS:
268         item.LSB_to_force([chunknum], [spring constant], [voltrange])
269
270         chunknum is the chunk you want to parse (0,1,2). The chunk used is by default 2.
271         The spring constant by default is the one parsed from the file.
272         The voltrange is by default set to 20 V.
273         '''
274         if Kspring is None:
275             Kspring=self.get_spring_constant()
276
277         lsbdefl=self.LSB_to_deflection(chunknum)
278         return DataChunk([(meter*Kspring) for meter in lsbdefl])
279
280     def get_Zscan_V_start(self):
281         return self._get_Zscan_info(1)
282
283     def get_Zscan_V_size(self):
284         return self._get_Zscan_info(2)
285
286     def get_Z_scan_sensitivity(self):
287         '''
288         gets Z sensitivity
289         '''
290         self.textfile.seek(0)
291
292         z_sensitivity_expr=re.compile(".*@Sens. Zsens")
293
294         for line in self.textfile.readlines():
295             if z_sensitivity_expr.match(line):
296                 z_sensitivity=float(line.split()[3])
297         #return it in SI units (that is: m/V, not nm/V)
298         return z_sensitivity*(10**(-9))
299
300     def get_Z_magnify(self,whichchunk):
301         '''
302         Gets the Z magnify factor. Normally it is 1, unknown exact use as of 2006-01-13
303         '''
304         return self._get_Z_magnify_scale(whichchunk)[0]
305
306     def get_Z_scale(self,whichchunk):
307         '''
308         Gets the Z scale.
309         '''
310         return self._get_Z_magnify_scale(whichchunk)[1]
311
312     def get_ramp_size(self,whichchunk):
313         '''
314         Gets the -user defined- ramp size
315         '''
316         return self._get_Z_magnify_scale(whichchunk)[2]
317
318     def get_ramp_offset(self,whichchunk):
319         '''
320         Gets the ramp offset
321         '''
322         return self._get_Z_magnify_scale(whichchunk)[3]
323
324     def get_Z_scale_LSB(self,whichchunk):
325         '''
326         Gets the LSB-to-volt conversion factor of the Z data.
327         (so called hard-scale in the Nanoscope documentation)
328
329         '''
330         return self._get_Z_magnify_scale(whichchunk)[4]
331
332     def get_deflection_sensitivity(self):
333         '''
334         gets deflection sensitivity
335         '''
336         self.textfile.seek(0)
337
338         def_sensitivity_expr=re.compile(".*@Sens. DeflSens")
339
340         for line in self.textfile.readlines():
341             if def_sensitivity_expr.match(line):
342                 def_sensitivity=float(line.split()[3])
343                 break
344         #return it in SI units (that is: m/V, not nm/V)
345         return def_sensitivity*(10**(-9))
346
347     def get_spring_constant(self):
348         '''
349         gets spring constant.
350         We actually find *three* spring constant values, one for each data chunk (F/t, Z/t, F/z).
351         They are normally all equal, but we retain all three for future...
352         '''
353         self.textfile.seek(0)
354
355         springconstant_expr=re.compile(".*Spring Constant")
356
357         constants=[]
358
359         for line in self.textfile.readlines():
360             if springconstant_expr.match(line):
361                 constants.append(float(line.split()[2]))
362
363         return constants[0]
364
365     def get_Zsensorsens(self):
366         '''
367         gets Zsensorsens for Z data.
368
369         This is the sensitivity needed to convert the LSB data in nanometers for the Z-vs-T data chunk.
370         '''
371         self.textfile.seek(0)
372
373         zsensorsens_expr=re.compile(".*Sens. ZSensorSens")
374
375         for line in self.textfile.readlines():
376             if zsensorsens_expr.match(line):
377                 zsensorsens_raw_expression=line.split()
378                 #we must take only first occurrence, so we exit from the cycle immediately
379                 break
380
381         return (float(zsensorsens_raw_expression[3]))*(10**(-9))
382
383     def Z_data(self):
384         '''
385         returns converted ext and ret Z curves.
386         They're on the second chunk (Z vs t).
387         '''
388         #Zmagnify_zt=self.get_Z_magnify(2)
389         #Zscale_zt=self.get_Z_scale(2)
390         Zlsb_zt=self.get_Z_scale_LSB(2)
391         #rampsize_zt=self.get_ramp_size(2)
392         #rampoffset_zt=self.get_ramp_offset(2)
393         zsensorsens=self.get_Zsensorsens()
394
395         '''
396         The magic formula that converts the Z data is:
397
398         meters = LSB * V_lsb_conversion_factor * ZSensorSens
399         '''
400
401         #z_curves=[item*Zlsb_zt*zsensorsens for item in self.data_chunks[1].pair['ext']],[item*Zlsb_zt*zsensorsens for item in self.data_chunks[1].pair['ret']]
402         z_curves=[item*Zlsb_zt*zsensorsens for item in self.data_chunks[1].ext()],[item*Zlsb_zt*zsensorsens for item in self.data_chunks[1].ret()]
403         z_curves=[DataChunk(item) for item in z_curves]
404         return z_curves
405
406     def Z_extremes(self):
407         '''
408         returns the extremes of the Z values
409         '''
410         zcurves=self.Z_data()
411         z_extremes={}
412         z_extremes['ext']=zcurves[0][0],zcurves[0][-1]
413         z_extremes['ret']=zcurves[1][0],zcurves[1][-1]
414
415         return z_extremes
416
417     def Z_step(self):
418         '''
419         returns the calculated step between the Z values
420         '''
421         zrange={}
422         zpoints={}
423
424         z_extremes=self.Z_extremes()
425
426         zrange['ext']=abs(z_extremes['ext'][0]-z_extremes['ext'][1])
427         zrange['ret']=abs(z_extremes['ret'][0]-z_extremes['ret'][1])
428
429         #We must take 1 from the calculated zpoints, or when I use the arange function gives me a point more
430         #with the step. That is, if I have 1000 points, and I use arange(start,stop,step), I have 1001 points...
431         #For cleanness, solution should really be when using arange, but oh well...
432         zpoints['ext']=len(self.Z_data()[0])-1
433         zpoints['ret']=len(self.Z_data()[1])-1
434         #this syntax must become coherent!!
435         return (zrange['ext']/zpoints['ext']),(zrange['ret']/zpoints['ret'])
436
437     def Z_domains(self):
438         '''
439         returns the Z domains on which to plot the force data.
440
441         The Z domains are returned as a single long DataChunk() extended list. The extension and retraction part
442         can be extracted using ext() and ret() methods.
443         '''
444         x1step=self.Z_step()[0]
445         x2step=self.Z_step()[1]
446
447         try:
448             xext=arange(self.Z_extremes()['ext'][0],self.Z_extremes()['ext'][1],-x1step)
449             xret=arange(self.Z_extremes()['ret'][0],self.Z_extremes()['ret'][1],-x2step)
450         except:
451             xext=arange(0,1)
452             xret=arange(0,1)
453             print 'picoforce.py: Warning. xext, xret domains cannot be extracted.'
454
455         if not (len(xext)==len(xret)):
456             if self.debug:
457                 #print warning
458                 print "picoforce.py: Warning. Extension and retraction domains have different sizes."
459                 print "length extension: ", len(xext)
460                 print "length retraction: ", len(xret)
461                 print "You cannot trust the resulting curve."
462                 print "Until a solution is found, I substitute the ext domain with the ret domain. Sorry."
463             xext=xret
464
465         return DataChunk(xext.tolist()+xret.tolist())
466
467     def Z_scan_size(self):
468         return self.get_Zscan_V_size()*self.get_Z_scan_sensitivity()
469
470     def Z_start(self):
471         return self.get_Zscan_V_start()*self.get_Z_scan_sensitivity()
472
473     def ramp_size(self,whichchunk):
474         '''
475         to be implemented if needed
476         '''
477         raise "Not implemented yet."
478
479
480     def ramp_offset(self,whichchunk):
481         '''
482         to be implemented if needed
483         '''
484         raise "Not implemented yet."
485
486     def detriggerize(self, forcext):
487         '''
488         Cuts away the trigger-induced s**t on the extension curve.
489         DEPRECATED
490         cutindex=2
491         startvalue=forcext[0]
492
493         for index in range(len(forcext)-1,2,-2):
494            if forcext[index]>startvalue:
495                 cutindex=index
496            else:
497                 break
498
499         return cutindex
500         '''
501         return 0
502
503     def is_me(self):
504         '''
505         self-identification of file type magic
506         '''
507         curve_file=file(self.filepath)
508         header=curve_file.read(30)
509         curve_file.close()
510
511         if header[2:17] == 'Force file list': #header of a picoforce file
512             self.data_chunks=[self._get_data_chunk(num) for num in [0,1,2]]
513             return True
514         else:
515             return False
516
517     def close_all(self):
518         '''
519         Explicitly closes all files
520         '''
521         self.textfile.close()
522         self.binfile.close()
523
524     def default_plots(self):
525         '''
526         creates the default PlotObject
527         '''
528
529
530         force=self.LSB_to_force()
531         zdomain=self.Z_domains()
532
533         samples=self._get_samples_line()
534         #cutindex=0
535         #cutindex=self.detriggerize(force.ext())
536
537         main_plot=lhc.PlotObject()
538
539         main_plot.vectors = [[zdomain.ext()[0:samples], force.ext()[0:samples]],[zdomain.ret()[0:samples], force.ret()[0:samples]]]
540         main_plot.normalize_vectors()
541         main_plot.units = ['meters','newton']
542         main_plot.destination = 0
543         main_plit.filename = self.filepath
544         main_plot.title = self.filepath
545         main_plot.colors = ['red', 'blue']
546         main_plit.styles = ['plot', 'plot']
547
548         return [main_plot]