From ef7adeb36ecac44c154c8b666668e229581a194b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 21 Dec 2008 00:01:11 -0500 Subject: [PATCH] calibrate.py should now work. A bunch of changes in one commit, sorry. Moved to fledgling splittable_kwargs system to make default argument maintenance easier. I expect the splittable_kwargs system still has some growing to do, but it's already better than the old system. Merged BE database from the calibcant subdir into the main BE database. It was a mistake to create the database there in the first place. --- calibcant/calibrate.py | 393 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 calibcant/calibrate.py diff --git a/calibcant/calibrate.py b/calibcant/calibrate.py new file mode 100644 index 0000000..64ac942 --- /dev/null +++ b/calibcant/calibrate.py @@ -0,0 +1,393 @@ +#!/usr/bin/python + +""" +Aquire and analyze cantilever calibration data. + +W. Trevor King Dec. 2007-Jan. 2008 + +GPL BOILERPLATE + + +The relevent physical quantities are : + Vzp_out Output z-piezo voltage (what we generate) + Vzp Applied z-piezo voltage (after external ZPGAIN) + Zp The z-piezo position + Zcant The cantilever vertical deflection + Vphoto The photodiode vertical deflection voltage (what we measure) + Fcant The force on the cantilever + T The temperature of the cantilever and surrounding solution + (another thing we measure or guess) + k_b Boltzmann's constant + +Which are related by the parameters : + zpGain Vzp_out / Vzp + zpSensitivity Zp / Vzp + photoSensitivity Vphoto / Zcant + k_cant Fcant / Zcant + +Cantilever calibration assumes a pre-calibrated z-piezo +(zpSensitivity) and a amplifier (zpGain). In our lab, the z-piezo is +calibrated by imaging a calibration sample, which has features with +well defined sizes, and the gain is set with a knob on the Nanoscope. + +photoSensitivity is measured by bumping the cantilever against the +surface, where Zp = Zcant (see bump_aquire() and the bump_analyze +submodule). + +k_cant is measured by watching the cantilever vibrate in free solution +(see the vib_aquire() and the vib_analyze submodule). The average +energy of the cantilever in the vertical direction is given by the +equipartition theorem. + 1/2 k_b T = 1/2 k_cant + so k_cant = k_b T / Zcant**2 + but Zcant = Vphoto / photoSensitivity + so k_cant = k_b T * photoSensitivty**2 / + +We measured photoSensitivity with the surface bumps. We can either +measure T using an external function (see temperature.py), or just +estimate it (see T_aquire() and the T_analyze submodule). Guessing +room temp ~22 deg C is actually fairly reasonable. Assuming the +actual fluid temperature is within +/- 5 deg, the error in the spring +constant k_cant is within 5/273.15 ~= 2%. A time series of Vphoto +while we're far from the surface and not changing Vzp_out will give us +the average variance . + +We do all these measurements a few times to estimate statistical +errors. + +The functions are layed out in the families: + bump_*(), vib_*(), T_*(), and calib_*() +where calib_{save|load|analyze}() deal with derived data, not +real-world data. + +For each family, * can be any of : + aquire get real-world data + save store real-world data to disk + load get real-world data from disk + analyze interperate the real-world data. + plot show a nice graphic to convince people we're working :p + load_analyze_tweaked + read a file with a list of paths to previously saved + real world data load each file using *_load(), analyze + using *_analyze(), and optionally plot using *_plot(). + Intended for re-processing old data. +A family name without any _* extension (e.g. bump()), runs *_aquire(), + *_save(), *_analyze(), *_plot(). + +We also define the two positioning functions: + move_just_onto_surface() and move_far_from_surface() +which make automating the calibration procedure more straightforward. +""" + +import numpy +import time +import z_piezo_utils +from splittable_kwargs import splittableKwargsFunction, \ + make_splittable_kwargs_function + +import common +import config +import bump_analyze +import T_analyze +import vib_analyze +import analyze + +# bump family + +@splittableKwargsFunction() +def bump_aquire(zpiezo, push_depth=200, npoints=1024, freq=100e3) : + """ + Ramps closer push_depth and returns to the original position. + Inputs: + zpiezo an opened zpiezo.zpiezo instance + push_depth distance to approach, in nm + npoints number points during the approach and during the retreat + freq rate at which data is aquired + Returns the aquired ramp data dictionary, with data in DAC/ADC bits. + """ + # generate the bump output + start_pos = zpiezo.curPos() + pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0) + close_pos = start_pos + pos_dist + appr = linspace(start_pos, close_pos, npoints) + retr = linspace(close_pos, start_pos, npoints) + out = concatenate((appr, retr)) + # run the bump, and measure deflection + if config.TEXT_VERBOSE : + print "Bump %g nm" % push_depth + data = zpiezo.ramp(out, freq) + return data + +@splittableKwargsFunction(bump_aquire, + (bump_analyze.bump_save, 'data'), + (bump_analyze.bump_analyze, 'data')) +def bump(**kwargs): + """ + Wrapper around bump_aquire(), bump_save(), bump_analyze() + """ + bump_aquire_kwargs,bump_save_kwargs,bump_analyze_kwargs = \ + bump._splitargs(bump, kwargs) + data = bump_aquire(**bump_aquire_kwargs) + bump_analyze.bump_save(data, **bump_save_kwargs) + photoSensitivity = bump_analyze.bump_analyze(data, **bump_analyze_kwargs) + return photoSensitivity + +# T family. +# Fairly stubby, since a one shot Temp measurement is a common thing. +# We just wrap that to provide a consistent interface. + +@splittableKwargsFunction() +def T_aquire(get_T=None) : + """ + Measure the current temperature of the sample, + or, if get_T == None, fake it by returning config.DEFAULT_TEMP + """ + if get_T == None : + if config.TEXT_VERBOSE : + print "Fake temperature %g" % config.DEFAULT_TEMP + return config.DEFAULT_TEMP + else : + if config.TEXT_VERBOSE : + print "Measure temperature" + return get_T() + +@splittableKwargsFunction(T_aquire, + (T_analyze.T_save, 'T'), + (T_analyze.T_analyze, 'T')) +def T(**kwargs): + """ + Wrapper around T_aquire(), T_save(), T_analyze(), T_plot() + """ + T_aquire_kwargs,T_save_kwargs,T_analyze_kwargs = \ + T._splitargs(T, kwargs) + T_raw = T_aquire(**T_aquire_kwargs) + T_analyze.T_save(T_raw, **T_save_kwargs) + T_ret = T_analyze.T_analyze(T_raw, **T_analyze_kwargs) + return T_ret + +# vib family + +@splittableKwargsFunction() +def vib_aquire(zpiezo, time=1, freq=50e3) : + """ + Record data for TIME seconds at FREQ Hz from ZPIEZO at it's current position. + """ + # round up to the nearest power of two, for efficient FFT-ing + nsamps = ceil_pow_of_two(time*freq) + time = nsamps / freq + # take some data, keeping the position voltage at it's current value + out = ones((nsamps,), dtype=uint16) * zpiezo.curPos() + if config.TEXT_VERBOSE : + print "get %g seconds of data" % time + data = zpiezo.ramp(out, freq) + data['sample frequency Hz'] = freq + return data + +@splittableKwargsFunction(vib_aquire, + (vib_analyze.vib_save, 'data'), + (vib_analyze.vib_analyze, 'deflection_bits', 'freq')) +def vib(**kwargs) : + """ + Wrapper around vib_aquire(), vib_save(), vib_analyze() + """ + vib_aquire_kwargs,vib_save_kwargs,vib_analyze_kwargs = \ + vib._splitargs(vib, kwargs) + data = vib_aquire(freq=freq, **vib_aquire_kwargs) + vib_analyze.vib_save(data, **vib_save_kwargs) + freq = data['sample frequency Hz'] + Vphoto_var = vib_analyze.vib_analyze(deflection_bits=data, freq=freq, + **vib_analyze_kwargs) + return Vphoto_var + +# A few positioning functions, so we can run bump_aquire() and vib_aquire() +# with proper spacing relative to the surface. + +@splittableKwargsFunction() +def move_just_onto_surface(stepper, zpiezo, Depth_nm=100, setpoint=2) : + """ + Uses z_piezo_utils.getSurfPos() to pinpoint the position of the surface. + Adjusts the stepper position as required to get within stepper_tol nm + of the surface. + Then set Vzp to place the cantilever Depth_nm onto the surface. + + If getSurfPos() fails to find the surface, backs off (for safety) + and steps in (without moving the zpiezo) until Vphoto > setpoint. + """ + stepper_tol = 250 # nm, generous estimate of the fullstep stepsize + + if config.TEXT_VERBOSE : + print "moving just onto surface" + # Zero the piezo + if config.TEXT_VERBOSE : + print "zero the z piezo output" + zpiezo.jumpToPos(zpiezo.pos_nm2out(0)) + # See if we're near the surface already + if config.TEXT_VERBOSE : + print "See if we're starting near the surface" + try : + dist = zpiezo.pos_out2nm( \ + z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)) + ) + except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string : + if config.TEXT_VERBOSE : + print "distance failed with: ", string + print "Back off 200 half steps" + # Back away 200 steps + stepper.step_rel(-400) + stepper.step_rel(200) + sp = zpiezo.def_V2in(setpoint) # sp = setpoint in bits + zpiezo.updateInputs() + cd = zpiezo.curDef() # cd = current deflection in bits + if config.TEXT_VERBOSE : + print "Single stepping approach" + while cd < sp : + if config.TEXT_VERBOSE : + print "deflection %g < setpoint %g. step closer" % (cd, sp) + stepper.step_rel(2) # Full step in + zpiezo.updateInputs() + cd = zpiezo.curDef() + # Back off two steps (protecting against backlash) + if config.TEXT_VERBOSE : + print "Step back 4 half steps to get off the setpoint" + stepper.step_rel(-200) + stepper.step_rel(196) + # get the distance to the surface + zpiezo.updateInputs() + if config.TEXT_VERBOSE : + print "get surf pos, with setpoint %g (%d)" % (setpoint, zpiezo.def_V2in(setpoint)) + for i in range(20) : # HACK, keep stepping back until we get a distance + try : + dist = zpiezo.pos_out2nm(getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))) + except (tooClose, poorFit), string : + stepper.step_rel(-200) + stepper.step_rel(198) + continue + break + if i >= 19 : + print "tried %d times, still too close! bailing" % i + print "probably an invalid setpoint." + raise Exception, "weirdness" + if config.TEXT_VERBOSE : + print 'distance to surface ', dist, ' nm' + # fine tune the stepper position + while dist < -stepper_tol : # step back if we need to + stepper.step_rel(-200) + stepper.step_rel(198) + dist = zpiezo.pos_out2nm(getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))) + if config.TEXT_VERBOSE : + print 'distance to surface ', dist, ' nm, step back' + while dist > stepper_tol : # and step forward if we need to + stepper.step_rel(2) + dist = zpiezo.pos_out2nm(getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))) + if config.TEXT_VERBOSE : + print 'distance to surface ', dist, ' nm, step closer' + # now adjust the zpiezo to place us just onto the surface + target = dist + Depth_nm + zpiezo.jumpToPos(zpiezo.pos_nm2out(target)) + # and we're there :) + if config.TEXT_VERBOSE : + print "We're %g nm into the surface" % Depth_nm + +@splittableKwargsFunction() +def move_far_from_surface(stepper, um_back=50) : + """ + Step back a specified number of microns. + (uses very rough estimate of step distance at the moment) + """ + step_nm = 100 + steps = int(um_back*1000/step_nm) + print "step back %d steps" % steps + stepper.step_rel(-steps) + + +# and finally, the calib family + +@splittableKwargsFunction((move_just_onto_surface, 'stepper', 'zpiezo'), + (bump, 'zpiezo', 'freq', 'log_dir', 'Vphoto_in2V'), + (move_far_from_surface, 'stepper'), + (T, 'log_dir'), + (vib, 'zpiezo', 'log_dir', 'Vphoto_in2V'), + (analyze.calib_save, 'bumps','Ts','vibs','log_dir')) +def calib_aquire(stepper, zpiezo, num_bumps=10, num_Ts=10, num_vibs=20, + bump_freq=100e3, + log_dir=config.LOG_DATA, Vphoto_in2V=config.Vphoto_in2V, + **kwargs): + """ + Aquire data for calibrating a cantilever in one function. + return (bump, T, vib), each of which is an array. + Inputs : + stepper a stepper.stepper_obj for coarse Z positioning + zpiezo a z_piezo.z_piezo for fine positioning and deflection readin + setpoint maximum allowed deflection (in Volts) during approaches + num_bumps number of 'a's (see Outputs) + push_depth_nm depth of each push when generating a + num_temps number of 'T's (see Outputs) + num_vibs number of 'vib's (see Outputs) + log_dir directory to log data to. Default 'None' disables logging. + Outputs (all are arrays of recorded data) : + bumps measured (V_photodiode / nm_tip) proportionality constant + Ts measured temperature (K) + vibs measured V_photodiode variance in free solution + """ + move_just_onto_surface_kwargs,bump_kwargs,move_far_from_surface_kwargs, \ + T_kwargs,vib_kwargs,calib_save_kwargs = \ + calib_aquire._splitargs(calib_aquire, kwargs) + # get bumps + move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs) + bumps=zeros((num_bumps,)) + for i in range(num_bumps) : + bumps[i] = bump(zpiezo, freq=bump_freq, log_dir=log_dir, + Vphot_in2V=Vphoto_in2V, **bump_kwargs) + if config.TEXT_VERBOSE : + print bumps + + move_far_from_surface(stepper, **move_far_from_surface_kwargs) + + # get Ts + Ts=zeros((num_Ts,), dtype=float) + for i in range(num_Ts) : + Ts[i] = T(**T_kwargs) + time.sleep(1) # wait a bit to get an independent temperature measure + print Ts + + # get vibs + vibs=zeros((num_vibs,)) + for i in range(num_vibs) : + vibs[i] = vib(zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V, + **vib_kwargs) + print vibs + + analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs) + + return (bumps, Ts, vibs) + + +@splittableKwargsFunction( \ + (calib_aquire, 'log_dir'), + (analyze.calib_analyze, 'bumps','Ts','vibs')) +def calib(log_dir=None, **kwargs) : + """ + Calibrate a cantilever in one function. + The I-don't-care-about-the-details black box version :p. + return (k, k_s) + Inputs: + (see calib_aquire()) + Outputs : + k cantilever spring constant (in N/m, or equivalently nN/nm) + k_s standard deviation in our estimate of k + Notes : + See get_calibration_data() for the data aquisition code + See analyze_calibration_data() for the analysis code + """ + calib_aquire_kwargs,calib_analyze_kwargs = \ + calib._splitargs(calib, kwargs) + a, T, vib = calib_aquire(**calib_aquire_kwargs) + k,k_s,ps2_m, ps2_s,T_m,T_s,one_o_Vp2_m,one_o_Vp2_s = \ + analyze.calib_analyze(a, T, vib, log_dir=log_dir, + **calib_analyze_kwargs) + analyze.calib_save_analysis(k, k_s, ps2_m, ps2_s, T_m, T_s, + one_o_Vp2_m, one_o_Vp2_s, log_dir) + return (k, k_s) + + + -- 2.26.2