1 # calibcant - tools for thermally calibrating AFM cantilevers
3 # Copyright (C) 2008-2012 W. Trevor King <wking@drexel.edu>
5 # This file is part of calibcant.
7 # calibcant is free software: you can redistribute it and/or modify it under
8 # the terms of the GNU General Public License as published by the Free Software
9 # Foundation, either version 3 of the License, or (at your option) any later
12 # calibcant is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along with
17 # calibcant. If not, see <http://www.gnu.org/licenses/>.
19 """Calculate `k` from arrays of bumps, temperatures, and vibrations.
21 Separate the more general `analyze()` from the other calibration
22 functions in calibcant.
24 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)
33 k_b Boltzmann's constant
35 Which are related by the parameters:
37 zp_sensitivity Zp / Vzp
38 photo_sensitivity Vphoto / Zcant
43 >>> from .config import CalibrateConfig
45 >>> config = CalibrateConfig()
46 >>> bumps = numpy.array((15.9e6, 16.9e6, 16.3e6))
47 >>> temperatures = numpy.array((295, 295.2, 294.8))
48 >>> vibrations = numpy.array((2.20e-5, 2.22e-5, 2.21e-5))
50 >>> k,k_s = analyze(bumps=bumps, temperatures=temperatures,
51 ... vibrations=vibrations)
52 >>> (k, k_s) # doctest: +ELLIPSIS
53 (0.0493..., 0.00248...)
55 Most of the error in this example comes from uncertainty in the
56 photodiode sensitivity (bumps).
58 >>> k_s/k # doctest: +ELLIPSIS
60 >>> bumps.std()/bumps.mean() # doctest: +ELLIPSIS
62 >>> temperatures.std()/temperatures.mean() # doctest: +ELLIPSIS
64 >>> vibrations.std()/vibrations.mean() # doctest: +ELLIPSIS
69 import numpy as _numpy
71 from scipy.constants import Boltzmann as _kB # in J/K
73 from scipy.constants import Bolzmann as _kB # in J/K
74 # Bolzmann -> Boltzmann patch submitted:
75 # http://projects.scipy.org/scipy/ticket/1417
76 # Fixed in scipy commit 4716d91, Apr 2, 2011, during work after v0.9.0rc5.
79 import matplotlib as _matplotlib
80 import matplotlib.pyplot as _matplotlib_pyplot
81 import time as _time # for timestamping lines on plots
82 except (ImportError, RuntimeError), e:
84 _matplotlib_import_error = e
86 from h5config.storage.hdf5 import h5_create_group as _h5_create_group
87 from pypiezo.base import get_axis_name as _get_axis_name
89 from . import LOG as _LOG
90 from . import package_config as _package_config
92 from .bump_analyze import analyze as _bump_analyze
93 from .bump_analyze import save as _bump_save
94 from .temperature_analyze import analyze as _temperature_analyze
95 from .temperature_analyze import save as _temperature_save
96 from .vibration_analyze import analyze as _vibration_analyze
97 from .vibration_analyze import save as _vibration_save
98 from .util import SaveSpec as _SaveSpec
99 from .util import save as _save
102 def analyze(bumps, temperatures, vibrations):
103 """Analyze data from `get_calibration_data()`
105 Inputs (all are arrays of recorded data):
106 bumps measured (V_photodiode / nm_tip) proportionality constant
107 temperatures measured temperature (K)
108 vibrations measured V_photodiode variance in free solution (V**2)
110 k cantilever spring constant (in N/m, or equivalently nN/nm)
111 k_s standard deviation in our estimate of k
115 We're assuming vib is mostly from thermal cantilever vibrations
116 (and then only from vibrations in the single vertical degree of
117 freedom), and not from other noise sources.
119 If the error is large, check the relative errors
120 (`x.std()/x.mean()`)of your input arrays. If one of them is
121 small, don't bother repeating that measurment too often. If one
122 is large, try repeating that measurement more. Remember that you
123 need enough samples to have a valid error estimate in the first
124 place, and that none of this addresses any systematic errors.
126 ps_m = bumps.mean() # ps for photo-sensitivity
128 T_m = temperatures.mean()
129 T_s = temperatures.std()
130 v2_m = vibrations.mean() # average voltage variance
131 v2_s = vibrations.std()
133 # Vphoto / photo_sensitivity = x
134 # k = kB T / <x**2> = kB T photo_sensitivity**2 / Vphoto_var
136 # units, photo_sensitivity = Vphoto/(Zcant in m),
137 # so Vphoto/photo_sensitivity = Zcant in m
138 # so k = J/K * K / m^2 = J / m^2 = N/m
139 k = _kB * T_m * ps_m**2 / v2_m
141 # propogation of errors
145 dk_ps = 2*k/ps_m * ps_s
147 dk_v = -k/v2_m * v2_s
149 k_s = _numpy.sqrt(dk_T**2 + dk_ps**2 + dk_v**2)
151 _LOG.info('variable (units) : '
152 'mean +/- std. dev. (relative error)')
153 _LOG.info('cantilever k (N/m) : %g +/- %g (%g)' % (k, k_s, k_s/k))
154 _LOG.info('photo sensitivity (V/m) : %g +/- %g (%g)'
155 % (ps_m, ps_s, ps_s/ps_m))
156 _LOG.info('T (K) : %g +/- %g (%g)'
157 % (T_m, T_s, T_s/T_m))
158 _LOG.info('vibration variance (V^2) : %g +/- %g (%g)'
159 % (v2_m, v2_s, v2_s/v2_m))
161 if _package_config['matplotlib']:
162 plot(bumps, temperatures, vibrations)
167 def plot(bumps, temperatures, vibrations):
169 raise _matplotlib_import_error
170 figure = _matplotlib_pyplot.figure()
172 bump_axes = figure.add_subplot(3, 1, 1)
173 T_axes = figure.add_subplot(3, 1, 2)
174 vib_axes = figure.add_subplot(3, 1, 3)
176 timestamp = _time.strftime('%H%M%S')
177 bump_axes.set_title('cantilever calibration %s' % timestamp)
179 bump_axes.autoscale(tight=True)
180 bump_axes.plot(bumps, 'g.-')
181 bump_axes.set_ylabel('photodiode sensitivity (V/m)')
182 T_axes.autoscale(tight=True)
183 T_axes.plot(temperatures, 'r.-')
184 T_axes.set_ylabel('temperature (K)')
185 vib_axes.autoscale(tight=True)
186 vib_axes.plot(vibrations, 'b.-')
187 vib_axes.set_ylabel('thermal deflection variance (V^2)')
189 if hasattr(figure, 'show'):
192 _plot = plot # alternative name for use inside analyze_all()
194 def save_results(filename=None, group='/', bump=None,
195 temperature=None, vibration=None, spring_constant=None,
196 spring_constant_deviation=None):
198 _SaveSpec(item=bump, relpath='raw/photodiode-sensitivity',
199 array=True, units='V/m'),
200 _SaveSpec(item=temperature, relpath='raw/temperature',
201 array=True, units='K'),
202 _SaveSpec(item=vibration, relpath='raw/vibration',
203 array=True, units='V^2/Hz'),
204 _SaveSpec(item=spring_constant, relpath='processed/spring-constant',
205 units='N/m', deviation=spring_constant_deviation),
207 _save(filename=filename, group=group, specs=specs)
209 def analyze_all(config, data, raw_data, maximum_relative_error=1e-5,
210 filename=None, group=None, plot=False, dry_run=False):
211 "(Re)analyze (and possibly plot) all data from a `calib()` run."
212 if not data.get('bump', None):
213 data['bump'] = _numpy.zeros((config['num-bumps'],), dtype=float)
214 if not data.get('temperature', None):
215 data['temperature'] = _numpy.zeros(
216 (config['num-temperatures'],), dtype=float)
217 if not data.get('vibrations', None):
218 data['vibration'] = _numpy.zeros(
219 (config['num-vibrations'],), dtype=float)
220 axis_config = config['afm']['piezo'].select_config(
222 attribute_value=config['afm']['main-axis'],
223 get_attribute=_get_axis_name)
224 input_config = config['afm']['piezo'].select_config(
225 setting_name='inputs', attribute_value='deflection')
226 bumps_changed = temperatures_changed = vibrations_changed = False
227 if not isinstance(group, _h5py.Group) and not dry_run:
228 f = _h5py.File(filename, mode='a')
229 group = _h5_create_group(f, group)
233 if len(data.get('raw/bump', [])) != len(data['bump']):
235 for i,bump in enumerate(raw_data['bump']):
236 data['bump'][i],changed = check_bump(
237 index=i, bump=bump, z_axis_config=axis_config,
238 deflection_channel_config=input_config, plot=plot,
239 maximum_relative_error=maximum_relative_error)
240 if changed and not dry_run:
242 bump_group = _h5_create_group(group, 'bump/{}'.format(i))
243 _bump_save(group=bump_group, processed=data['bump'][i])
244 if len(data.get('raw/temperature', [])) != len(data['temperature']):
245 temperatures_changed = True
246 for i,temperature in enumerate(raw_data['temperature']):
247 data['temperature'][i],changed = check_temperature(
248 index=i, temperature=temperature,
249 maximum_relative_error=maximum_relative_error)
250 if changed and not dry_run:
251 temperatures_changed = True
252 temperature_group = _h5_create_group(
253 group, 'temperature/{}'.format(i))
255 group=temerature_group, processed=data['temperature'][i])
256 if len(data.get('raw/vibration', [])) != len(data['vibration']):
257 vibrations_changed = True
258 for i,vibration in enumerate(raw_data['vibration']):
259 data['vibration'][i],changed = check_vibration(
260 index=i, vibration=vibration,
261 deflection_channel_config=input_config, plot=plot,
262 maximum_relative_error=maximum_relative_error)
263 if changed and not dry_run:
264 vibrations_changed = True
265 vibration_group = _h5_create_group(
266 group, 'vibration/{}'.format(i))
268 group=vibration_group, processed=data['vibration'])
269 k,k_s,changed = check_calibration(
270 k=data.get('processed/spring_constant', None),
271 k_s=data.get('processed/spring_constant_deviation', None),
273 temperatures=data['temperature'], vibrations=data['vibration'],
274 maximum_relative_error=maximum_relative_error)
275 if (changed or bumps_changed or temperatures_changed or
276 vibrations_changed) and not dry_run:
277 calibration_group = _h5_create_group(group, 'calibration')
280 group=calibration_group, bump=data['bump'])
281 if temperatures_changed:
283 group=calibration_group, temperature=data['temperature'])
284 if vibrations_changed:
286 group=calibration_group, vibration=data['vibration'])
289 group=calibration_group,
290 spring_constant=k, spring_constant_deviation=k_s)
295 _plot(bumps=data['raw']['bump'],
296 temperatures=data['raw']['temperature'],
297 vibrations=data['raw']['vibration'])
300 def check_bump(index, bump, maximum_relative_error, **kwargs):
302 sensitivity = _bump_analyze(
303 config=bump['config']['bump'], data=bump['raw'], **kwargs)
304 if bump.get('processed', None) is None:
306 _LOG.warn('new analysis for bump {}: {}'.format(index, sensitivity))
308 rel_error = abs(sensitivity - bump['processed'])/bump['processed']
309 if rel_error > maximum_relative_error:
311 _LOG.warn(("new analysis doesn't match for bump {}: {} -> {} "
312 "(difference: {}, relative error: {})").format(
313 index, bump['processed'], sensitivity,
314 sensitivity-bump['processed'], rel_error))
315 return (sensitivity, changed)
317 def check_temperature(index, temperature, maximum_relative_error, **kwargs):
319 temp = _temperature_analyze(
320 config=temperature['config']['temperature'],
321 temperature=temperature['raw'], **kwargs)
322 if temperature.get('processed', None) is None:
324 _LOG.warn('new analysis for temperature {}: {}'.format(index, temp))
326 rel_error = abs(temp - temperature['processed']
327 )/temperature['processed']
328 if rel_error > maximum_relative_error:
330 _LOG.warn(("new analysis doesn't match for temperature "
331 "{} -> {} (difference: {}, relative error: {})"
333 index, temperature['processed'], temp,
334 temp-temperature['processed'], rel_error))
335 return (temp, changed)
337 def check_vibration(index, vibration, maximum_relative_error, **kwargs):
339 variance = _vibration_analyze(
340 config=vibration['config']['vibration'],
341 deflection=vibration['raw'], **kwargs)
342 if vibration.get('processed', None) is None:
344 _LOG.warn('new analysis for temperature {}: {}'.format(
347 rel_error = abs(variance-vibration['processed'])/vibration['processed']
348 if rel_error > maximum_relative_error:
349 _LOG.warn(("new analysis doesn't match for vibration {}: {} != {} "
350 "(difference: {}, relative error: {})").format(
351 index, variance, vibration['processed'],
352 variance-vibration['processed'], rel_error))
353 return (variance, changed)
355 def check_calibration(k, k_s, maximum_relative_error, **kwargs):
357 new_k,new_k_s = analyze(**kwargs)
360 _LOG.warn('new analysis for the spring constant: {}'.format(new_k))
362 rel_error = abs(new_k-k)/k
363 if rel_error > maximum_relative_error:
365 _LOG.warn(("new analysis doesn't match for the spring constant: "
366 "{} != {} (difference: {}, relative error: {})").format(
367 new_k, k, new_k-k, rel_error))
370 _LOG.warn('new analysis for the spring constant deviation: {}'.format(
373 rel_error = abs(new_k_s-k_s)/k_s
374 if rel_error > maximum_relative_error:
377 ("new analysis doesn't match for the spring constant deviation"
378 ": {} != {} (difference: {}, relative error: {})").format(
379 new_k_s, k_s, new_k_s-k_s, rel_error))
380 return (new_k, new_k_s, changed)