1 # calibcant - tools for thermally calibrating AFM cantilevers
3 # Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
5 # This file is part of calibcant.
7 # calibcant is free software: you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation, either
10 # version 3 of the License, or (at your option) any later version.
12 # calibcant is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Lesser General Public License for more details.
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with calibcant. If not, see
19 # <http://www.gnu.org/licenses/>.
21 """Acquire and analyze cantilever calibration data.
23 The relevent physical quantities are:
25 * Vzp_out Output z-piezo voltage (what we generate)
26 * Vzp Applied z-piezo voltage (after external ZPGAIN)
27 * Zp The z-piezo position
28 * Zcant The cantilever vertical deflection
29 * Vphoto The photodiode vertical deflection voltage (what we measure)
30 * Fcant The force on the cantilever
31 * T The temperature of the cantilever and surrounding solution
32 * (another thing we measure or guess)
33 * k_b Boltzmann's constant
35 Which are related by the parameters:
37 * zpGain Vzp_out / Vzp
38 * zpSensitivity Zp / Vzp
39 * photoSensitivity Vphoto / Zcant
40 * k_cant Fcant / Zcant
42 Cantilever calibration assumes a pre-calibrated z-piezo
43 (zpSensitivity) and a amplifier (zpGain). In our lab, the z-piezo is
44 calibrated by imaging a calibration sample, which has features with
45 well defined sizes, and the gain is set with a knob on the Nanoscope.
47 photoSensitivity is measured by bumping the cantilever against the
48 surface, where Zp = Zcant (see bump_acquire() and the bump_analyze
51 k_cant is measured by watching the cantilever vibrate in free solution
52 (see the vib_acquire() and the vib_analyze submodule). The average
53 energy of the cantilever in the vertical direction is given by the
54 equipartition theorem.
56 .. math:: \frac{1}{2} k_b T = \frac{1}{2} k_cant <Zcant**2>
60 .. math:: k_cant = \frac{k_b T}{Zcant**2}
64 .. math:: Zcant = \frac{Vphoto}{photoSensitivity}
68 .. math:: k_cant = \frac{k_b T * photoSensitivty^2}{<Vphoto**2>}
70 We measured photoSensitivity with the surface bumps. We can either
71 measure T using an external function (see temperature.py), or just
72 estimate it (see T_acquire() and the T_analyze submodule). Guessing
73 room temp ~22 deg C is actually fairly reasonable. Assuming the
74 actual fluid temperature is within +/- 5 deg, the error in the spring
75 constant k_cant is within 5/273.15 ~= 2%. A time series of Vphoto
76 while we're far from the surface and not changing Vzp_out will give us
77 the average variance <Vphoto**2>.
79 We do all these measurements a few times to estimate statistical
82 The functions are layed out in the families::
84 bump_*(), vib_*(), T_*(), and calib_*()
86 For each family, * can be any of:
88 * acquire get real-world data
89 * save store real-world data to disk
90 * load get real-world data from disk
91 * analyze interperate the real-world data.
92 * plot show a nice graphic to convince people we're working :p
94 A family name without any `_*` extension (e.g. `bump()`), runs
95 `*_acquire()`, `*_analyze()`, and `*_save()`. `*_analyze()` will run
96 `*_plot()` if `matplotlib` is set in `calibcant.package_config`.
99 from numpy import zeros as _zeros
100 from numpy import float as _float
101 from time import sleep as _sleep
103 from . import LOG as _LOG
105 from .bump import bump as _bump
106 from .T import T as _T
107 from .vib import vib as _vib
108 from .analyze import calib_analyze as _calib_analyze
109 from .analyze import calib_save as _calib_save
112 def move_far_from_surface(stepper, distance):
113 """Step back approximately `distance` meters.
115 steps = int(distance/stepper.step_size)
116 _LOG.info('step back %d steps (~%g m)' % (steps, distance))
117 stepper.step_relative(-steps)
119 def calib_acquire(afm, calibration_config, filename=None, group='/'):
120 """Acquire data for calibrating a cantilever in one function.
123 afm a pyafm.AFM instance
124 calibration_config a .config._CalibrationConfig instance
126 Outputs (all are arrays of recorded data):
127 bumps measured (V_photodiode / nm_tip) proportionality constant
128 Ts measured temperature (K)
129 vibs measured V_photodiode variance (Volts**2) in free solution
131 The temperatures are collected after moving far from the surface
132 but before and vibrations are measured to give everything time to
133 settle after the big move.
135 assert group.endswith('/'), group
137 bumps = _zeros((calibration_config['num-bumps'],), dtype=_float)
138 for i in range(calibration_config['num-bumps']):
139 _LOG.info('acquire bump %d of %d' % (i, calibration_config['num-bumps']))
140 bumps[i] = _bump(afm=afm, bump_config=calibration_config['bump'],
141 filename=filename, group='%sbump/%d/' % (group, i))
142 _LOG.debug('bumps: %s' % bumps)
144 move_far_from_surface(
145 afm.stepper, distance=calibration_config['vibration-spacing'])
147 Ts = _zeros((calibration_config['num-temperatures'],), dtype=_float)
148 for i in range(calibration_config['num-temperatures']):
149 _LOG.info('acquire T %d of %d'
150 % (i, calibration_config['num-temperatures']))
152 get_T=afm.get_temperature,
153 temperature_config=calibration_config['temperature'],
154 filename=filename, group='%stemperature/%d/' % (group, i))
155 _sleep(calibration_config['temperature-sleep'])
156 _LOG.debug('temperatures: %s' % Ts)
159 vibs = _zeros((calibration_config['num-vibrations'],), dtype=_float)
160 for i in range(calibration_config['num-vibrations']):
162 piezo=afm.piezo, vibration_config=calibration_config['vibration'],
163 filename=filename, group='%svibration/%d/' % (group, i))
164 _LOG.debug('vibrations: %s' % vibs)
166 return (bumps, Ts, vibs)
168 def calib(afm, calibration_config, filename=None, group='/'):
169 """Calibrate a cantilever in one function.
172 (see `calib_acquire()`)
175 k cantilever spring constant (in N/m, or equivalently nN/nm)
176 k_s standard deviation in our estimate of k
179 >>> from pprint import pprint
181 >>> from h5config.storage.hdf5 import pprint_HDF5
182 >>> from pycomedi.device import Device
183 >>> from pycomedi.subdevice import StreamingSubdevice
184 >>> from pycomedi.channel import AnalogChannel, DigitalChannel
185 >>> from pycomedi.constant import AREF, IO_DIRECTION, SUBDEVICE_TYPE, UNIT
186 >>> from pypiezo.afm import AFMPiezo
187 >>> from pypiezo.base import PiezoAxis, InputChannel
188 >>> from pypiezo.config import ChannelConfig, AxisConfig
189 >>> from stepper import Stepper
190 >>> from pyafm import AFM
191 >>> from .config import (CalibrationConfig, BumpConfig,
192 ... TemperatureConfig, VibrationConfig)
193 >>> from .analyze import calib_load_all
195 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
198 >>> d = Device('/dev/comedi0')
201 Setup an `AFMPiezo` instance.
203 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
204 ... factory=StreamingSubdevice)
205 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
206 ... factory=StreamingSubdevice)
208 >>> axis_channel = s_out.channel(
209 ... 0, factory=AnalogChannel, aref=AREF.ground)
210 >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
211 >>> for chan in [axis_channel, input_channel]:
212 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
214 We set the minimum voltage for the `z` axis to -9 (a volt above
215 the minimum possible voltage) to help with testing
216 `.get_surface_position`. Without this minimum voltage, small
217 calibration errors could lead to a railed -10 V input for the
218 first few surface approaching steps, which could lead to an
219 `EdgeKink` error instead of a `FlatFit` error.
221 >>> axis_config = AxisConfig()
222 >>> axis_config.update(
223 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
224 >>> axis_channel_config = ChannelConfig()
225 >>> input_channel_config = ChannelConfig()
227 >>> a = PiezoAxis(axis_config=axis_config,
228 ... axis_channel_config=axis_channel_config,
229 ... axis_channel=axis_channel, name='z')
232 >>> c = InputChannel(
233 ... channel_config=input_channel_config, channel=input_channel,
234 ... name='deflection')
237 >>> piezo = AFMPiezo(axes=[a], input_channels=[c])
239 Setup a `stepper` instance.
241 >>> s_d = d.find_subdevice_by_type(SUBDEVICE_TYPE.dio)
242 >>> d_channels = [s_d.channel(i, factory=DigitalChannel)
243 ... for i in (0, 1, 2, 3)]
244 >>> for chan in d_channels:
245 ... chan.dio_config(IO_DIRECTION.output)
247 >>> def write(value):
248 ... s_d.dio_bitfield(bits=value, write_mask=2**4-1)
250 >>> stepper = Stepper(write=write)
252 Setup an `AFM` instance.
254 >>> afm = AFM(piezo, stepper)
258 >>> calibration_config = CalibrationConfig()
259 >>> bump_config = BumpConfig()
260 >>> temperature_config = TemperatureConfig()
261 >>> vibration_config = VibrationConfig()
262 >>> calib(afm, calibration_config, bump_config, temperature_config,
263 ... vibration_config, filename=filename, group='/')
264 TODO: replace skipped example data with real-world values
265 >>> pprint_HDF5(filename) # doctest: +ELLIPSIS, +REPORT_UDIFF
266 >>> everything = calib_load_all(filename, '/')
267 >>> pprint(everything)
269 Close the Comedi device.
273 Cleanup our temporary config file.
277 bumps, Ts, vibs = calib_acquire(
278 afm, calibration_config, bump_config, temperature_config,
279 vibration_config, filename=filename, group=group)
280 # TODO: convert vib units?
281 k,k_s = _calib_analyze(bumps, Ts, vibs)
282 _calib_save(filename, group=group+'calibration/', bumps=bumps,
283 temperatures=Ts, vibrations=vibs,
284 calibration_config=calibration_config, k=k, k_s=k_s)