calibrate.py should now work.
authorW. Trevor King <wking@drexel.edu>
Sun, 21 Dec 2008 05:01:11 +0000 (00:01 -0500)
committerW. Trevor King <wking@drexel.edu>
Sun, 21 Dec 2008 05:01:11 +0000 (00:01 -0500)
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 [new file with mode: 0644]

diff --git a/calibcant/calibrate.py b/calibcant/calibrate.py
new file mode 100644 (file)
index 0000000..64ac942
--- /dev/null
@@ -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 <Zcant**2>
+ so     k_cant  = k_b T / Zcant**2
+ but    Zcant   = Vphoto / photoSensitivity
+ so     k_cant  = k_b T * photoSensitivty**2 / <Vphoto**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 <Vphoto**2>.
+
+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)
+
+    
+