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 Aquire and analyze cantilever calibration data.
29 W. Trevor King Dec. 2007-Jan. 2008
34 The relevent physical quantities are :
35 Vzp_out Output z-piezo voltage (what we generate)
36 Vzp Applied z-piezo voltage (after external ZPGAIN)
37 Zp The z-piezo position
38 Zcant The cantilever vertical deflection
39 Vphoto The photodiode vertical deflection voltage (what we measure)
40 Fcant The force on the cantilever
41 T The temperature of the cantilever and surrounding solution
42 (another thing we measure or guess)
43 k_b Boltzmann's constant
45 Which are related by the parameters :
47 zpSensitivity Zp / Vzp
48 photoSensitivity Vphoto / Zcant
51 Cantilever calibration assumes a pre-calibrated z-piezo
52 (zpSensitivity) and a amplifier (zpGain). In our lab, the z-piezo is
53 calibrated by imaging a calibration sample, which has features with
54 well defined sizes, and the gain is set with a knob on the Nanoscope.
56 photoSensitivity is measured by bumping the cantilever against the
57 surface, where Zp = Zcant (see bump_aquire() and the bump_analyze
60 k_cant is measured by watching the cantilever vibrate in free solution
61 (see the vib_aquire() and the vib_analyze submodule). The average
62 energy of the cantilever in the vertical direction is given by the
63 equipartition theorem.
64 1/2 k_b T = 1/2 k_cant <Zcant**2>
65 so k_cant = k_b T / Zcant**2
66 but Zcant = Vphoto / photoSensitivity
67 so k_cant = k_b T * photoSensitivty**2 / <Vphoto**2>
69 We measured photoSensitivity with the surface bumps. We can either
70 measure T using an external function (see temperature.py), or just
71 estimate it (see T_aquire() and the T_analyze submodule). Guessing
72 room temp ~22 deg C is actually fairly reasonable. Assuming the
73 actual fluid temperature is within +/- 5 deg, the error in the spring
74 constant k_cant is within 5/273.15 ~= 2%. A time series of Vphoto
75 while we're far from the surface and not changing Vzp_out will give us
76 the average variance <Vphoto**2>.
78 We do all these measurements a few times to estimate statistical
81 The functions are layed out in the families:
82 bump_*(), vib_*(), T_*(), and calib_*()
83 where calib_{save|load|analyze}() deal with derived data, not
86 For each family, * can be any of :
87 aquire get real-world data
88 save store real-world data to disk
89 load get real-world data from disk
90 analyze interperate the real-world data.
91 plot show a nice graphic to convince people we're working :p
93 read a file with a list of paths to previously saved
94 real world data load each file using *_load(), analyze
95 using *_analyze(), and optionally plot using *_plot().
96 Intended for re-processing old data.
97 A family name without any _* extension (e.g. bump()), runs *_aquire(),
98 *_save(), *_analyze(), *_plot().
100 We also define the two positioning functions:
101 move_just_onto_surface() and move_far_from_surface()
102 which make automating the calibration procedure more straightforward.
108 from splittable_kwargs import splittableKwargsFunction, \
109 make_splittable_kwargs_function
121 @splittableKwargsFunction()
122 def bump_aquire(zpiezo, push_depth=200, npoints=1024, freq=100e3) :
124 Ramps closer push_depth and returns to the original position.
126 zpiezo an opened zpiezo.zpiezo instance
127 push_depth distance to approach, in nm
128 npoints number points during the approach and during the retreat
129 freq rate at which data is aquired
130 Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
132 # generate the bump output
133 start_pos = zpiezo.curPos()
134 pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
135 close_pos = start_pos + pos_dist
136 appr = numpy.linspace(start_pos, close_pos, npoints)
137 retr = numpy.linspace(close_pos, start_pos, npoints)
138 out = numpy.concatenate((appr, retr))
139 # run the bump, and measure deflection
140 if config.TEXT_VERBOSE :
141 print "Bump %g nm" % push_depth
142 data = zpiezo.ramp(out, freq)
145 @splittableKwargsFunction(bump_aquire,
146 (bump_analyze.bump_save, 'data'),
147 (bump_analyze.bump_analyze, 'data'))
150 Wrapper around bump_aquire(), bump_save(), bump_analyze()
152 bump_aquire_kwargs,bump_save_kwargs,bump_analyze_kwargs = \
153 bump._splitargs(bump, kwargs)
154 data = bump_aquire(**bump_aquire_kwargs)
155 bump_analyze.bump_save(data, **bump_save_kwargs)
156 photoSensitivity = bump_analyze.bump_analyze(data, **bump_analyze_kwargs)
157 return photoSensitivity
160 # Fairly stubby, since a one shot Temp measurement is a common thing.
161 # We just wrap that to provide a consistent interface.
163 @splittableKwargsFunction()
164 def T_aquire(get_T=None) :
166 Measure the current temperature of the sample,
167 or, if get_T == None, fake it by returning config.DEFAULT_TEMP
170 if config.TEXT_VERBOSE :
171 print "Fake temperature %g" % config.DEFAULT_TEMP
172 return config.DEFAULT_TEMP
174 if config.TEXT_VERBOSE :
175 print "Measure temperature"
178 @splittableKwargsFunction(T_aquire,
179 (T_analyze.T_save, 'T'),
180 (T_analyze.T_analyze, 'T'))
183 Wrapper around T_aquire(), T_save(), T_analyze(), T_plot()
185 T_aquire_kwargs,T_save_kwargs,T_analyze_kwargs = \
186 T._splitargs(T, kwargs)
187 T_raw = T_aquire(**T_aquire_kwargs)
188 T_analyze.T_save(T_raw, **T_save_kwargs)
189 T_ret = T_analyze.T_analyze(T_raw, **T_analyze_kwargs) # returns array
194 @splittableKwargsFunction()
195 def vib_aquire(zpiezo, time=1, freq=50e3) :
197 Record data for TIME seconds at FREQ Hz from ZPIEZO at it's current position.
199 # round up to the nearest power of two, for efficient FFT-ing
200 nsamps = FFT_tools.ceil_pow_of_two(time*freq)
202 # take some data, keeping the position voltage at it's current value
203 out = numpy.ones((nsamps,), dtype=numpy.uint16) * zpiezo.curPos()
204 if config.TEXT_VERBOSE :
205 print "get %g seconds of data" % time
206 data = zpiezo.ramp(out, freq)
207 data['sample frequency Hz'] = numpy.array([freq])
210 @splittableKwargsFunction(vib_aquire,
211 (vib_analyze.vib_save, 'data'),
212 (vib_analyze.vib_analyze, 'deflection_bits', 'freq'))
215 Wrapper around vib_aquire(), vib_save(), vib_analyze()
217 vib_aquire_kwargs,vib_save_kwargs,vib_analyze_kwargs = \
218 vib._splitargs(vib, kwargs)
219 data = vib_aquire(**vib_aquire_kwargs)
220 vib_analyze.vib_save(data, **vib_save_kwargs)
221 freq = data['sample frequency Hz']
222 deflection_bits = data['Deflection input']
223 Vphoto_var = vib_analyze.vib_analyze(deflection_bits=deflection_bits,
224 freq=freq, **vib_analyze_kwargs)
227 # A few positioning functions, so we can run bump_aquire() and vib_aquire()
228 # with proper spacing relative to the surface.
230 @splittableKwargsFunction()
231 def move_just_onto_surface(stepper, zpiezo, Depth_nm=100, setpoint=2) :
233 Uses z_piezo_utils.getSurfPos() to pinpoint the position of the surface.
234 Adjusts the stepper position as required to get within stepper_tol nm
236 Then set Vzp to place the cantilever Depth_nm onto the surface.
238 If getSurfPos() fails to find the surface, backs off (for safety)
239 and steps in (without moving the zpiezo) until Vphoto > setpoint.
241 stepper_tol = 250 # nm, generous estimate of the fullstep stepsize
243 if config.TEXT_VERBOSE :
244 print "moving just onto surface"
246 if config.TEXT_VERBOSE :
247 print "zero the z piezo output"
248 zpiezo.jumpToPos(zpiezo.pos_nm2out(0))
249 # See if we're near the surface already
250 if config.TEXT_VERBOSE :
251 print "See if we're starting near the surface"
253 dist = zpiezo.pos_out2nm( \
254 z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))
256 except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
257 if config.TEXT_VERBOSE :
258 print "distance failed with: ", string
259 print "Back off 200 half steps"
260 # Back away 200 steps
261 stepper.step_rel(-400)
262 stepper.step_rel(200)
263 sp = zpiezo.def_V2in(setpoint) # sp = setpoint in bits
264 zpiezo.updateInputs()
265 cd = zpiezo.curDef() # cd = current deflection in bits
266 if config.TEXT_VERBOSE :
267 print "Single stepping approach"
269 if config.TEXT_VERBOSE :
270 print "deflection %g < setpoint %g. step closer" % (cd, sp)
271 stepper.step_rel(2) # Full step in
272 zpiezo.updateInputs()
274 # Back off two steps (protecting against backlash)
275 if config.TEXT_VERBOSE :
276 print "Step back 4 half steps to get off the setpoint"
277 stepper.step_rel(-200)
278 stepper.step_rel(196)
279 # get the distance to the surface
280 zpiezo.updateInputs()
281 if config.TEXT_VERBOSE :
282 print "get surf pos, with setpoint %g (%d)" % (setpoint, zpiezo.def_V2in(setpoint))
283 for i in range(20) : # HACK, keep stepping back until we get a distance
285 dist = zpiezo.pos_out2nm( \
286 z_piezo_utils.getSurfPos(zpiezo,zpiezo.def_V2in(setpoint)))
287 except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
288 stepper.step_rel(-200)
289 stepper.step_rel(198)
293 print "tried %d times, still too close! bailing" % i
294 print "probably an invalid setpoint."
295 raise Exception, "weirdness"
296 if config.TEXT_VERBOSE :
297 print 'distance to surface ', dist, ' nm'
298 # fine tune the stepper position
299 while dist < -stepper_tol : # step back if we need to
300 stepper.step_rel(-200)
301 stepper.step_rel(198)
302 dist = zpiezo.pos_out2nm( \
303 z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
304 if config.TEXT_VERBOSE :
305 print 'distance to surface ', dist, ' nm, step back'
306 while dist > stepper_tol : # and step forward if we need to
308 dist = zpiezo.pos_out2nm( \
309 z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
310 if config.TEXT_VERBOSE :
311 print 'distance to surface ', dist, ' nm, step closer'
312 # now adjust the zpiezo to place us just onto the surface
313 target = dist + Depth_nm
314 zpiezo.jumpToPos(zpiezo.pos_nm2out(target))
316 if config.TEXT_VERBOSE :
317 print "We're %g nm into the surface" % Depth_nm
319 @splittableKwargsFunction()
320 def move_far_from_surface(stepper, um_back=50) :
322 Step back a specified number of microns.
323 (uses very rough estimate of step distance at the moment)
326 steps = int(um_back*1000/step_nm)
327 print "step back %d steps" % steps
328 stepper.step_rel(-steps)
331 # and finally, the calib family
333 @splittableKwargsFunction((move_just_onto_surface, 'stepper', 'zpiezo'),
334 (bump, 'zpiezo', 'freq', 'log_dir', 'Vphoto_in2V'),
335 (move_far_from_surface, 'stepper'),
337 (vib, 'zpiezo', 'log_dir', 'Vphoto_in2V'),
338 (analyze.calib_save, 'bumps','Ts','vibs','log_dir'))
339 def calib_aquire(stepper, zpiezo, num_bumps=10, num_Ts=10, num_vibs=20,
341 log_dir=config.LOG_DIR, Vphoto_in2V=config.Vphoto_in2V,
344 Aquire data for calibrating a cantilever in one function.
345 return (bump, T, vib), each of which is an array.
347 stepper a stepper.stepper_obj for coarse Z positioning
348 zpiezo a z_piezo.z_piezo for fine positioning and deflection readin
349 setpoint maximum allowed deflection (in Volts) during approaches
350 num_bumps number of 'a's (see Outputs)
351 push_depth_nm depth of each push when generating a
352 num_temps number of 'T's (see Outputs)
353 num_vibs number of 'vib's (see Outputs)
354 log_dir directory to log data to. Default 'None' disables logging.
355 Outputs (all are arrays of recorded data) :
356 bumps measured (V_photodiode / nm_tip) proportionality constant
357 Ts measured temperature (K)
358 vibs measured V_photodiode variance in free solution
360 move_just_onto_surface_kwargs,bump_kwargs,move_far_from_surface_kwargs, \
361 T_kwargs,vib_kwargs,calib_save_kwargs = \
362 calib_aquire._splitargs(calib_aquire, kwargs)
364 move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
365 bumps = numpy.zeros((num_bumps,), dtype=numpy.float)
366 for i in range(num_bumps) :
367 bumps[i] = bump(zpiezo=zpiezo, freq=bump_freq, log_dir=log_dir,
368 Vphoto_in2V=Vphoto_in2V, **bump_kwargs)
369 if config.TEXT_VERBOSE :
372 move_far_from_surface(stepper, **move_far_from_surface_kwargs)
375 Ts = numpy.zeros((num_Ts,), dtype=numpy.float)
376 for i in range(num_Ts) :
377 Ts[i] = T(**T_kwargs)
378 time.sleep(1) # wait a bit to get an independent temperature measure
382 vibs = numpy.zeros((num_vibs,), dtype=numpy.float)
383 for i in range(num_vibs) :
384 vibs[i] = vib(zpiezo=zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V,
388 analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs)
390 return (bumps, Ts, vibs)
393 @splittableKwargsFunction( \
394 (calib_aquire, 'log_dir'),
395 (analyze.calib_analyze, 'bumps','Ts','vibs'))
396 def calib(log_dir=config.LOG_DIR, **kwargs) :
398 Calibrate a cantilever in one function.
399 The I-don't-care-about-the-details black box version :p.
404 k cantilever spring constant (in N/m, or equivalently nN/nm)
405 k_s standard deviation in our estimate of k
407 See get_calibration_data() for the data aquisition code
408 See analyze_calibration_data() for the analysis code
410 calib_aquire_kwargs,calib_analyze_kwargs = \
411 calib._splitargs(calib, kwargs)
412 a, T, vib = calib_aquire(**calib_aquire_kwargs)
413 k,k_s,ps2_m, ps2_s,T_m,T_s,one_o_Vp2_m,one_o_Vp2_s = \
414 analyze.calib_analyze(a, T, vib, **calib_analyze_kwargs)
415 analyze.calib_save_analysis(k, k_s, ps2_m, ps2_s, T_m, T_s,
416 one_o_Vp2_m, one_o_Vp2_s, log_dir)