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 """Acquire and analyze cantilever calibration data.
21 The relevent physical quantities are:
23 * Vzp_out Output z-piezo voltage (what we generate)
24 * Vzp Applied z-piezo voltage (after external ZPGAIN)
25 * Zp The z-piezo position
26 * Zcant The cantilever vertical deflection
27 * Vphoto The photodiode vertical deflection voltage (what we measure)
28 * Fcant The force on the cantilever
29 * T The temperature of the cantilever and surrounding solution
30 (another thing we measure or guess)
31 * k_b Boltzmann's constant
33 Which are related by the parameters:
35 * zpGain Vzp_out / Vzp
36 * zpSensitivity Zp / Vzp
37 * photoSensitivity Vphoto / Zcant
38 * k_cant Fcant / Zcant
40 Cantilever calibration assumes a pre-calibrated z-piezo
41 (zpSensitivity) and a amplifier (zpGain). In our lab, the z-piezo is
42 calibrated by imaging a calibration sample, which has features with
43 well defined sizes, and the gain is set with a knob on the Nanoscope.
45 photoSensitivity is measured by bumping the cantilever against the
46 surface, where Zp = Zcant (see bump_acquire() and the bump_analyze
49 k_cant is measured by watching the cantilever vibrate in free solution
50 (see the vib_acquire() and the vib_analyze submodule). The average
51 energy of the cantilever in the vertical direction is given by the
52 equipartition theorem.
54 .. math:: \frac{1}{2} k_b T = \frac{1}{2} k_cant <Zcant**2>
58 .. math:: k_cant = \frac{k_b T}{Zcant**2}
62 .. math:: Zcant = \frac{Vphoto}{photoSensitivity}
66 .. math:: k_cant = \frac{k_b T * photoSensitivty^2}{<Vphoto**2>}
68 We measured photoSensitivity with the surface bumps. We can either
69 measure T using an external function (see temperature.py), or just
70 estimate it (see T_acquire() and the T_analyze submodule). Guessing
71 room temp ~22 deg C is actually fairly reasonable. Assuming the
72 actual fluid temperature is within +/- 5 deg, the error in the spring
73 constant k_cant is within 5/273.15 ~= 2%. A time series of Vphoto
74 while we're far from the surface and not changing Vzp_out will give us
75 the average variance <Vphoto**2>.
77 We do all these measurements a few times to estimate statistical
81 from time import sleep as _sleep
83 from numpy import zeros as _zeros
84 from numpy import float as _float
87 from pyafm.afm import AFM as _AFM
88 from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage
90 from . import LOG as _LOG
91 from .config import CalibrateConfig as _CalibrateConfig
92 from .bump import run as _bump
93 from .bump_analyze import load as _bump_load
94 from .temperature import run as _temperature
95 from .temperature_analyze import load as _temperature_load
96 from .vibration import run as _vibration
97 from .vibration_analyze import load as _vibration_load
98 from .analyze import analyze as _analyze
99 from .util import SaveSpec as _SaveSpec
100 from .util import save as _save
101 from .util import load as _load
104 def load(filename=None, group='/'):
105 config = _CalibrateConfig(storage=_HDF5_Storage(
106 filename=filename, group=group))
108 return Calibrator(config=config)
110 def load_all(filename=None, group='/', raw=True):
111 "Load all data from a `Calibration.calibrate()` run."
112 assert group.endswith('/'), group
114 filename=filename, group='{}config/'.format(group))
115 data = calibrator.load_results(
116 filename=filename, group='{}calibration/'.format(group))
118 raw_data = calibrator.load_raw(filename=filename, group=group)
121 return (calibrator, data, raw_data)
124 class Calibrator (object):
125 """Calibrate a cantilever spring constant using the thermal tune method.
128 >>> from pprint import pprint
130 >>> from h5config.storage.hdf5 import pprint_HDF5
131 >>> from pyafm.storage import load_afm
132 >>> from .config import (CalibrateConfig, BumpConfig,
133 ... TemperatureConfig, VibrationConfig)
135 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
141 >>> afm.load_from_config(devices=devices)
142 >>> if afm.piezo is None:
143 ... raise NotImplementedError('save a better default AFM!')
144 >>> config = CalibrateConfig()
145 >>> config['bump'] = BumpConfig()
146 >>> config['temperature'] = TemperatureConfig()
147 >>> config['vibration'] = VibrationConfig()
148 >>> c = Calibrator(config=config, afm=afm)
150 >>> k,k_s,data = c.calibrate(filename=filename)
151 >>> k # doctest: +SKIP
153 >>> k_s # doctest: +SKIP
154 0.0010609833397949553
155 >>> pprint(data) # doctest: +ELLIPSIS, +REPORT_UDIFF
156 {'bump': array([...]),
157 'temperature': array([...]),
158 'vibration': array([...])}
159 >>> pprint_HDF5(filename) # doctest: +ELLIPSIS, +REPORT_UDIFF
165 <HDF5 dataset "far-steps": shape (), type "<i4">
167 <HDF5 dataset "initial-position": shape (), type "<f8">
171 <HDF5 dataset "data": shape (), type "<f8">
173 <HDF5 dataset "units": shape (), type "|S3">
176 /bump/0/raw/deflection
177 <HDF5 dataset "data": shape (2048,), type "<u2">
179 <HDF5 dataset "units": shape (), type "|S4">
182 <HDF5 dataset "data": shape (2048,), type "<u2">
184 <HDF5 dataset "units": shape (), type "|S4">
190 <HDF5 dataset "fallback-temperature": shape (), type "<f8">
192 <HDF5 dataset "far": shape (), type "<f8">
194 <HDF5 dataset "main-axis": shape (), type "|S1">
196 <HDF5 dataset "name": shape (), type "|S5">
199 /config/afm/piezo/axes
200 /config/afm/piezo/axes/0
201 /config/afm/piezo/axes/0/channel
202 <HDF5 dataset "analog-reference": shape (), type "|S6">
204 <HDF5 dataset "channel": shape (), type "<i4">
206 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
207 [ -1.00000000e+01 3.05180438e-04]
208 <HDF5 dataset "conversion-origin": shape (), type "<f8">
210 <HDF5 dataset "device": shape (), type "|S12">
212 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
214 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
216 <HDF5 dataset "maxdata": shape (), type "<i8">
218 <HDF5 dataset "name": shape (), type "|S1">
220 <HDF5 dataset "range": shape (), type "<i4">
222 <HDF5 dataset "subdevice": shape (), type "<i4">
224 <HDF5 dataset "gain": shape (), type "<f8">
226 <HDF5 dataset "maximum": shape (), type "<f8">
228 <HDF5 dataset "minimum": shape (), type "<f8">
230 <HDF5 dataset "monitor": shape (), type "|S1">
232 <HDF5 dataset "sensitivity": shape (), type "<f8">
234 /config/afm/piezo/axes/1
235 /config/afm/piezo/axes/1/channel
236 <HDF5 dataset "analog-reference": shape (), type "|S6">
238 <HDF5 dataset "channel": shape (), type "<i4">
240 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
241 [ -1.00000000e+01 3.05180438e-04]
242 <HDF5 dataset "conversion-origin": shape (), type "<f8">
244 <HDF5 dataset "device": shape (), type "|S12">
246 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
248 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
250 <HDF5 dataset "maxdata": shape (), type "<i8">
252 <HDF5 dataset "name": shape (), type "|S1">
254 <HDF5 dataset "range": shape (), type "<i4">
256 <HDF5 dataset "subdevice": shape (), type "<i4">
258 <HDF5 dataset "gain": shape (), type "<f8">
260 <HDF5 dataset "maximum": shape (), type "<f8">
262 <HDF5 dataset "minimum": shape (), type "<f8">
264 <HDF5 dataset "monitor": shape (), type "|S1">
266 <HDF5 dataset "sensitivity": shape (), type "<f8">
268 /config/afm/piezo/inputs
269 /config/afm/piezo/inputs/0
270 <HDF5 dataset "analog-reference": shape (), type "|S4">
272 <HDF5 dataset "channel": shape (), type "<i4">
274 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
275 [ -1.00000000e+01 3.05180438e-04]
276 <HDF5 dataset "conversion-origin": shape (), type "<f8">
278 <HDF5 dataset "device": shape (), type "|S12">
280 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
282 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
284 <HDF5 dataset "maxdata": shape (), type "<i8">
286 <HDF5 dataset "name": shape (), type "|S10">
288 <HDF5 dataset "range": shape (), type "<i4">
290 <HDF5 dataset "subdevice": shape (), type "<i4">
292 <HDF5 dataset "name": shape (), type "|S5">
295 <HDF5 dataset "backlash": shape (), type "<i4">
297 <HDF5 dataset "delay": shape (), type "<f8">
299 <HDF5 dataset "full-step": shape (), type "|b1">
301 <HDF5 dataset "logic": shape (), type "|b1">
303 <HDF5 dataset "name": shape (), type "|S9">
305 /config/afm/stepper/port
306 <HDF5 dataset "channels": shape (4,), type "<i4">
308 <HDF5 dataset "device": shape (), type "|S12">
310 <HDF5 dataset "direction": shape (), type "|S6">
312 <HDF5 dataset "name": shape (), type "|S12">
314 <HDF5 dataset "subdevice": shape (), type "<i4">
316 <HDF5 dataset "subdevice-type": shape (), type "|S3">
318 <HDF5 dataset "step-size": shape (), type "<f8">
320 /config/afm/temperature
321 <HDF5 dataset "baudrate": shape (), type "<i4">
323 <HDF5 dataset "controller": shape (), type "<i4">
325 <HDF5 dataset "device": shape (), type "|S10">
327 <HDF5 dataset "max-current": shape (), type "<f8">
329 <HDF5 dataset "name": shape (), type "|S14">
331 <HDF5 dataset "units": shape (), type "|S7">
334 <HDF5 dataset "far-steps": shape (), type "<i4">
336 <HDF5 dataset "initial-position": shape (), type "<f8">
338 <HDF5 dataset "min-slope-ratio": shape (), type "<f8">
340 <HDF5 dataset "model": shape (), type "|S9">
342 <HDF5 dataset "push-depth": shape (), type "<f8">
344 <HDF5 dataset "push-speed": shape (), type "<f8">
346 <HDF5 dataset "samples": shape (), type "<i4">
348 <HDF5 dataset "setpoint": shape (), type "<f8">
350 <HDF5 dataset "num-bumps": shape (), type "<i4">
352 <HDF5 dataset "num-temperatures": shape (), type "<i4">
354 <HDF5 dataset "num-vibrations": shape (), type "<i4">
357 <HDF5 dataset "sleep": shape (), type "<i4">
360 <HDF5 dataset "chunk-size": shape (), type "<i4">
362 <HDF5 dataset "frequency": shape (), type "<f8">
364 <HDF5 dataset "maximum-fit-frequency": shape (), type "<f8">
366 <HDF5 dataset "minimum-fit-frequency": shape (), type "<f8">
368 <HDF5 dataset "model": shape (), type "|S12">
370 <HDF5 dataset "overlap": shape (), type "|b1">
372 <HDF5 dataset "sample-time": shape (), type "<i4">
374 <HDF5 dataset "window": shape (), type "|S4">
376 <HDF5 dataset "vibration-spacing": shape (), type "<f8">
380 /temperature/0/config
381 /temperature/0/config/temperature
382 <HDF5 dataset "sleep": shape (), type "<i4">
384 /temperature/0/processed
385 <HDF5 dataset "data": shape (), type "<f8">
387 <HDF5 dataset "units": shape (), type "|S1">
390 <HDF5 dataset "data": shape (), type "<f8">
392 <HDF5 dataset "units": shape (), type "|S1">
399 /vibration/0/config/deflection
401 /vibration/0/config/vibration
402 <HDF5 dataset "chunk-size": shape (), type "<i4">
404 <HDF5 dataset "frequency": shape (), type "<f8">
407 /vibration/0/processed
408 <HDF5 dataset "data": shape (), type "<f8">
410 <HDF5 dataset "units": shape (), type "|S6">
413 <HDF5 dataset "data": shape (65536,), type "<u2">
415 <HDF5 dataset "units": shape (), type "|S4">
419 >>> calibrator,data,raw_data = load_all(filename=filename)
420 >>> calibrator.load_from_config(devices=devices)
421 >>> print(calibrator.config.dump()) # doctest: +ELLIPSIS, +REPORT_UDIFF
428 >>> pprint(data) # doctest: +ELLIPSIS, +REPORT_UDIFF
429 {'processed': {'spring_constant': ...
430 'spring_constant_deviation': ...},
431 'raw': {'bump': array([...]),
432 'temperature': array([...]),
433 'vibration': array([...])}}
435 >>> pprint(raw_data) # doctest: +ELLIPSIS, +REPORT_UDIFF
436 {'bump': [{'config': {'bump': <BumpConfig ...>},
438 'raw': {'deflection': array([...], dtype=uint16),
439 'z': array([...], dtype=uint16)}},
442 'temperature': [{'config': {'temperature': <TemperatureConfig ...>},
447 'vibration': [{'config': {'vibration': <InputChannelConfig ...>},
449 'raw': array([...], dtype=uint16)},
453 Close the Comedi devices.
455 >>> for device in devices:
458 Cleanup our temporary config file.
460 >>> os.remove(filename)
462 def __init__(self, config, afm=None):
466 def load_from_config(self, devices):
468 self.afm = _AFM(config=self.config['afm'])
469 self.afm.load_from_config(devices=devices)
471 def setup_config(self):
473 self.afm.setup_config()
474 self.config['afm'] = self.afm.config
476 def calibrate(self, filename=None, group='/'):
477 """Main calibration method.
480 k cantilever spring constant (in N/m, or equivalently nN/nm)
481 k_s standard deviation in our estimate of k
482 data the data used to determine k
484 data = self.acquire(filename=filename, group=group)
485 k = k_s = bumps = temperatures = vibrations = None
486 bumps = data.get('bump', None)
487 temperatures = data.get('temperature', None)
488 vibrations = data.get('vibration', None)
489 if None not in [bumps, temperatures, vibrations]:
491 bumps=bumps, temperatures=temperatures, vibrations=vibrations)
492 if filename is not None:
494 filename=filename, group='{}calibration/'.format(group),
495 spring_constant=k, spring_constant_deviation=k_s, **data)
496 return (k, k_s, data)
498 def acquire(self, filename=None, group='/'):
499 """Acquire data for calibrating a cantilever in one function.
501 Outputs a dict of `action` -> `data_array` pairs, for each
502 action (bump, temperature, vibration) that is actually
503 configured. For example, if you wanted to skip the surface
504 approach, bumping, and retraction, you could just set
505 `.config['bump']` to `None`.
507 The temperatures are collected after moving far from the
508 surface but before and vibrations are measured to give
509 everything time to settle after the big move.
511 Because theres a fair amount of data coming in during a
512 calibration, we save the data as it comes in. So the
513 procedure is bump-0, save-bump-0, bump-1, save-bump-0, etc.
514 To disable the saving, just set `filename` to `None`.
516 if filename is not None:
517 assert group.endswith('/'), group
518 self.save(filename=filename, group='{}config/'.format(group))
520 if self.config['bump'] and self.config['num-bumps'] > 0:
521 data['bump'] = _zeros((self.config['num-bumps'],), dtype=_float)
522 for i in range(self.config['num-bumps']):
523 _LOG.info('acquire bump {} of {}'.format(
524 i, self.config['num-bumps']))
525 data['bump'][i] = _bump(
526 afm=self.afm, config=self.config['bump'],
527 filename=filename, group='{}bump/{}/'.format(group, i))
528 _LOG.debug('bumps: {}'.format(data['bump']))
529 self.afm.move_away_from_surface(
530 distance=self.config['vibration-spacing'])
531 if self.config['temperature'] and self.config['num-temperatures'] > 0:
532 data['temperature'] = _zeros(
533 (self.config['num-temperatures'],), dtype=_float)
534 for i in range(self.config['num-temperatures']):
535 _LOG.info('acquire temperature {} of {}'.format(
536 i, self.config['num-temperatures']))
537 data['temperature'][i] = _temperature(
538 get=self.afm.get_temperature,
539 config=self.config['temperature'],
541 group='{}temperature/{}/'.format(group, i))
542 _sleep(self.config['temperature']['sleep'])
543 _LOG.debug('temperatures: {}'.format(data['temperature']))
544 if self.config['vibration'] and self.config['num-vibrations'] > 0:
545 data['vibration'] = _zeros(
546 (self.config['num-vibrations'],), dtype=_float)
547 for i in range(self.config['num-vibrations']):
548 data['vibration'][i] = _vibration(
549 piezo=self.afm.piezo, config=self.config['vibration'],
551 group='{}vibration/{}/'.format(group, i))
552 _LOG.debug('vibrations: {}'.format(data['vibration']))
555 def save(self, filename=None, group='/'):
556 storage = _HDF5_Storage(filename=filename, group=group)
557 storage.save(config=self.config)
560 def save_results(filename=None, group='/', bump=None,
561 temperature=None, vibration=None, spring_constant=None,
562 spring_constant_deviation=None):
564 _SaveSpec(item=bump, relpath='raw/photodiode-sensitivity',
565 array=True, units='V/m'),
566 _SaveSpec(item=temperature, relpath='raw/temperature',
567 array=True, units='K'),
568 _SaveSpec(item=vibration, relpath='raw/vibration',
569 array=True, units='V^2/Hz'),
570 _SaveSpec(item=spring_constant, relpath='processed/spring-constant',
571 units='N/m', deviation=spring_constant_deviation),
573 _save(filename=filename, group=group, specs=specs)
576 def load_results(filename, group='/'):
577 """Load results saved with `.save_results()`."""
579 _SaveSpec(key=('raw', 'bump'),
580 relpath='raw/photodiode-sensitivity',
581 array=True, units='V/m'),
582 _SaveSpec(key=('raw', 'temperature'), relpath='raw/temperature',
583 array=True, units='K'),
584 _SaveSpec(key=('raw', 'vibration'),
585 relpath='raw/vibration',
586 array=True, units='V^2/Hz'),
587 _SaveSpec(key=('processed', 'spring_constant'),
588 relpath='processed/spring-constant',
589 units='N/m', deviation='spring_constant_deviation'),
591 return _load(filename=filename, group=group, specs=specs)
593 def load_raw(self, filename=None, group='/'):
594 """Load results saved during `.aquire()` by bumps, etc."""
596 with _h5py.File(filename, 'r') as f:
597 for name,loader in [('bump',_bump_load),
598 ('temperature', _temperature_load),
599 ('vibration', _vibration_load),
601 n = self.config['num-{}s'.format(name)]
606 cwg = f['{}{}/{}/'.format(group, name, i)]
610 data[name].append(loader(group=cwg))