Massive rewrite (v 0.6) to base everything on Cython and revamped pypiezo.
authorW. Trevor King <wking@drexel.edu>
Thu, 21 Apr 2011 20:45:59 +0000 (16:45 -0400)
committerW. Trevor King <wking@drexel.edu>
Thu, 21 Apr 2011 20:45:59 +0000 (16:45 -0400)
calibcant/calibrate.py

index 54279a5652e0238e115c058f5ebc9c90c409c655..e8b7293c7760c9759916fea38f005b43f0cf5808 100644 (file)
 # License along with calibcant.  If not, see
 # <http://www.gnu.org/licenses/>.
 
-"""
-Aquire and analyze cantilever calibration data.
-
-W. Trevor King Dec. 2007-Jan. 2008
+"""Acquire and analyze cantilever calibration data.
 
-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
 
-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:
 
-Which are related by the parameters :
- zpGain           Vzp_out / Vzp
- zpSensitivity    Zp / Vzp
- photoSensitivity Vphoto / Zcant
- k_cant           Fcant / Zcant
+* 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
@@ -49,21 +45,31 @@ 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
+surface, where Zp = Zcant (see bump_acquire() 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
+(see the vib_acquire() 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>
+
+.. math::  \frac{1}{2} k_b T = \frac{1}{2} k_cant <Zcant**2>
+
+so
+
+.. math::   k_cant = \frac{k_b T}{Zcant**2}
+
+but
+
+.. math::   Zcant = \frac{Vphoto}{photoSensitivity}
+
+so
+
+.. math:: k_cant = \frac{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
+estimate it (see T_acquire() 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
@@ -73,351 +79,216 @@ 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.
-"""
+The functions are layed out in the families::
 
-import numpy
-import time 
+  bump_*(), vib_*(), T_*(), and calib_*()
 
-import FFT_tools
-import piezo.z_piezo_utils as z_piezo_utils
-from splittable_kwargs import splittableKwargsFunction, \
-    make_splittable_kwargs_function
+For each family, * can be any of:
 
-from . import common
-from . import config
-from . import bump_analyze
-from . import T_analyze
-from . import vib_analyze
-from . import analyze
+* acquire       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
 
+A family name without any `_*` extension (e.g. `bump()`), runs
+`*_acquire()`, `*_analyze()`, and `*_save()`.  `*_analyze()` will run
+`*_plot()` if `matplotlib` is set in `calibcant.base_config`.
+"""
 
-# bump family
+from numpy import zeros as _zeros
+from numpy import float as _float
+from time import sleep as _sleep
 
-@splittableKwargsFunction()
-def bump_aquire(zpiezo, push_depth=200, npoints=1024, push_speed=1000) :
-    """
-    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
-     push_speed piezo speed during approach and retreat, in nm/s
-    Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
-    """
-    # generate the bump output
-    nm_per_step = float(push_depth) / npoints
-    freq = push_speed / nm_per_step # freq is sample frequency in Hz
-    start_pos = zpiezo.curPos()
-    pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
-    close_pos = start_pos + pos_dist
-    appr = numpy.linspace(start_pos, close_pos, npoints)
-    retr = numpy.linspace(close_pos, start_pos, npoints)
-    out = numpy.concatenate((appr, retr))
-    # run the bump, and measure deflection
-    if config.TEXT_VERBOSE :
-        print "Bump %g nm at %g nm/s" % (push_depth, push_speed)
-    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) # returns array
-    return T_ret[0]
+from . import LOG as _LOG
 
-# vib family
+from .bump import bump as _bump
+from .T import T as _T
+from .vib import vib as _vib
+from .analyze import calib_analyze as _calib_analyze
+from .analyze import calib_save as _calib_save
 
-@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 = FFT_tools.ceil_pow_of_two(time*freq)
-    time = nsamps / freq
-    # take some data, keeping the position voltage at it's current value
-    out = numpy.ones((nsamps,), dtype=numpy.uint16) * zpiezo.curPos()
-    if config.TEXT_VERBOSE :
-        print "get %g seconds of data" % time
-    data = zpiezo.ramp(out, freq)
-    data['sample frequency Hz'] = numpy.array([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(**vib_aquire_kwargs)
-    vib_analyze.vib_save(data, **vib_save_kwargs)
-    freq = data['sample frequency Hz']
-    deflection_bits = data['Deflection input']
-    Vphoto_var = vib_analyze.vib_analyze(deflection_bits=deflection_bits,
-                                         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=-50, 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.  Negative Depth_nm values
-    will place the cantilever that many nm _off_ 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( \
-                    z_piezo_utils.getSurfPos(zpiezo,zpiezo.def_V2in(setpoint)))
-            except (z_piezo_utils.tooClose, z_piezo_utils.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( \
-            z_piezo_utils.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( \
-            z_piezo_utils.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', '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,
-                 log_dir=config.LOG_DIR, Vphoto_in2V=config.Vphoto_in2V,
-                 **kwargs):
+
+def move_far_from_surface(stepper, distance):
+    """Step back approximately `distance` meters.
     """
-    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
-     num_bumps     number of 'bumps' (see Outputs)
-     num_temps     number of 'Ts' (see Outputs)
-     num_vibs      number of 'vib's (see Outputs)
-     log_dir       directory to log data to.  Default 'None' disables logging.
-     Vphoto_in2V   function to convert photodiode input bits to Volts
-
-     + other kwargs.  Run calib_aquire._kwargs(calib_aquire) to see
-     all options.  Run calib_aquire._childSplittables to see a list
-     of kwarg functions that this function calls.
-
-    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
+    steps = int(distance/stepper.step_size)
+    _LOG.info('step back %d steps (~%g m)' % (steps, distance))
+    stepper.step_relative(-steps)
+
+def calib_acquire(afm, calibration_config, bump_config, temperature_config,
+                  vibration_config, filename=None, group='/'):
+    """Acquire data for calibrating a cantilever in one function.
+
+    Inputs:
+      afm                 a pyafm.AFM instance
+      calibration_config  a .config._CalibrationConfig instance
+      bump_config         a .config._BumpConfig instance
+      temperature_config            a .config._TConfig instance
+      vibration_config    a .config._VibrationConfig instance
+
+    Outputs (all are arrays of recorded data):
+      bumps measured (V_photodiode / nm_tip) proportionality constant
+      Ts    measured temperature (K)
+      vibs  measured V_photodiode variance (Volts**2) in free solution
+
+    The temperatures are collected after moving far from the surface
+    but before and vibrations are measured to give everything time to
+    settle after the big move.
     """
-    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
-    bumps = numpy.zeros((num_bumps,), dtype=numpy.float)
-    for i in range(num_bumps) :
-        move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
-        bumps[i] = bump(zpiezo=zpiezo, log_dir=log_dir,
-                        Vphoto_in2V=Vphoto_in2V, **bump_kwargs)
-    if config.TEXT_VERBOSE :
-        print bumps
-
-    move_far_from_surface(stepper, **move_far_from_surface_kwargs)
-
-    # get Ts
-    Ts = numpy.zeros((num_Ts,), dtype=numpy.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
+    assert group.endswith('/'), group
+
+    bumps = _zeros((calibration_config['num-bumps'],), dtype=_float)
+    for i in range(calibration_config['num-bumps']):
+        _LOG.info('acquire bump %d of %d' % (i, calibration_config['num-bumps']))
+        bumps[i] = _bump(afm=afm, bump_config=bump_config,
+                         filename=filename, group='%sbump/%d/' % (group, i))
+    _LOG.debug('bumps: %s' % bumps)
+
+    move_far_from_surface(
+        afm.stepper, distance=calibration_config['vibration-spacing'])
+
+    Ts = _zeros((calibration_config['num-temperatures'],), dtype=_float)
+    for i in range(calibration_config['num-temperatures']):
+        _LOG.info('acquire T %d of %d'
+                 % (i, calibration_config['num-temperatures']))
+        Ts[i] = _T(
+            get_T=afm.get_temperature, temperature_config=temperature_config,
+            filename=filename, group='%stemperature/%d/' % (group, i))
+        _sleep(calibration_config['temperature-sleep'])
+    _LOG.debug('temperatures: %s' % Ts)
 
     # get vibs
-    vibs = numpy.zeros((num_vibs,), dtype=numpy.float)
-    for i in range(num_vibs) :
-        vibs[i] = vib(zpiezo=zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V,
-                      **vib_kwargs)
-    print vibs
-    
-    analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs)
-    
+    vibs = _zeros((calibration_config['num-vibrations'],), dtype=_float)
+    for i in range(calibration_config['num-vibrations']):
+        vibs[i] = _vib(
+            piezo=afm.piezo, vibration_config=vibration_config,
+            filename=filename, group='%svibration/%d/' % (group, i))
+    _LOG.debug('vibrations: %s' % vibs)
+
     return (bumps, Ts, vibs)
 
+def calib(afm, calibration_config, bump_config, temperature_config,
+          vibration_config, filename=None, group='/'):
+    """Calibrate a cantilever in one function.
 
-@splittableKwargsFunction( \
-    (calib_aquire, 'log_dir'),
-    (analyze.calib_analyze, 'bumps','Ts','vibs'))
-def calib(log_dir=config.LOG_DIR, **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
+      (see `calib_acquire()`)
+
+    Outputs:
+      k    cantilever spring constant (in N/m, or equivalently nN/nm)
+      k_s  standard deviation in our estimate of k
+
+    >>> import os
+    >>> from pprint import pprint
+    >>> import tempfile
+    >>> from pycomedi.device import Device
+    >>> from pycomedi.subdevice import StreamingSubdevice
+    >>> from pycomedi.channel import AnalogChannel, DigitalChannel
+    >>> from pycomedi.constant import AREF, IO_DIRECTION, SUBDEVICE_TYPE, UNIT
+    >>> from pypiezo.afm import AFMPiezo
+    >>> from pypiezo.base import PiezoAxis, InputChannel
+    >>> from pypiezo.config import (HDF5_ChannelConfig, HDF5_AxisConfig,
+    ...     pprint_HDF5)
+    >>> from stepper import Stepper
+    >>> from pyafm import AFM
+    >>> from .config import (HDF5_CalibrationConfig, HDF5_BumpConfig,
+    ...     HDF5_TemperatureConfig, HDF5_VibrationConfig)
+    >>> from .analyze import calib_load_all
+
+    >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
+    >>> os.close(fd)
+
+    >>> d = Device('/dev/comedi0')
+    >>> d.open()
+
+    Setup an `AFMPiezo` instance.
+
+    >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
+    ...     factory=StreamingSubdevice)
+    >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
+    ...     factory=StreamingSubdevice)
+
+    >>> axis_channel = s_out.channel(
+    ...     0, factory=AnalogChannel, aref=AREF.ground)
+    >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
+    >>> for chan in [axis_channel, input_channel]:
+    ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
+
+    We set the minimum voltage for the `z` axis to -9 (a volt above
+    the minimum possible voltage) to help with testing
+    `.get_surface_position`.  Without this minimum voltage, small
+    calibration errors could lead to a railed -10 V input for the
+    first few surface approaching steps, which could lead to an
+    `EdgeKink` error instead of a `FlatFit` error.
+
+    >>> axis_config = HDF5_AxisConfig(filename, '/bump/config/z/axis')
+    >>> axis_config.update(
+    ...     {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
+    >>> axis_channel_config = HDF5_ChannelConfig(
+    ...     filename, '/bump/config/z/channel')
+    >>> input_channel_config = HDF5_ChannelConfig(
+    ...     filename, '/bump/config/deflection/channel')
+
+    >>> a = PiezoAxis(axis_config=axis_config,
+    ...     axis_channel_config=axis_channel_config,
+    ...     axis_channel=axis_channel, name='z')
+    >>> a.setup_config()
+
+    >>> c = InputChannel(
+    ...     channel_config=input_channel_config, channel=input_channel,
+    ...     name='deflection')
+    >>> c.setup_config()
+
+    >>> piezo = AFMPiezo(axes=[a], input_channels=[c])
+
+    Setup a `stepper` instance.
+
+    >>> s_d = d.find_subdevice_by_type(SUBDEVICE_TYPE.dio)
+    >>> d_channels = [s_d.channel(i, factory=DigitalChannel)
+    ...             for i in (0, 1, 2, 3)]
+    >>> for chan in d_channels:
+    ...     chan.dio_config(IO_DIRECTION.output)
+
+    >>> def write(value):
+    ...     s_d.dio_bitfield(bits=value, write_mask=2**4-1)
+
+    >>> stepper = Stepper(write=write)
+
+    Setup an `AFM` instance.
+
+    >>> afm = AFM(piezo, stepper)
+
+    Test calibration:
+
+    >>> calibration_config = HDF5_CalibrationConfig(
+    ...     filename=filename, group='/bump/config/calibration/')
+    >>> bump_config = HDF5_BumpConfig(
+    ...     filename=filename, group='/bump/config/bump/')
+    >>> temperature_config = HDF5_TemperatureConfig(
+    ...     filename=filename, group='/bump/config/temperature/')
+    >>> vibration_config = HDF5_VibrationConfig(
+    ...     filename=filename, group='/bump/config/vibration')
+    >>> calib(afm, calibration_config, bump_config, temperature_config,
+    ...     vibration_config, filename=filename, group='/')
+    TODO: replace skipped example data with real-world values
+    >>> pprint_HDF5(filename)  # doctest: +ELLIPSIS, +REPORT_UDIFF
+    >>> everything = calib_load_all(filename, '/')
+    >>> pprint(everything)
+
+    Close the Comedi device.
+
+    >>> d.close()
+
+    Cleanup our temporary config file.
+
+    os.remove(filename)
     """
-    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, **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)
+    bumps, Ts, vibs = calib_acquire(
+        afm, calibration_config, bump_config, temperature_config,
+        vibration_config, filename=filename, group=group)
+    # TODO: convert vib units?
+    k,k_s = _calib_analyze(bumps, Ts, vibs)
+    _calib_save(filename, group=group+'calibration/', bumps=bumps, Ts=Ts,
+                vibs=vibs, calibration_config=calibration_config, k=k, k_s=k_s)
     return (k, k_s)
-
-    
-