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