Use relative imports (PEP 328) for calibcant sibling imports.
[calibcant.git] / calibcant / bump_analyze.py
1 #!/usr/bin/python
2 #
3 # calibcant - tools for thermally calibrating AFM cantilevers
4 #
5 # Copyright (C) 2007,2008, William Trevor King
6 #
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU 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.
11 #
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15 # See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
20 # 02111-1307, USA.
21 #
22 # The author may be contacted at <wking@drexel.edu> on the Internet, or
23 # write to Trevor King, Drexel University, Physics Dept., 3141 Chestnut St.,
24 # Philadelphia PA 19104, USA.
25
26 """
27 Separate the more general bump_analyze() from the other bump_*()
28 functions in calibcant.  Also provide a command line interface
29 for analyzing data acquired through other workflows.
30
31 The relevant physical quantities are :
32  Vzp_out  Output z-piezo voltage (what we generate)
33  Vzp      Applied z-piezo voltage (after external ZPGAIN)
34  Zp       The z-piezo position
35  Zcant    The cantilever vertical deflection
36  Vphoto   The photodiode vertical deflection voltage (what we measure)
37
38 Which are related by the parameters :
39  zpGain           Vzp_out / Vzp
40  zpSensitivity    Zp / Vzp
41  photoSensitivity Vphoto / Zcant
42
43 photoSensitivity is measured by bumping the cantilever against the
44 surface, where Zp = Zcant (see calibrate.bump_aquire()).  The measured
45 slope Vphoto/Vout is converted to photoSensitivity with bump_analyze().
46 """
47
48 import numpy
49 import scipy.optimize
50
51 import data_logger
52 from splittable_kwargs import splittableKwargsFunction, \
53     make_splittable_kwargs_function
54
55 import .common
56 import .config
57
58
59 @splittableKwargsFunction()
60 def Vzp_bits2nm(data_bits, zpGain=config.zpGain,
61                 zpSensitivity=config.zpSensitivity,
62                 Vzp_out2V=config.Vzp_out2V):
63     scale_Vzp_bits2V = Vzp_out2V(1) - Vzp_out2V(0)
64     data_V = data_bits / scale_Vzp_bits2V
65     #             bits / (bits/V) = V
66     data_nm = data_V * zpGain * zpSensitivity
67     return data_nm
68
69 @splittableKwargsFunction()
70 def Vphoto_bits2V(data_bits, Vphoto_in2V=config.Vphoto_in2V):
71     scale_Vphoto_bits2V = Vphoto_in2V(1) - Vphoto_in2V(0)
72     Vphoto_V = data_bits / scale_Vphoto_bits2V
73     #               bits / (bits/V) = V
74     return Vphoto_V
75
76 @splittableKwargsFunction((Vzp_bits2nm, 'data_bits'),
77                           (Vphoto_bits2V, 'data_bits'))
78 def slope_bitspbit2Vpnm(slope_bitspbit, **kwargs):
79     zp_kwargs,photo_kwargs = slope_bitspbit2Vpnm._splitargs(slope_bitspbit2Vpnm, kwargs)
80     Vzp_bits = 1.0
81     Vphoto_bits = slope_bitspbit * Vzp_bits
82     return Vphoto_bits2V(Vphoto_bits, **photo_kwargs)/Vzp_bits2nm(Vzp_bits, **zp_kwargs)
83     
84 #@splittableKwargsFunction((bump_fit, 'zpiezo_output_bits',
85 #                           'deflection_input_bits'),
86 #                          (slope_bitspbit2Vpnm, 'slope_bitspbit'))
87 # Some of the child functions aren't yet defined, so postpone
88 # make-splittable until later in the module.
89 def bump_analyze(data, **kwargs) :
90     """
91     Return the slope of the bump ;).
92     Inputs:
93       data        dictionary of data in DAC/ADC bits
94       Vzp_out2V   function that converts output DAC bits to Volts
95       Vphoto_in2V function that converts input ADC bits to Volts
96       zpGain      zpiezo applied voltage per output Volt
97       zpSensitivity  nm zpiezo response per applied Volt
98     Returns:
99      photoSensitivity (Vphoto/Zcant) in Volts/nm
100     Checks for strong correlation (r-value) and low randomness chance (p-value)
101     
102     With the current implementation, the data is regressed in DAC/ADC bits
103     and THEN converted, so we're assuming that both conversions are LINEAR.
104     If they aren't, rewrite to convert before the regression.
105     """
106     bump_fit_kwargs,slope_bitspbit2Vpnm_kwargs = \
107         bump_analyze._splitargs(bump_analyze, kwargs)
108     Vphoto2Vzp_out_bit = bump_fit(data['Z piezo output'],
109                                   data['Deflection input'],
110                                   **bump_fit_kwargs)
111     return slope_bitspbit2Vpnm(Vphoto2Vzp_out_bit, **slope_bitspbit2Vpnm_kwargs)
112
113 def limited_linear(x, params):
114     """
115     Model the bump as:
116       flat region (off-surface)
117       linear region (in-contact)
118       flat region (high-voltage-rail)
119     Parameters:
120       x_contact (x value for the surface-contact kink)
121       y_contact (y value for the surface-contact kink)
122       slope (dy/dx at the surface-contact kink)
123     """
124     high_voltage_rail = 2**16 - 1 # bits
125     x_contact,y_contact,slope = params
126     y = slope*(x-x_contact) + y_contact
127     y = numpy.clip(y, y_contact, high_voltage_rail)
128     return y
129
130 def limited_linear_param_guess(x, y) :
131     """
132     Guess rough parameters for a limited_linear model.  Assumes the
133     bump approaches (raising the deflection as it does so) first.
134     Retracting after the approach is optional.  Approximates the contact
135     position and an on-surface (high) position by finding first crossings
136     of thresholds 0.3 and 0.7 of the y value's total range.  Not the
137     most efficient algorithm, but it seems fairly robust.
138     """
139     y_contact = float(y.min())
140     y_max = float(y.max())
141     i = 0
142     y_low  = y_contact + 0.3 * (y_max-y_contact)
143     y_high = y_contact + 0.7 * (y_max-y_contact)
144     while y[i] < y_low :
145         i += 1
146     i_low = i
147     while y[i] < y_high :
148         i += 1
149     i_high = i
150     x_contact = float(x[i_low])
151     x_high = float(x[i_high])
152     slope = (y_high - y_contact) / (x_high - x_contact)
153     return (x_contact, y_contact, slope)
154
155 def limited_linear_sensitivity(params):
156     """
157     Return the estimated sensitivity to small deflections according to
158     limited_linear fit parameters.
159     """
160     slope = params[2]
161     return slope
162
163 def limited_quadratic(x, params):
164     """
165     Model the bump as:
166       flat region (off-surface)
167       quadratic region (in-contact)
168       flat region (high-voltage-rail)
169     Parameters:
170       x_contact (x value for the surface-contact kink)
171       y_contact (y value for the surface-contact kink)
172       slope (dy/dx at the surface-contact kink)
173       quad (d**2 y / dx**2, allow decreasing sensitivity with increased x)
174     """
175     high_voltage_rail = 2**16 - 1 # bits
176     x_contact,y_contact,slope,quad = params
177     y = slope*(x-x_contact) + quad*(x-x_contact)**2+ y_contact
178     y = numpy.clip(y, y_contact, high_voltage_rail)
179     return y
180
181 def limited_quadratic_param_guess(x, y) :
182     """
183     Guess rough parameters for a limited_quadratic model.  Assumes the
184     bump approaches (raising the deflection as it does so) first.
185     Retracting after the approach is optional.  Approximates the contact
186     position and an on-surface (high) position by finding first crossings
187     of thresholds 0.3 and 0.7 of the y value's total range.  Not the
188     most efficient algorithm, but it seems fairly robust.
189     """
190     x_contact,y_contact,slope = limited_linear_param_guess(x,y)
191     quad = 0
192     return (x_contact, y_contact, slope, quad)
193
194 def limited_quadratic_sensitivity(params):
195     """
196     Return the estimated sensitivity to small deflections according to
197     limited_quadratic fit parameters.
198     """
199     slope = params[2]
200     return slope
201
202 @splittableKwargsFunction()
203 def bump_fit(zpiezo_output_bits, deflection_input_bits,
204              param_guesser=limited_quadratic_param_guess,
205              model=limited_quadratic,
206              sensitivity_from_fit_params=limited_quadratic_sensitivity,
207              plotVerbose=False) :
208     x = zpiezo_output_bits
209     y = deflection_input_bits
210     def residual(p, y, x) :
211         return model(x, p) - y
212     param_guess = param_guesser(x, y)
213     p,cov,info,mesg,ier = \
214         scipy.optimize.leastsq(residual, param_guess, args=(y, x),
215                                full_output=True, maxfev=int(10e3))
216     if config.TEXT_VERBOSE :
217         print "Fitted params:",p
218         print "Covariance mx:",cov
219         print "Info:", info
220         print "mesg:", mesg
221         if ier == 1 :
222             print "Solution converged"
223         else :
224             print "Solution did not converge"
225     if plotVerbose or config.PYLAB_VERBOSE :
226         yguess = model(x, param_guess)
227         #yguess = None # Don't print the guess, since I'm convinced it's ok ;).
228         yfit = model(x, p)
229         bump_plot(data={"Z piezo output":x, "Deflection input":y},
230                   yguess=yguess, yfit=yfit, plotVerbose=plotVerbose)
231     return sensitivity_from_fit_params(p)
232
233 @splittableKwargsFunction()
234 def bump_save(data, log_dir=None) :
235     "Save the dictionary data, using data_logger.data_log()"
236     if log_dir != None :
237         log = data_logger.data_log(log_dir, noclobber_logsubdir=False,
238                                    log_name="bump")
239         log.write_dict_of_arrays(data)
240
241 def bump_load(datafile) :
242     "Load the dictionary data, using data_logger.date_load()"
243     dl = data_logger.data_load()
244     data = dl.read_dict_of_arrays(datafile)
245     return data
246
247 @splittableKwargsFunction()
248 def bump_plot(data, yguess=None, yfit=None, plotVerbose=False) :
249     "Plot the bump (Vphoto vs Vzp) if plotVerbose or PYLAB_VERBOSE == True"
250     if plotVerbose or config.PYLAB_VERBOSE :
251         common._import_pylab()
252         common._pylab.figure(config.BASE_FIGNUM)
253         if yfit != None: # two subplot figure
254             common._pylab.subplot(211)
255         common._pylab.hold(False)
256         common._pylab.plot(data["Z piezo output"], data["Deflection input"],
257                            '.', label='bump')
258         common._pylab.hold(True)
259         if yguess != None:
260             common._pylab.plot(data["Z piezo output"], yguess,
261                                'g-', label='guess')
262         if yfit != None:
263             common._pylab.plot(data["Z piezo output"], yfit,
264                                'r-', label='fit')
265         common._pylab.hold(False)
266         common._pylab.title("bump surface")
267         common._pylab.legend(loc='upper left')
268         common._pylab.xlabel("Z piezo output voltage (bits)")
269         common._pylab.ylabel("Photodiode input voltage (bits)")
270         if yfit != None:
271             # second subplot for residual
272             common._pylab.subplot(212)
273             common._pylab.plot(data["Z piezo output"],
274                                data["Deflection input"] - yfit,
275                                'r-', label='residual')
276             common._pylab.legend(loc='upper right')
277             common._pylab.xlabel("Z piezo output voltage (bits)")
278             common._pylab.ylabel("Photodiode input voltage (bits)")
279         common._flush_plot()
280
281 make_splittable_kwargs_function(bump_analyze,
282                                 (bump_fit, 'zpiezo_output_bits',
283                                  'deflection_input_bits'),
284                                 (slope_bitspbit2Vpnm, 'slope_bitspbit'))
285
286 @splittableKwargsFunction((bump_analyze, 'data'))
287 def bump_load_analyze_tweaked(tweak_file, **kwargs):
288     "Load the output file of tweak_calib_bump.sh, return an array of slopes"
289     bump_analyze_kwargs, = \
290         bump_load_analyze_tweaked._splitargs(bump_load_analyze_tweaked, kwargs)
291     photoSensitivity = []
292     for line in file(tweak_file, 'r') :
293         parsed = line.split()
294         path = parsed[0].strip()
295         if path[0] == '#' : # a comment
296             continue
297         if config.TEXT_VERBOSE :
298             print "Reading data from %s with ranges %s" % (path, parsed[1:])
299         # read the data
300         full_data = bump_load(path)
301         if len(parsed) == 1 :
302             data = full_data # use whole bump
303         else :
304             # use the listed sections
305             zp = []
306             df = []
307             for rng in parsed[1:] :
308                 p = rng.split(':')
309                 starti = int(p[0])
310                 stopi = int(p[1])
311                 zp.extend(full_data['Z piezo output'][starti:stopi])
312                 df.extend(full_data['Deflection input'][starti:stopi])
313             data = {'Z piezo output': numpy.array(zp),
314                     'Deflection input': numpy.array(df)}
315         pSi = bump_analyze(data, **bump_analyze_kwargs)
316         photoSensitivity.append(pSi)
317     return numpy.array(photoSensitivity, dtype=numpy.float)
318
319 # commandline interface functions
320 import scipy.io, sys
321
322 def read_data(ifile):
323     "ifile can be a filename string or open (seekable) file object"
324     if ifile == None :  ifile = sys.stdin
325     unlabeled_data=scipy.io.read_array(ifile)
326     data = {}
327     data['Z piezo output'] = unlabeled_data[:,0]
328     data['Deflection input'] = unlabeled_data[:,1]
329     return data
330
331 def remove_further_than(data, zp_crit) :
332     ndata = {}
333     ndata['Z piezo output'] = []
334     ndata['Deflection input'] = []
335     for zp,df in zip(data['Z piezo output'],data['Deflection input']) :
336         if zp > zp_crit :
337             ndata['Z piezo output'].append(zp)
338             ndata['Deflection input'].append(df)
339     return ndata
340
341 if __name__ == '__main__' :
342     # command line interface
343     from optparse import OptionParser
344     
345     usage_string = ('%prog <input-file>\n'
346                     '2008, W. Trevor King.\n'
347                     '\n'
348                     'There are two operation modes, one to analyze a single bump file,\n'
349                     'and one to analyze tweak files.\n'
350                     '\n'
351                     'Single file mode (the default) :\n'
352                     'Scales raw DAC/ADC bit data and fits a bounded quadratic.\n'
353                     'Returns photodiode sensitivity Vphotodiode/Zcantilever in V/nm, determined by.\n'
354                     'the slope at the kink between the non-contact region and the contact region.\n'
355                     '<input-file> should be whitespace-delimited, 2 column ASCII\n'
356                     'without a header line.  e.g: "<zp_DAC>\\t<deflection_ADC>\\n"\n'
357                     '\n'
358                     'Tweak file mode:\n'
359                     'Runs the same analysis as in single file mode for each bump in\n'
360                     'a tweak file.  Each line in the tweak file specifies a single bump.\n'
361                     'Blank lines and those beginning with a pound sign (#) are ignored.\n'
362                     'The format of a line is a series of whitespace-separated fields--\n'
363                     'a base file path followed by optional point index ranges, e.g.:\n'
364                     '20080919/20080919132500_bump_surface 10:651 1413:2047\n'
365                     'which only discards all points outside the index ranges [10,651)\n'
366                     'and [1413,2047) (indexing starts at 0).\n'
367                     )
368     parser = OptionParser(usage=usage_string, version='%prog '+common.VERSION)
369     parser.add_option('-o', '--output-file', dest='ofilename',
370                       help='write output to FILE (default stdout)',
371                       type='string', metavar='FILE')
372     parser.add_option('-c', '--comma-out', dest='comma_out', action='store_true',
373                       help='Output comma-seperated values (default %default)',
374                       default=False)
375     parser.add_option('-p', '--pylab', dest='pylab', action='store_true',
376                       help='Produce pylab fit checks during execution',
377                       default=False)
378     parser.add_option('-t', '--tweak-mode', dest='tweakmode', action='store_true',
379                       help='Run in tweak-file mode',
380                       default=False)
381     parser.add_option('-d', '--datalogger-mode', dest='datalogger_mode', action='store_true',
382                       help='Run input files with datalogger.read_dict_of_arrays().  This is useful, for example, to test a single line from a tweakfile.',
383                       default=False)
384     parser.add_option('-q', '--disable-quadratic', dest='quadratic', action='store_false',
385                       help='Disable quadratic term in fitting (i.e. use bounded linear fits).',
386                       default=True)
387     parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
388                       help='Print lots of debugging information',
389                       default=False)
390
391     options,args = parser.parse_args()
392     parser.destroy()
393     assert len(args) >= 1, "Need an input file"
394         
395     ifilename = args[0]
396
397     if options.ofilename != None :
398         ofile = file(options.ofilename, 'w')
399     else :
400         ofile = sys.stdout
401     config.TEXT_VERBOSE = options.verbose
402     config.PYLAB_INTERACTIVE = False
403     config.PYLAB_VERBOSE = options.pylab
404     config.GNUPLOT_VERBOSE = False
405     if options.quadratic == True:
406         param_guesser = limited_quadratic_param_guess
407         model = limited_quadratic
408         sensitivity_from_fit_params = limited_quadratic_sensitivity
409     else:
410         param_guesser = limited_linear_param_guess
411         model = limited_linear
412         sensitivity_from_fit_params = limited_linear_sensitivity
413     
414     if options.tweakmode == False :
415         if options.datalogger_mode:
416             data = bump_load(ifilename)
417         else:
418             data = read_data(ifilename)
419         photoSensitivity = bump_analyze(data,
420                                         param_guesser=param_guesser,
421                                         model=model,
422                                         sensitivity_from_fit_params=sensitivity_from_fit_params)
423         
424         print >> ofile, photoSensitivity
425     else : # tweak file mode
426         slopes = bump_load_analyze_tweaked(ifilename,
427                                            param_guesser=param_guesser,
428                                            model=model,
429                                            sensitivity_from_fit_params=sensitivity_from_fit_params)
430         if options.comma_out :
431             sep = ','
432         else :
433             sep = '\n'
434         common.write_array(ofile, slopes, sep)
435     
436     if options.ofilename != None :
437         ofile.close()