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
97 @splittableKwargsFunction()
98 def bump_aquire(zpiezo, push_depth=200, npoints=1024, freq=100e3) :
100 Ramps closer push_depth and returns to the original position.
102 zpiezo an opened zpiezo.zpiezo instance
103 push_depth distance to approach, in nm
104 npoints number points during the approach and during the retreat
105 freq rate at which data is aquired
106 Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
108 # generate the bump output
109 start_pos = zpiezo.curPos()
110 pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
111 close_pos = start_pos + pos_dist
112 appr = linspace(start_pos, close_pos, npoints)
113 retr = linspace(close_pos, start_pos, npoints)
114 out = concatenate((appr, retr))
115 # run the bump, and measure deflection
116 if config.TEXT_VERBOSE :
117 print "Bump %g nm" % push_depth
118 data = zpiezo.ramp(out, freq)
121 @splittableKwargsFunction(bump_aquire,
122 (bump_analyze.bump_save, 'data'),
123 (bump_analyze.bump_analyze, 'data'))
126 Wrapper around bump_aquire(), bump_save(), bump_analyze()
128 bump_aquire_kwargs,bump_save_kwargs,bump_analyze_kwargs = \
129 bump._splitargs(bump, kwargs)
130 data = bump_aquire(**bump_aquire_kwargs)
131 bump_analyze.bump_save(data, **bump_save_kwargs)
132 photoSensitivity = bump_analyze.bump_analyze(data, **bump_analyze_kwargs)
133 return photoSensitivity
136 # Fairly stubby, since a one shot Temp measurement is a common thing.
137 # We just wrap that to provide a consistent interface.
139 @splittableKwargsFunction()
140 def T_aquire(get_T=None) :
142 Measure the current temperature of the sample,
143 or, if get_T == None, fake it by returning config.DEFAULT_TEMP
146 if config.TEXT_VERBOSE :
147 print "Fake temperature %g" % config.DEFAULT_TEMP
148 return config.DEFAULT_TEMP
150 if config.TEXT_VERBOSE :
151 print "Measure temperature"
154 @splittableKwargsFunction(T_aquire,
155 (T_analyze.T_save, 'T'),
156 (T_analyze.T_analyze, 'T'))
159 Wrapper around T_aquire(), T_save(), T_analyze(), T_plot()
161 T_aquire_kwargs,T_save_kwargs,T_analyze_kwargs = \
162 T._splitargs(T, kwargs)
163 T_raw = T_aquire(**T_aquire_kwargs)
164 T_analyze.T_save(T_raw, **T_save_kwargs)
165 T_ret = T_analyze.T_analyze(T_raw, **T_analyze_kwargs)
170 @splittableKwargsFunction()
171 def vib_aquire(zpiezo, time=1, freq=50e3) :
173 Record data for TIME seconds at FREQ Hz from ZPIEZO at it's current position.
175 # round up to the nearest power of two, for efficient FFT-ing
176 nsamps = ceil_pow_of_two(time*freq)
178 # take some data, keeping the position voltage at it's current value
179 out = ones((nsamps,), dtype=uint16) * zpiezo.curPos()
180 if config.TEXT_VERBOSE :
181 print "get %g seconds of data" % time
182 data = zpiezo.ramp(out, freq)
183 data['sample frequency Hz'] = freq
186 @splittableKwargsFunction(vib_aquire,
187 (vib_analyze.vib_save, 'data'),
188 (vib_analyze.vib_analyze, 'deflection_bits', 'freq'))
191 Wrapper around vib_aquire(), vib_save(), vib_analyze()
193 vib_aquire_kwargs,vib_save_kwargs,vib_analyze_kwargs = \
194 vib._splitargs(vib, kwargs)
195 data = vib_aquire(freq=freq, **vib_aquire_kwargs)
196 vib_analyze.vib_save(data, **vib_save_kwargs)
197 freq = data['sample frequency Hz']
198 Vphoto_var = vib_analyze.vib_analyze(deflection_bits=data, freq=freq,
199 **vib_analyze_kwargs)
202 # A few positioning functions, so we can run bump_aquire() and vib_aquire()
203 # with proper spacing relative to the surface.
205 @splittableKwargsFunction()
206 def move_just_onto_surface(stepper, zpiezo, Depth_nm=100, setpoint=2) :
208 Uses z_piezo_utils.getSurfPos() to pinpoint the position of the surface.
209 Adjusts the stepper position as required to get within stepper_tol nm
211 Then set Vzp to place the cantilever Depth_nm onto the surface.
213 If getSurfPos() fails to find the surface, backs off (for safety)
214 and steps in (without moving the zpiezo) until Vphoto > setpoint.
216 stepper_tol = 250 # nm, generous estimate of the fullstep stepsize
218 if config.TEXT_VERBOSE :
219 print "moving just onto surface"
221 if config.TEXT_VERBOSE :
222 print "zero the z piezo output"
223 zpiezo.jumpToPos(zpiezo.pos_nm2out(0))
224 # See if we're near the surface already
225 if config.TEXT_VERBOSE :
226 print "See if we're starting near the surface"
228 dist = zpiezo.pos_out2nm( \
229 z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))
231 except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
232 if config.TEXT_VERBOSE :
233 print "distance failed with: ", string
234 print "Back off 200 half steps"
235 # Back away 200 steps
236 stepper.step_rel(-400)
237 stepper.step_rel(200)
238 sp = zpiezo.def_V2in(setpoint) # sp = setpoint in bits
239 zpiezo.updateInputs()
240 cd = zpiezo.curDef() # cd = current deflection in bits
241 if config.TEXT_VERBOSE :
242 print "Single stepping approach"
244 if config.TEXT_VERBOSE :
245 print "deflection %g < setpoint %g. step closer" % (cd, sp)
246 stepper.step_rel(2) # Full step in
247 zpiezo.updateInputs()
249 # Back off two steps (protecting against backlash)
250 if config.TEXT_VERBOSE :
251 print "Step back 4 half steps to get off the setpoint"
252 stepper.step_rel(-200)
253 stepper.step_rel(196)
254 # get the distance to the surface
255 zpiezo.updateInputs()
256 if config.TEXT_VERBOSE :
257 print "get surf pos, with setpoint %g (%d)" % (setpoint, zpiezo.def_V2in(setpoint))
258 for i in range(20) : # HACK, keep stepping back until we get a distance
260 dist = zpiezo.pos_out2nm(getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
261 except (tooClose, poorFit), string :
262 stepper.step_rel(-200)
263 stepper.step_rel(198)
267 print "tried %d times, still too close! bailing" % i
268 print "probably an invalid setpoint."
269 raise Exception, "weirdness"
270 if config.TEXT_VERBOSE :
271 print 'distance to surface ', dist, ' nm'
272 # fine tune the stepper position
273 while dist < -stepper_tol : # step back if we need to
274 stepper.step_rel(-200)
275 stepper.step_rel(198)
276 dist = zpiezo.pos_out2nm(getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
277 if config.TEXT_VERBOSE :
278 print 'distance to surface ', dist, ' nm, step back'
279 while dist > stepper_tol : # and step forward if we need to
281 dist = zpiezo.pos_out2nm(getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
282 if config.TEXT_VERBOSE :
283 print 'distance to surface ', dist, ' nm, step closer'
284 # now adjust the zpiezo to place us just onto the surface
285 target = dist + Depth_nm
286 zpiezo.jumpToPos(zpiezo.pos_nm2out(target))
288 if config.TEXT_VERBOSE :
289 print "We're %g nm into the surface" % Depth_nm
291 @splittableKwargsFunction()
292 def move_far_from_surface(stepper, um_back=50) :
294 Step back a specified number of microns.
295 (uses very rough estimate of step distance at the moment)
298 steps = int(um_back*1000/step_nm)
299 print "step back %d steps" % steps
300 stepper.step_rel(-steps)
303 # and finally, the calib family
305 @splittableKwargsFunction((move_just_onto_surface, 'stepper', 'zpiezo'),
306 (bump, 'zpiezo', 'freq', 'log_dir', 'Vphoto_in2V'),
307 (move_far_from_surface, 'stepper'),
309 (vib, 'zpiezo', 'log_dir', 'Vphoto_in2V'),
310 (analyze.calib_save, 'bumps','Ts','vibs','log_dir'))
311 def calib_aquire(stepper, zpiezo, num_bumps=10, num_Ts=10, num_vibs=20,
313 log_dir=config.LOG_DATA, Vphoto_in2V=config.Vphoto_in2V,
316 Aquire data for calibrating a cantilever in one function.
317 return (bump, T, vib), each of which is an array.
319 stepper a stepper.stepper_obj for coarse Z positioning
320 zpiezo a z_piezo.z_piezo for fine positioning and deflection readin
321 setpoint maximum allowed deflection (in Volts) during approaches
322 num_bumps number of 'a's (see Outputs)
323 push_depth_nm depth of each push when generating a
324 num_temps number of 'T's (see Outputs)
325 num_vibs number of 'vib's (see Outputs)
326 log_dir directory to log data to. Default 'None' disables logging.
327 Outputs (all are arrays of recorded data) :
328 bumps measured (V_photodiode / nm_tip) proportionality constant
329 Ts measured temperature (K)
330 vibs measured V_photodiode variance in free solution
332 move_just_onto_surface_kwargs,bump_kwargs,move_far_from_surface_kwargs, \
333 T_kwargs,vib_kwargs,calib_save_kwargs = \
334 calib_aquire._splitargs(calib_aquire, kwargs)
336 move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
337 bumps=zeros((num_bumps,))
338 for i in range(num_bumps) :
339 bumps[i] = bump(zpiezo, freq=bump_freq, log_dir=log_dir,
340 Vphot_in2V=Vphoto_in2V, **bump_kwargs)
341 if config.TEXT_VERBOSE :
344 move_far_from_surface(stepper, **move_far_from_surface_kwargs)
347 Ts=zeros((num_Ts,), dtype=float)
348 for i in range(num_Ts) :
349 Ts[i] = T(**T_kwargs)
350 time.sleep(1) # wait a bit to get an independent temperature measure
354 vibs=zeros((num_vibs,))
355 for i in range(num_vibs) :
356 vibs[i] = vib(zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V,
360 analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs)
362 return (bumps, Ts, vibs)
365 @splittableKwargsFunction( \
366 (calib_aquire, 'log_dir'),
367 (analyze.calib_analyze, 'bumps','Ts','vibs'))
368 def calib(log_dir=None, **kwargs) :
370 Calibrate a cantilever in one function.
371 The I-don't-care-about-the-details black box version :p.
376 k cantilever spring constant (in N/m, or equivalently nN/nm)
377 k_s standard deviation in our estimate of k
379 See get_calibration_data() for the data aquisition code
380 See analyze_calibration_data() for the analysis code
382 calib_aquire_kwargs,calib_analyze_kwargs = \
383 calib._splitargs(calib, kwargs)
384 a, T, vib = calib_aquire(**calib_aquire_kwargs)
385 k,k_s,ps2_m, ps2_s,T_m,T_s,one_o_Vp2_m,one_o_Vp2_s = \
386 analyze.calib_analyze(a, T, vib, log_dir=log_dir,
387 **calib_analyze_kwargs)
388 analyze.calib_save_analysis(k, k_s, ps2_m, ps2_s, T_m, T_s,
389 one_o_Vp2_m, one_o_Vp2_s, log_dir)