4 Aquire and analyze cantilever calibration data.
6 W. Trevor King Dec. 2007-Jan. 2008
11 The relevent physical quantities are :
12 Vzp_out Output z-piezo voltage (what we generate)
13 Vzp Applied z-piezo voltage (after external ZPGAIN)
14 Zp The z-piezo position
15 Zcant The cantilever vertical deflection
16 Vphoto The photodiode vertical deflection voltage (what we measure)
17 Fcant The force on the cantilever
18 T The temperature of the cantilever and surrounding solution
19 (another thing we measure or guess)
20 k_b Boltzmann's constant
22 Which are related by the parameters :
24 zpSensitivity Zp / Vzp
25 photoSensitivity Vphoto / Zcant
28 Cantilever calibration assumes a pre-calibrated z-piezo
29 (zpSensitivity) and a amplifier (zpGain). In our lab, the z-piezo is
30 calibrated by imaging a calibration sample, which has features with
31 well defined sizes, and the gain is set with a knob on the Nanoscope.
33 photoSensitivity is measured by bumping the cantilever against the
34 surface, where Zp = Zcant (see bump_aquire() and the bump_analyze
37 k_cant is measured by watching the cantilever vibrate in free solution
38 (see the vib_aquire() and the vib_analyze submodule). The average
39 energy of the cantilever in the vertical direction is given by the
40 equipartition theorem.
41 1/2 k_b T = 1/2 k_cant <Zcant**2>
42 so k_cant = k_b T / Zcant**2
43 but Zcant = Vphoto / photoSensitivity
44 so k_cant = k_b T * photoSensitivty**2 / <Vphoto**2>
46 We measured photoSensitivity with the surface bumps. We can either
47 measure T using an external function (see temperature.py), or just
48 estimate it (see T_aquire() and the T_analyze submodule). Guessing
49 room temp ~22 deg C is actually fairly reasonable. Assuming the
50 actual fluid temperature is within +/- 5 deg, the error in the spring
51 constant k_cant is within 5/273.15 ~= 2%. A time series of Vphoto
52 while we're far from the surface and not changing Vzp_out will give us
53 the average variance <Vphoto**2>.
55 We do all these measurements a few times to estimate statistical
58 The functions are layed out in the families:
59 bump_*(), vib_*(), T_*(), and calib_*()
60 where calib_{save|load|analyze}() deal with derived data, not
63 For each family, * can be any of :
64 aquire get real-world data
65 save store real-world data to disk
66 load get real-world data from disk
67 analyze interperate the real-world data.
68 plot show a nice graphic to convince people we're working :p
70 read a file with a list of paths to previously saved
71 real world data load each file using *_load(), analyze
72 using *_analyze(), and optionally plot using *_plot().
73 Intended for re-processing old data.
74 A family name without any _* extension (e.g. bump()), runs *_aquire(),
75 *_save(), *_analyze(), *_plot().
77 We also define the two positioning functions:
78 move_just_onto_surface() and move_far_from_surface()
79 which make automating the calibration procedure more straightforward.
85 from splittable_kwargs import splittableKwargsFunction, \
86 make_splittable_kwargs_function
98 @splittableKwargsFunction()
99 def bump_aquire(zpiezo, push_depth=200, npoints=1024, freq=100e3) :
101 Ramps closer push_depth and returns to the original position.
103 zpiezo an opened zpiezo.zpiezo instance
104 push_depth distance to approach, in nm
105 npoints number points during the approach and during the retreat
106 freq rate at which data is aquired
107 Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
109 # generate the bump output
110 start_pos = zpiezo.curPos()
111 pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
112 close_pos = start_pos + pos_dist
113 appr = numpy.linspace(start_pos, close_pos, npoints)
114 retr = numpy.linspace(close_pos, start_pos, npoints)
115 out = numpy.concatenate((appr, retr))
116 # run the bump, and measure deflection
117 if config.TEXT_VERBOSE :
118 print "Bump %g nm" % push_depth
119 data = zpiezo.ramp(out, freq)
122 @splittableKwargsFunction(bump_aquire,
123 (bump_analyze.bump_save, 'data'),
124 (bump_analyze.bump_analyze, 'data'))
127 Wrapper around bump_aquire(), bump_save(), bump_analyze()
129 bump_aquire_kwargs,bump_save_kwargs,bump_analyze_kwargs = \
130 bump._splitargs(bump, kwargs)
131 data = bump_aquire(**bump_aquire_kwargs)
132 bump_analyze.bump_save(data, **bump_save_kwargs)
133 photoSensitivity = bump_analyze.bump_analyze(data, **bump_analyze_kwargs)
134 return photoSensitivity
137 # Fairly stubby, since a one shot Temp measurement is a common thing.
138 # We just wrap that to provide a consistent interface.
140 @splittableKwargsFunction()
141 def T_aquire(get_T=None) :
143 Measure the current temperature of the sample,
144 or, if get_T == None, fake it by returning config.DEFAULT_TEMP
147 if config.TEXT_VERBOSE :
148 print "Fake temperature %g" % config.DEFAULT_TEMP
149 return config.DEFAULT_TEMP
151 if config.TEXT_VERBOSE :
152 print "Measure temperature"
155 @splittableKwargsFunction(T_aquire,
156 (T_analyze.T_save, 'T'),
157 (T_analyze.T_analyze, 'T'))
160 Wrapper around T_aquire(), T_save(), T_analyze(), T_plot()
162 T_aquire_kwargs,T_save_kwargs,T_analyze_kwargs = \
163 T._splitargs(T, kwargs)
164 T_raw = T_aquire(**T_aquire_kwargs)
165 T_analyze.T_save(T_raw, **T_save_kwargs)
166 T_ret = T_analyze.T_analyze(T_raw, **T_analyze_kwargs) # returns array
171 @splittableKwargsFunction()
172 def vib_aquire(zpiezo, time=1, freq=50e3) :
174 Record data for TIME seconds at FREQ Hz from ZPIEZO at it's current position.
176 # round up to the nearest power of two, for efficient FFT-ing
177 nsamps = FFT_tools.ceil_pow_of_two(time*freq)
179 # take some data, keeping the position voltage at it's current value
180 out = numpy.ones((nsamps,), dtype=numpy.uint16) * zpiezo.curPos()
181 if config.TEXT_VERBOSE :
182 print "get %g seconds of data" % time
183 data = zpiezo.ramp(out, freq)
184 data['sample frequency Hz'] = numpy.array([freq])
187 @splittableKwargsFunction(vib_aquire,
188 (vib_analyze.vib_save, 'data'),
189 (vib_analyze.vib_analyze, 'deflection_bits', 'freq'))
192 Wrapper around vib_aquire(), vib_save(), vib_analyze()
194 vib_aquire_kwargs,vib_save_kwargs,vib_analyze_kwargs = \
195 vib._splitargs(vib, kwargs)
196 data = vib_aquire(**vib_aquire_kwargs)
197 vib_analyze.vib_save(data, **vib_save_kwargs)
198 freq = data['sample frequency Hz']
199 deflection_bits = data['Deflection input']
200 Vphoto_var = vib_analyze.vib_analyze(deflection_bits=deflection_bits,
201 freq=freq, **vib_analyze_kwargs)
204 # A few positioning functions, so we can run bump_aquire() and vib_aquire()
205 # with proper spacing relative to the surface.
207 @splittableKwargsFunction()
208 def move_just_onto_surface(stepper, zpiezo, Depth_nm=100, setpoint=2) :
210 Uses z_piezo_utils.getSurfPos() to pinpoint the position of the surface.
211 Adjusts the stepper position as required to get within stepper_tol nm
213 Then set Vzp to place the cantilever Depth_nm onto the surface.
215 If getSurfPos() fails to find the surface, backs off (for safety)
216 and steps in (without moving the zpiezo) until Vphoto > setpoint.
218 stepper_tol = 250 # nm, generous estimate of the fullstep stepsize
220 if config.TEXT_VERBOSE :
221 print "moving just onto surface"
223 if config.TEXT_VERBOSE :
224 print "zero the z piezo output"
225 zpiezo.jumpToPos(zpiezo.pos_nm2out(0))
226 # See if we're near the surface already
227 if config.TEXT_VERBOSE :
228 print "See if we're starting near the surface"
230 dist = zpiezo.pos_out2nm( \
231 z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))
233 except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
234 if config.TEXT_VERBOSE :
235 print "distance failed with: ", string
236 print "Back off 200 half steps"
237 # Back away 200 steps
238 stepper.step_rel(-400)
239 stepper.step_rel(200)
240 sp = zpiezo.def_V2in(setpoint) # sp = setpoint in bits
241 zpiezo.updateInputs()
242 cd = zpiezo.curDef() # cd = current deflection in bits
243 if config.TEXT_VERBOSE :
244 print "Single stepping approach"
246 if config.TEXT_VERBOSE :
247 print "deflection %g < setpoint %g. step closer" % (cd, sp)
248 stepper.step_rel(2) # Full step in
249 zpiezo.updateInputs()
251 # Back off two steps (protecting against backlash)
252 if config.TEXT_VERBOSE :
253 print "Step back 4 half steps to get off the setpoint"
254 stepper.step_rel(-200)
255 stepper.step_rel(196)
256 # get the distance to the surface
257 zpiezo.updateInputs()
258 if config.TEXT_VERBOSE :
259 print "get surf pos, with setpoint %g (%d)" % (setpoint, zpiezo.def_V2in(setpoint))
260 for i in range(20) : # HACK, keep stepping back until we get a distance
262 dist = zpiezo.pos_out2nm( \
263 z_piezo_utils.getSurfPos(zpiezo,zpiezo.def_V2in(setpoint)))
264 except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
265 stepper.step_rel(-200)
266 stepper.step_rel(198)
270 print "tried %d times, still too close! bailing" % i
271 print "probably an invalid setpoint."
272 raise Exception, "weirdness"
273 if config.TEXT_VERBOSE :
274 print 'distance to surface ', dist, ' nm'
275 # fine tune the stepper position
276 while dist < -stepper_tol : # step back if we need to
277 stepper.step_rel(-200)
278 stepper.step_rel(198)
279 dist = zpiezo.pos_out2nm( \
280 z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
281 if config.TEXT_VERBOSE :
282 print 'distance to surface ', dist, ' nm, step back'
283 while dist > stepper_tol : # and step forward if we need to
285 dist = zpiezo.pos_out2nm( \
286 z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
287 if config.TEXT_VERBOSE :
288 print 'distance to surface ', dist, ' nm, step closer'
289 # now adjust the zpiezo to place us just onto the surface
290 target = dist + Depth_nm
291 zpiezo.jumpToPos(zpiezo.pos_nm2out(target))
293 if config.TEXT_VERBOSE :
294 print "We're %g nm into the surface" % Depth_nm
296 @splittableKwargsFunction()
297 def move_far_from_surface(stepper, um_back=50) :
299 Step back a specified number of microns.
300 (uses very rough estimate of step distance at the moment)
303 steps = int(um_back*1000/step_nm)
304 print "step back %d steps" % steps
305 stepper.step_rel(-steps)
308 # and finally, the calib family
310 @splittableKwargsFunction((move_just_onto_surface, 'stepper', 'zpiezo'),
311 (bump, 'zpiezo', 'freq', 'log_dir', 'Vphoto_in2V'),
312 (move_far_from_surface, 'stepper'),
314 (vib, 'zpiezo', 'log_dir', 'Vphoto_in2V'),
315 (analyze.calib_save, 'bumps','Ts','vibs','log_dir'))
316 def calib_aquire(stepper, zpiezo, num_bumps=10, num_Ts=10, num_vibs=20,
318 log_dir=config.LOG_DIR, Vphoto_in2V=config.Vphoto_in2V,
321 Aquire data for calibrating a cantilever in one function.
322 return (bump, T, vib), each of which is an array.
324 stepper a stepper.stepper_obj for coarse Z positioning
325 zpiezo a z_piezo.z_piezo for fine positioning and deflection readin
326 setpoint maximum allowed deflection (in Volts) during approaches
327 num_bumps number of 'a's (see Outputs)
328 push_depth_nm depth of each push when generating a
329 num_temps number of 'T's (see Outputs)
330 num_vibs number of 'vib's (see Outputs)
331 log_dir directory to log data to. Default 'None' disables logging.
332 Outputs (all are arrays of recorded data) :
333 bumps measured (V_photodiode / nm_tip) proportionality constant
334 Ts measured temperature (K)
335 vibs measured V_photodiode variance in free solution
337 move_just_onto_surface_kwargs,bump_kwargs,move_far_from_surface_kwargs, \
338 T_kwargs,vib_kwargs,calib_save_kwargs = \
339 calib_aquire._splitargs(calib_aquire, kwargs)
341 move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
342 bumps = numpy.zeros((num_bumps,), dtype=numpy.float)
343 for i in range(num_bumps) :
344 bumps[i] = bump(zpiezo=zpiezo, freq=bump_freq, log_dir=log_dir,
345 Vphoto_in2V=Vphoto_in2V, **bump_kwargs)
346 if config.TEXT_VERBOSE :
349 move_far_from_surface(stepper, **move_far_from_surface_kwargs)
352 Ts = numpy.zeros((num_Ts,), dtype=numpy.float)
353 for i in range(num_Ts) :
354 Ts[i] = T(**T_kwargs)
355 time.sleep(1) # wait a bit to get an independent temperature measure
359 vibs = numpy.zeros((num_vibs,), dtype=numpy.float)
360 for i in range(num_vibs) :
361 vibs[i] = vib(zpiezo=zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V,
365 analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs)
367 return (bumps, Ts, vibs)
370 @splittableKwargsFunction( \
371 (calib_aquire, 'log_dir'),
372 (analyze.calib_analyze, 'bumps','Ts','vibs'))
373 def calib(log_dir=config.LOG_DIR, **kwargs) :
375 Calibrate a cantilever in one function.
376 The I-don't-care-about-the-details black box version :p.
381 k cantilever spring constant (in N/m, or equivalently nN/nm)
382 k_s standard deviation in our estimate of k
384 See get_calibration_data() for the data aquisition code
385 See analyze_calibration_data() for the analysis code
387 calib_aquire_kwargs,calib_analyze_kwargs = \
388 calib._splitargs(calib, kwargs)
389 a, T, vib = calib_aquire(**calib_aquire_kwargs)
390 k,k_s,ps2_m, ps2_s,T_m,T_s,one_o_Vp2_m,one_o_Vp2_s = \
391 analyze.calib_analyze(a, T, vib, **calib_analyze_kwargs)
392 analyze.calib_save_analysis(k, k_s, ps2_m, ps2_s, T_m, T_s,
393 one_o_Vp2_m, one_o_Vp2_s, log_dir)