+++ /dev/null
-# calibcant - tools for thermally calibrating AFM cantilevers
-#
-# Copyright (C) 2008-2012 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of calibcant.
-#
-# calibcant 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.
-#
-# 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# calibcant. If not, see <http://www.gnu.org/licenses/>.
-
-"""Acquire and analyze cantilever calibration data.
-
-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_acquire() and the bump_analyze
-submodule).
-
-k_cant is measured by watching the cantilever vibrate in free solution
-(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.
-
-.. 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_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
-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_*()
-
-For each family, * can be any of:
-
-* 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.package_config`.
-"""
-
-from numpy import zeros as _zeros
-from numpy import float as _float
-from time import sleep as _sleep
-
-from . import LOG as _LOG
-
-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.
- """
- 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, filename=None, group='/'):
- """Acquire data for calibrating a cantilever in one function.
-
- Inputs:
- afm a pyafm.AFM instance
- calibration_config a .config._CalibrationConfig 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.
- """
- 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=calibration_config['bump'],
- 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=calibration_config['temperature'],
- filename=filename, group='%stemperature/%d/' % (group, i))
- _sleep(calibration_config['temperature-sleep'])
- _LOG.debug('temperatures: %s' % Ts)
-
- # get vibs
- vibs = _zeros((calibration_config['num-vibrations'],), dtype=_float)
- for i in range(calibration_config['num-vibrations']):
- vibs[i] = _vib(
- piezo=afm.piezo, vibration_config=calibration_config['vibration'],
- filename=filename, group='%svibration/%d/' % (group, i))
- _LOG.debug('vibrations: %s' % vibs)
-
- return (bumps, Ts, vibs)
-
-def calib(afm, calibration_config, filename=None, group='/'):
- """Calibrate a cantilever in one function.
-
- Inputs:
- (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 h5config.storage.hdf5 import pprint_HDF5
- >>> 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 ChannelConfig, AxisConfig
- >>> from stepper import Stepper
- >>> from pyafm.afm import AFM
- >>> from .config import (CalibrationConfig, BumpConfig,
- ... TemperatureConfig, 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 = AxisConfig()
- >>> axis_config.update(
- ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
- >>> axis_channel_config = ChannelConfig()
- >>> axis_channel_config['name'] = 'z'
- >>> axis_config['channel'] = axis_channel_config
- >>> input_channel_config = ChannelConfig()
- >>> input_channel_config['name'] = 'deflection'
-
- >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel)
- >>> a.setup_config()
-
- >>> c = InputChannel(config=input_channel_config, channel=input_channel)
- >>> c.setup_config()
-
- >>> piezo = AFMPiezo(axes=[a], inputs=[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 = CalibrationConfig()
- >>> calibration_config['bump'] = BumpConfig()
- >>> calibration_config['temperature'] = TemperatureConfig()
- >>> calibration_config['vibration'] = VibrationConfig()
- >>> calib(afm, calibration_config, filename=filename, group='/')
- TODO: replace skipped example data with real-world values
- >>> pprint_HDF5(filename) # doctest: +ELLIPSIS, +REPORT_UDIFF
- /
- /bump
- /bump/0
- /bump/0/config
- /bump/0/config/bump
- <HDF5 dataset "far-steps": shape (), type "<i4">
- 200
- ...
- /bump/0/config/deflection
- /bump/0/config/deflection/channel
- <HDF5 dataset "channel": shape (), type "<i4">
- 0
- ...
- /bump/0/config/z
- /bump/0/config/z/axis
- /bump/0/config/z/axis/channel
- <HDF5 dataset "channel": shape (), type "<i4">
- 0
- ...
- <HDF5 dataset "gain": shape (), type "<i4">
- 20
- ...
- <HDF5 dataset "processed": shape (), type "<f8">
- ...
- /bump/0/raw
- <HDF5 dataset "deflection": shape (2048,), type "<u2">
- [...]
- <HDF5 dataset "z": shape (2048,), type "<u2">
- [...]
- /bump/1
- ...
- /calibration
- /calibration/config
- /calibration/config/bump
- <HDF5 dataset "far-steps": shape (), type "<i4">
- 200
- ...
- <HDF5 dataset "num-bumps": shape (), type "<i4">
- 10
- ...
- /calibration/processed
- /calibration/processed/spring-constant
- <HDF5 dataset "data": shape (), type "<f8">
- ...
- <HDF5 dataset "standard-deviation": shape (), type "<f8">
- ...
- <HDF5 dataset "units": shape (), type "|S3">
- N/m
- /calibration/raw
- /calibration/raw/photodiode-sensitivity
- <HDF5 dataset "data": shape (10,), type "<f8">
- [...]
- <HDF5 dataset "units": shape (), type "|S3">
- V/m
- /calibration/raw/temperature
- <HDF5 dataset "data": shape (10,), type "<f8">
- [...]
- <HDF5 dataset "units": shape (), type "|S1">
- K
- /calibration/raw/thermal-vibration-variance
- <HDF5 dataset "data": shape (20,), type "<f8">
- [...]
- <HDF5 dataset "units": shape (), type "|S3">
- V^2
- /temperature
- /temperature/0
- /temperature/0/config
- <HDF5 dataset "default": shape (), type "|b1">
- False
- <HDF5 dataset "units": shape (), type "|S7">
- Celsius
- <HDF5 dataset "processed": shape (), type "<f8">
- 295.15
- <HDF5 dataset "raw": shape (), type "<i4">
- 22
- /temperature/1
- ...
- /vibration
- /vibration/0
- /vibration/0/config
- /vibration/0/config/deflection
- <HDF5 dataset "channel": shape (), type "<i4">
- 0
- ...
- /vibration/0/config/vibration
- <HDF5 dataset "chunk-size": shape (), type "<i4">
- 2048
- ...
- <HDF5 dataset "processed": shape (), type "<f8">
- ...
- /vibration/0/raw
- <HDF5 dataset "deflection": shape (65536,), type "<u2">
- [...]
- /vibration/1
- ...
- /vibration/19
- ...
- /vibration/19/raw
- <HDF5 dataset "deflection": shape (65536,), type "<u2">
- [...]
- >>> everything = calib_load_all(filename, '/')
- >>> pprint(everything) # doctest: +ELLIPSIS, +REPORT_UDIFF
- {'bump_details': [{'bump_config': <BumpConfig ...>,
- 'deflection_channel_config': <ChannelConfig ...>,
- 'processed_bump': ...,
- 'raw_bump': {'deflection': array([...], dtype=uint16),
- 'z': array([...], dtype=uint16)},
- 'z_axis_config': <AxisConfig ...>},
- ...],
- 'bumps': array([...]),
- 'calibration_config': <CalibrationConfig ...>,
- 'k': ...,
- 'k_s': ...,
- 'temperature_details': [{'processed_temperature': ...,
- 'raw_temperature': array(22),
- 'temperature_config': <TemperatureConfig ...>},
- ...],
- 'temperatures': array([...]),
- 'vibration_details': [{'deflection_channel_config': <ChannelConfig ...>,
- 'processed_vibration': ...,
- 'raw_vibration': array([...], dtype=uint16),
- 'vibration_config': <VibrationConfig ...>},
- ...],
- 'vibrations': array([...])}
-
- Close the Comedi device.
-
- >>> d.close()
-
- Cleanup our temporary config file.
-
- os.remove(filename)
- """
- bumps, Ts, vibs = calib_acquire(
- afm, calibration_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,
- temperatures=Ts, vibrations=vibs,
- calibration_config=calibration_config, k=k, k_s=k_s)
- return (k, k_s)