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