Massive rewrite (v 0.6) to base everything on Cython and revamped pypiezo.
[pyafm.git] / calibcant / calibrate.py
old mode 100755 (executable)
new mode 100644 (file)
index 10622f4..e8b7293
@@ -1,52 +1,43 @@
-#!/usr/bin/python
-#
 # calibcant - tools for thermally calibrating AFM cantilevers
 #
 # calibcant - tools for thermally calibrating AFM cantilevers
 #
-# Copyright (C) 2007,2008, William Trevor King
+# Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
 #
 #
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This file is part of calibcant.
 #
 #
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the GNU General Public License for more details.
+# calibcant is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation, either
+# version 3 of the License, or (at your option) any later version.
 #
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
-# 02111-1307, USA.
+# calibcant is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
 #
 #
-# The author may be contacted at <wking@drexel.edu> on the Internet, or
-# write to Trevor King, Drexel University, Physics Dept., 3141 Chestnut St.,
-# Philadelphia PA 19104, USA.
-
-"""
-Aquire and analyze cantilever calibration data.
+# You should have received a copy of the GNU Lesser General Public
+# License along with calibcant.  If not, see
+# <http://www.gnu.org/licenses/>.
 
 
-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
 
 Cantilever calibration assumes a pre-calibrated z-piezo
 (zpSensitivity) and a amplifier (zpGain).  In our lab, the z-piezo is
@@ -54,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
 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
 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.
 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
 
 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
 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
@@ -78,343 +79,216 @@ the average variance <Vphoto**2>.
 We do all these measurements a few times to estimate statistical
 errors.
 
 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 
-import z_piezo_utils
-from splittable_kwargs import splittableKwargsFunction, \
-    make_splittable_kwargs_function
-import FFT_tools
+  bump_*(), vib_*(), T_*(), and calib_*()
 
 
-import common
-import config
-import bump_analyze
-import T_analyze
-import vib_analyze
-import analyze
+For each family, * can be any of:
 
 
-# bump family
+* 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
 
 
-@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 = 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" % 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) # returns array
-    return T_ret[0]
+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`.
+"""
 
 
-# vib family
+from numpy import zeros as _zeros
+from numpy import float as _float
+from time import sleep as _sleep
 
 
-@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=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.
+from . import LOG as _LOG
 
 
-    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', '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_DIR, Vphoto_in2V=config.Vphoto_in2V,
-                 **kwargs):
+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
+
+
+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
-     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
+    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
-    move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
-    bumps = numpy.zeros((num_bumps,), dtype=numpy.float)
-    for i in range(num_bumps) :
-        bumps[i] = bump(zpiezo=zpiezo, freq=bump_freq, 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
 
     # 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)
 
     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:
     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)
     return (k, k_s)
-
-    
-