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