3 # calibcant - tools for thermally calibrating AFM cantilevers
5 # Copyright (C) 2007,2008, William Trevor King
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.
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.
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
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.
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.
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)
38 Which are related by the parameters :
40 zpSensitivity Zp / Vzp
41 photoSensitivity Vphoto / Zcant
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().
50 import common # common module for the calibcant package
51 import config # config module for the calibcant package
53 from splittable_kwargs import splittableKwargsFunction, \
54 make_splittable_kwargs_function
57 @splittableKwargsFunction()
58 def Vzp_bits2nm(data_bits, zpGain=config.zpGain,
59 zpSensitivity=config.zpSensitivity,
60 Vzp_out2V=config.Vzp_out2V):
61 scale_Vzp_bits2V = Vzp_out2V(1) - Vzp_out2V(0)
62 data_V = data_bits / scale_Vzp_bits2V
64 data_nm = data_V * zpGain * zpSensitivity
67 @splittableKwargsFunction()
68 def Vphoto_bits2V(data_bits, Vphoto_in2V=config.Vphoto_in2V):
69 scale_Vphoto_bits2V = Vphoto_in2V(1) - Vphoto_in2V(0)
70 Vphoto_V = data_bits / scale_Vphoto_bits2V
74 @splittableKwargsFunction((Vzp_bits2nm, 'data_bits'),
75 (Vphoto_bits2V, 'data_bits'))
76 def slope_bitspbit2Vpnm(slope_bitspbit, **kwargs):
77 zp_kwargs,photo_kwargs = slope_bitspbit2Vpnm._splitargs(slope_bitspbit2Vpnm, kwargs)
79 Vphoto_bits = slope_bitspbit * Vzp_bits
80 return Vphoto_bits2V(Vphoto_bits, **photo_kwargs)/Vzp_bits2nm(Vzp_bits, **zp_kwargs)
82 #@splittableKwargsFunction((bump_fit, 'zpiezo_output_bits',
83 # 'deflection_input_bits'),
84 # (slope_bitspbit2Vpnm, 'slope_bitspbit'))
85 # Some of the child functions aren't yet defined, so postpone
86 # make-splittable until later in the module.
87 def bump_analyze(data, **kwargs) :
89 Return the slope of the bump ;).
91 data dictionary of data in DAC/ADC bits
92 Vzp_out2V function that converts output DAC bits to Volts
93 Vphoto_in2V function that converts input ADC bits to Volts
94 zpGain zpiezo applied voltage per output Volt
95 zpSensitivity nm zpiezo response per applied Volt
97 photoSensitivity (Vphoto/Zcant) in Volts/nm
98 Checks for strong correlation (r-value) and low randomness chance (p-value)
100 With the current implementation, the data is regressed in DAC/ADC bits
101 and THEN converted, so we're assuming that both conversions are LINEAR.
102 If they aren't, rewrite to convert before the regression.
104 bump_fit_kwargs,slope_bitspbit2Vpnm_kwargs = \
105 bump_analyze._splitargs(bump_analyze, kwargs)
106 Vphoto2Vzp_out_bit = bump_fit(data['Z piezo output'],
107 data['Deflection input'],
109 return slope_bitspbit2Vpnm(Vphoto2Vzp_out_bit, **slope_bitspbit2Vpnm_kwargs)
111 def limited_linear(x, params):
114 flat region (off-surface)
115 linear region (in-contact)
116 flat region (high-voltage-rail)
118 x_contact (x value for the surface-contact kink)
119 y_contact (y value for the surface-contact kink)
120 slope (dy/dx at the surface-contact kink)
122 high_voltage_rail = 2**16 - 1 # bits
123 x_contact,y_contact,slope = params
124 y = slope*(x-x_contact) + y_contact
125 y = numpy.clip(y, y_contact, high_voltage_rail)
128 def limited_linear_param_guess(x, y) :
130 Guess rough parameters for a limited_linear model. Assumes the
131 bump approaches (raising the deflection as it does so) first.
132 Retracting after the approach is optional. Approximates the contact
133 position and an on-surface (high) position by finding first crossings
134 of thresholds 0.3 and 0.7 of the y value's total range. Not the
135 most efficient algorithm, but it seems fairly robust.
137 y_contact = float(y.min())
138 y_max = float(y.max())
140 y_low = y_contact + 0.3 * (y_max-y_contact)
141 y_high = y_contact + 0.7 * (y_max-y_contact)
145 while y[i] < y_high :
148 x_contact = float(x[i_low])
149 x_high = float(x[i_high])
150 slope = (y_high - y_contact) / (x_high - x_contact)
151 return (x_contact, y_contact, slope)
153 def limited_linear_sensitivity(params):
155 Return the estimated sensitivity to small deflections according to
156 limited_linear fit parameters.
161 def limited_quadratic(x, params):
164 flat region (off-surface)
165 quadratic region (in-contact)
166 flat region (high-voltage-rail)
168 x_contact (x value for the surface-contact kink)
169 y_contact (y value for the surface-contact kink)
170 slope (dy/dx at the surface-contact kink)
171 quad (d**2 y / dx**2, allow decreasing sensitivity with increased x)
173 high_voltage_rail = 2**16 - 1 # bits
174 x_contact,y_contact,slope,quad = params
175 y = slope*(x-x_contact) + quad*(x-x_contact)**2+ y_contact
176 y = numpy.clip(y, y_contact, high_voltage_rail)
179 def limited_quadratic_param_guess(x, y) :
181 Guess rough parameters for a limited_quadratic model. Assumes the
182 bump approaches (raising the deflection as it does so) first.
183 Retracting after the approach is optional. Approximates the contact
184 position and an on-surface (high) position by finding first crossings
185 of thresholds 0.3 and 0.7 of the y value's total range. Not the
186 most efficient algorithm, but it seems fairly robust.
188 x_contact,y_contact,slope = limited_linear_param_guess(x,y)
190 return (x_contact, y_contact, slope, quad)
192 def limited_quadratic_sensitivity(params):
194 Return the estimated sensitivity to small deflections according to
195 limited_quadratic fit parameters.
200 @splittableKwargsFunction()
201 def bump_fit(zpiezo_output_bits, deflection_input_bits,
202 param_guesser=limited_quadratic_param_guess,
203 model=limited_quadratic,
204 sensitivity_from_fit_params=limited_quadratic_sensitivity,
206 x = zpiezo_output_bits
207 y = deflection_input_bits
208 def residual(p, y, x) :
209 return model(x, p) - y
210 param_guess = param_guesser(x, y)
211 p,cov,info,mesg,ier = \
212 scipy.optimize.leastsq(residual, param_guess, args=(y, x),
213 full_output=True, maxfev=int(10e3))
214 if config.TEXT_VERBOSE :
215 print "Fitted params:",p
216 print "Covariance mx:",cov
220 print "Solution converged"
222 print "Solution did not converge"
223 if plotVerbose or config.PYLAB_VERBOSE :
224 yguess = model(x, param_guess)
225 #yguess = None # Don't print the guess, since I'm convinced it's ok ;).
227 bump_plot(data={"Z piezo output":x, "Deflection input":y},
228 yguess=yguess, yfit=yfit, plotVerbose=plotVerbose)
229 return sensitivity_from_fit_params(p)
231 @splittableKwargsFunction()
232 def bump_save(data, log_dir=None) :
233 "Save the dictionary data, using data_logger.data_log()"
235 log = data_logger.data_log(log_dir, noclobber_logsubdir=False,
237 log.write_dict_of_arrays(data)
239 def bump_load(datafile) :
240 "Load the dictionary data, using data_logger.date_load()"
241 dl = data_logger.data_load()
242 data = dl.read_dict_of_arrays(datafile)
245 @splittableKwargsFunction()
246 def bump_plot(data, yguess=None, yfit=None, plotVerbose=False) :
247 "Plot the bump (Vphoto vs Vzp) if plotVerbose or PYLAB_VERBOSE == True"
248 if plotVerbose or config.PYLAB_VERBOSE :
249 common._import_pylab()
250 common._pylab.figure(config.BASE_FIGNUM)
251 if yfit != None: # two subplot figure
252 common._pylab.subplot(211)
253 common._pylab.hold(False)
254 common._pylab.plot(data["Z piezo output"], data["Deflection input"],
256 common._pylab.hold(True)
258 common._pylab.plot(data["Z piezo output"], yguess,
261 common._pylab.plot(data["Z piezo output"], yfit,
263 common._pylab.hold(False)
264 common._pylab.title("bump surface")
265 common._pylab.legend(loc='upper left')
266 common._pylab.xlabel("Z piezo output voltage (bits)")
267 common._pylab.ylabel("Photodiode input voltage (bits)")
269 # second subplot for residual
270 common._pylab.subplot(212)
271 common._pylab.plot(data["Z piezo output"],
272 data["Deflection input"] - yfit,
273 'r-', label='residual')
274 common._pylab.legend(loc='upper right')
275 common._pylab.xlabel("Z piezo output voltage (bits)")
276 common._pylab.ylabel("Photodiode input voltage (bits)")
279 make_splittable_kwargs_function(bump_analyze,
280 (bump_fit, 'zpiezo_output_bits',
281 'deflection_input_bits'),
282 (slope_bitspbit2Vpnm, 'slope_bitspbit'))
284 @splittableKwargsFunction((bump_analyze, 'data'))
285 def bump_load_analyze_tweaked(tweak_file, **kwargs):
286 "Load the output file of tweak_calib_bump.sh, return an array of slopes"
287 bump_analyze_kwargs, = \
288 bump_load_analyze_tweaked._splitargs(bump_load_analyze_tweaked, kwargs)
289 photoSensitivity = []
290 for line in file(tweak_file, 'r') :
291 parsed = line.split()
292 path = parsed[0].strip()
293 if path[0] == '#' : # a comment
295 if config.TEXT_VERBOSE :
296 print "Reading data from %s with ranges %s" % (path, parsed[1:])
298 full_data = bump_load(path)
299 if len(parsed) == 1 :
300 data = full_data # use whole bump
302 # use the listed sections
305 for rng in parsed[1:] :
309 zp.extend(full_data['Z piezo output'][starti:stopi])
310 df.extend(full_data['Deflection input'][starti:stopi])
311 data = {'Z piezo output': numpy.array(zp),
312 'Deflection input': numpy.array(df)}
313 pSi = bump_analyze(data, **bump_analyze_kwargs)
314 photoSensitivity.append(pSi)
315 return numpy.array(photoSensitivity, dtype=numpy.float)
317 # commandline interface functions
320 def read_data(ifile):
321 "ifile can be a filename string or open (seekable) file object"
322 if ifile == None : ifile = sys.stdin
323 unlabeled_data=scipy.io.read_array(ifile)
325 data['Z piezo output'] = unlabeled_data[:,0]
326 data['Deflection input'] = unlabeled_data[:,1]
329 def remove_further_than(data, zp_crit) :
331 ndata['Z piezo output'] = []
332 ndata['Deflection input'] = []
333 for zp,df in zip(data['Z piezo output'],data['Deflection input']) :
335 ndata['Z piezo output'].append(zp)
336 ndata['Deflection input'].append(df)
339 if __name__ == '__main__' :
340 # command line interface
341 from optparse import OptionParser
343 usage_string = ('%prog <input-file>\n'
344 '2008, W. Trevor King.\n'
346 'There are two operation modes, one to analyze a single bump file,\n'
347 'and one to analyze tweak files.\n'
349 'Single file mode (the default) :\n'
350 'Scales raw DAC/ADC bit data and fits a bounded quadratic.\n'
351 'Returns photodiode sensitivity Vphotodiode/Zcantilever in V/nm, determined by.\n'
352 'the slope at the kink between the non-contact region and the contact region.\n'
353 '<input-file> should be whitespace-delimited, 2 column ASCII\n'
354 'without a header line. e.g: "<zp_DAC>\\t<deflection_ADC>\\n"\n'
357 'Runs the same analysis as in single file mode for each bump in\n'
358 'a tweak file. Each line in the tweak file specifies a single bump.\n'
359 'Blank lines and those beginning with a pound sign (#) are ignored.\n'
360 'The format of a line is a series of whitespace-separated fields--\n'
361 'a base file path followed by optional point index ranges, e.g.:\n'
362 '20080919/20080919132500_bump_surface 10:651 1413:2047\n'
363 'which only discards all points outside the index ranges [10,651)\n'
364 'and [1413,2047) (indexing starts at 0).\n'
366 parser = OptionParser(usage=usage_string, version='%prog '+common.VERSION)
367 parser.add_option('-o', '--output-file', dest='ofilename',
368 help='write output to FILE (default stdout)',
369 type='string', metavar='FILE')
370 parser.add_option('-c', '--comma-out', dest='comma_out', action='store_true',
371 help='Output comma-seperated values (default %default)',
373 parser.add_option('-p', '--pylab', dest='pylab', action='store_true',
374 help='Produce pylab fit checks during execution',
376 parser.add_option('-t', '--tweak-mode', dest='tweakmode', action='store_true',
377 help='Run in tweak-file mode',
379 parser.add_option('-d', '--datalogger-mode', dest='datalogger_mode', action='store_true',
380 help='Run input files with datalogger.read_dict_of_arrays(). This is useful, for example, to test a single line from a tweakfile.',
382 parser.add_option('-q', '--disable-quadratic', dest='quadratic', action='store_false',
383 help='Disable quadratic term in fitting (i.e. use bounded linear fits).',
385 parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
386 help='Print lots of debugging information',
389 options,args = parser.parse_args()
391 assert len(args) >= 1, "Need an input file"
395 if options.ofilename != None :
396 ofile = file(options.ofilename, 'w')
399 config.TEXT_VERBOSE = options.verbose
400 config.PYLAB_INTERACTIVE = False
401 config.PYLAB_VERBOSE = options.pylab
402 config.GNUPLOT_VERBOSE = False
403 if options.quadratic == True:
404 param_guesser = limited_quadratic_param_guess
405 model = limited_quadratic
406 sensitivity_from_fit_params = limited_quadratic_sensitivity
408 param_guesser = limited_linear_param_guess
409 model = limited_linear
410 sensitivity_from_fit_params = limited_linear_sensitivity
412 if options.tweakmode == False :
413 if options.datalogger_mode:
414 data = bump_load(ifilename)
416 data = read_data(ifilename)
417 photoSensitivity = bump_analyze(data,
418 param_guesser=param_guesser,
420 sensitivity_from_fit_params=sensitivity_from_fit_params)
422 print >> ofile, photoSensitivity
423 else : # tweak file mode
424 slopes = bump_load_analyze_tweaked(ifilename,
425 param_guesser=param_guesser,
427 sensitivity_from_fit_params=sensitivity_from_fit_params)
428 if options.comma_out :
432 common.write_array(ofile, slopes, sep)
434 if options.ofilename != None :