964ee24f2976a03b496efe92855ebe92cf777df9
[calibcant.git] / calibcant / calibrate.py
1 # calibcant - tools for thermally calibrating AFM cantilevers
2 #
3 # Copyright (C) 2008-2012 W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of calibcant.
6 #
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
10 # version.
11 #
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.
15 #
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/>.
18
19 """Acquire and analyze cantilever calibration data.
20
21 The relevent physical quantities are:
22
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
32
33 Which are related by the parameters:
34
35 * zpGain           Vzp_out / Vzp
36 * zpSensitivity    Zp / Vzp
37 * photoSensitivity Vphoto / Zcant
38 * k_cant           Fcant / Zcant
39
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.
44
45 photoSensitivity is measured by bumping the cantilever against the
46 surface, where Zp = Zcant (see bump_acquire() and the bump_analyze
47 submodule).
48
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.
53
54 .. math::  \frac{1}{2} k_b T = \frac{1}{2} k_cant <Zcant**2>
55
56 so
57
58 .. math::   k_cant = \frac{k_b T}{Zcant**2}
59
60 but
61
62 .. math::   Zcant = \frac{Vphoto}{photoSensitivity}
63
64 so
65
66 .. math:: k_cant = \frac{k_b T * photoSensitivty^2}{<Vphoto**2>}
67
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>.
76
77 We do all these measurements a few times to estimate statistical
78 errors.
79 """
80
81 from time import sleep as _sleep
82
83 from numpy import zeros as _zeros
84 from numpy import float as _float
85
86 import h5py as _h5py
87 from pyafm.afm import AFM as _AFM
88 from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage
89
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 .analyze import save_results as _save_results
100 from .util import SaveSpec as _SaveSpec
101 from .util import load as _load
102
103
104 def load(filename=None, group='/'):
105     config = _CalibrateConfig(storage=_HDF5_Storage(
106             filename=filename, group=group))
107     config.load()
108     return Calibrator(config=config)
109
110 def load_all(filename=None, group='/', raw=True):
111     "Load all data from a `Calibration.calibrate()` run."
112     assert group.endswith('/'), group
113     calibrator = load(
114         filename=filename, group='{}config/'.format(group))
115     data = calibrator.load_results(
116         filename=filename, group='{}calibration/'.format(group))
117     if raw:
118         raw_data = calibrator.load_raw(filename=filename, group=group)
119     else:
120         raw_data = None
121     return (calibrator, data, raw_data)
122
123
124 class Calibrator (object):
125     """Calibrate a cantilever spring constant using the thermal tune method.
126
127     >>> import os
128     >>> from pprint import pprint
129     >>> import tempfile
130     >>> from h5config.storage.hdf5 import pprint_HDF5
131     >>> from pyafm.storage import load_afm
132     >>> from .config import (CalibrateConfig, BumpConfig,
133     ...     TemperatureConfig, VibrationConfig)
134
135     >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
136     >>> os.close(fd)
137
138     >>> devices = []
139
140     >>> afm = load_afm()
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)
149     >>> c.setup_config()
150     >>> k,k_s,data = c.calibrate(filename=filename)
151     >>> k  # doctest: +SKIP
152     0.058402262154840491
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
160     /
161       /bump
162         /bump/0
163           /bump/0/config
164             /bump/0/config/bump
165               <HDF5 dataset "far-steps": shape (), type "<i4">
166                 200
167               <HDF5 dataset "initial-position": shape (), type "<f8">
168                 -5e-08
169               ...
170           /bump/0/processed
171             <HDF5 dataset "data": shape (), type "<f8">
172               ...
173             <HDF5 dataset "units": shape (), type "|S3">
174               V/m
175           /bump/0/raw
176             /bump/0/raw/deflection
177               <HDF5 dataset "data": shape (2048,), type "<u2">
178                 [...]
179               <HDF5 dataset "units": shape (), type "|S4">
180                 bits
181             /bump/0/raw/z
182               <HDF5 dataset "data": shape (2048,), type "<u2">
183                 [...]
184               <HDF5 dataset "units": shape (), type "|S4">
185                 bits
186         /bump/1
187         ...
188       /config
189         /config/afm
190           <HDF5 dataset "fallback-temperature": shape (), type "<f8">
191             295.15
192           <HDF5 dataset "far": shape (), type "<f8">
193             3e-05
194           <HDF5 dataset "main-axis": shape (), type "|S1">
195             z
196           <HDF5 dataset "name": shape (), type "|S5">
197             1B3D9
198           /config/afm/piezo
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">
203                     ground
204                   <HDF5 dataset "channel": shape (), type "<i4">
205                     0
206                   <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
207                     [ -1.00000000e+01   3.05180438e-04]
208                   <HDF5 dataset "conversion-origin": shape (), type "<f8">
209                     0.0
210                   <HDF5 dataset "device": shape (), type "|S12">
211                     /dev/comedi0
212                   <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
213                     [    0.    3276.75]
214                   <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
215                     -10.0
216                   <HDF5 dataset "maxdata": shape (), type "<i8">
217                     65535
218                   <HDF5 dataset "name": shape (), type "|S1">
219                     z
220                   <HDF5 dataset "range": shape (), type "<i4">
221                     0
222                   <HDF5 dataset "subdevice": shape (), type "<i4">
223                     1
224                 <HDF5 dataset "gain": shape (), type "<f8">
225                   20.0
226                 <HDF5 dataset "maximum": shape (), type "<f8">
227                   9.0
228                 <HDF5 dataset "minimum": shape (), type "<f8">
229                   -9.0
230                 <HDF5 dataset "monitor": shape (), type "|S1">
231     <BLANKLINE>
232                 <HDF5 dataset "sensitivity": shape (), type "<f8">
233                   8.8e-09
234               /config/afm/piezo/axes/1
235                 /config/afm/piezo/axes/1/channel
236                   <HDF5 dataset "analog-reference": shape (), type "|S6">
237                     ground
238                   <HDF5 dataset "channel": shape (), type "<i4">
239                     1
240                   <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
241                     [ -1.00000000e+01   3.05180438e-04]
242                   <HDF5 dataset "conversion-origin": shape (), type "<f8">
243                     0.0
244                   <HDF5 dataset "device": shape (), type "|S12">
245                     /dev/comedi0
246                   <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
247                     [    0.    3276.75]
248                   <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
249                     -10.0
250                   <HDF5 dataset "maxdata": shape (), type "<i8">
251                     65535
252                   <HDF5 dataset "name": shape (), type "|S1">
253                     x
254                   <HDF5 dataset "range": shape (), type "<i4">
255                     0
256                   <HDF5 dataset "subdevice": shape (), type "<i4">
257                     1
258                 <HDF5 dataset "gain": shape (), type "<f8">
259                   20.0
260                 <HDF5 dataset "maximum": shape (), type "<f8">
261                   8.0
262                 <HDF5 dataset "minimum": shape (), type "<f8">
263                   -8.0
264                 <HDF5 dataset "monitor": shape (), type "|S1">
265     <BLANKLINE>
266                 <HDF5 dataset "sensitivity": shape (), type "<f8">
267                   4.16e-09
268             /config/afm/piezo/inputs
269               /config/afm/piezo/inputs/0
270                 <HDF5 dataset "analog-reference": shape (), type "|S4">
271                   diff
272                 <HDF5 dataset "channel": shape (), type "<i4">
273                   0
274                 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
275                   [ -1.00000000e+01   3.05180438e-04]
276                 <HDF5 dataset "conversion-origin": shape (), type "<f8">
277                   0.0
278                 <HDF5 dataset "device": shape (), type "|S12">
279                   /dev/comedi0
280                 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
281                   [    0.    3276.75]
282                 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
283                   -10.0
284                 <HDF5 dataset "maxdata": shape (), type "<i8">
285                   65535
286                 <HDF5 dataset "name": shape (), type "|S10">
287                   deflection
288                 <HDF5 dataset "range": shape (), type "<i4">
289                   0
290                 <HDF5 dataset "subdevice": shape (), type "<i4">
291                   0
292             <HDF5 dataset "name": shape (), type "|S5">
293               2253E
294           /config/afm/stepper
295             <HDF5 dataset "backlash": shape (), type "<i4">
296               100
297             <HDF5 dataset "delay": shape (), type "<f8">
298               0.01
299             <HDF5 dataset "full-step": shape (), type "|b1">
300               True
301             <HDF5 dataset "logic": shape (), type "|b1">
302               True
303             <HDF5 dataset "name": shape (), type "|S9">
304               z-stepper
305             /config/afm/stepper/port
306               <HDF5 dataset "channels": shape (4,), type "<i4">
307                 [0 1 2 3]
308               <HDF5 dataset "device": shape (), type "|S12">
309                 /dev/comedi0
310               <HDF5 dataset "direction": shape (), type "|S6">
311                 output
312               <HDF5 dataset "name": shape (), type "|S12">
313                 stepper DB-9
314               <HDF5 dataset "subdevice": shape (), type "<i4">
315                 2
316               <HDF5 dataset "subdevice-type": shape (), type "|S3">
317                 dio
318             <HDF5 dataset "step-size": shape (), type "<f8">
319               1.7e-07
320           /config/afm/temperature
321             <HDF5 dataset "baudrate": shape (), type "<i4">
322               9600
323             <HDF5 dataset "controller": shape (), type "<i4">
324               1
325             <HDF5 dataset "device": shape (), type "|S10">
326               /dev/ttyS0
327             <HDF5 dataset "max-current": shape (), type "<f8">
328               0.0
329             <HDF5 dataset "name": shape (), type "|S14">
330               room (ambient)
331             <HDF5 dataset "units": shape (), type "|S7">
332               Celsius
333         /config/bump
334           <HDF5 dataset "far-steps": shape (), type "<i4">
335             200
336           <HDF5 dataset "initial-position": shape (), type "<f8">
337             -5e-08
338           <HDF5 dataset "min-slope-ratio": shape (), type "<f8">
339             10.0
340           <HDF5 dataset "model": shape (), type "|S9">
341             quadratic
342           <HDF5 dataset "push-depth": shape (), type "<f8">
343             2e-07
344           <HDF5 dataset "push-speed": shape (), type "<f8">
345             1e-06
346           <HDF5 dataset "samples": shape (), type "<i4">
347             1024
348           <HDF5 dataset "setpoint": shape (), type "<f8">
349             2.0
350         <HDF5 dataset "num-bumps": shape (), type "<i4">
351           10
352         <HDF5 dataset "num-temperatures": shape (), type "<i4">
353           10
354         <HDF5 dataset "num-vibrations": shape (), type "<i4">
355           20
356         /config/temperature
357           <HDF5 dataset "sleep": shape (), type "<i4">
358             1
359         /config/vibration
360           <HDF5 dataset "chunk-size": shape (), type "<i4">
361             2048
362           <HDF5 dataset "frequency": shape (), type "<f8">
363             50000.0
364           <HDF5 dataset "maximum-fit-frequency": shape (), type "<f8">
365             25000.0
366           <HDF5 dataset "minimum-fit-frequency": shape (), type "<f8">
367             500.0
368           <HDF5 dataset "model": shape (), type "|S12">
369             Breit-Wigner
370           <HDF5 dataset "overlap": shape (), type "|b1">
371             False
372           <HDF5 dataset "sample-time": shape (), type "<i4">
373             1
374           <HDF5 dataset "window": shape (), type "|S4">
375             Hann
376         <HDF5 dataset "vibration-spacing": shape (), type "<f8">
377           5e-05
378       /temperature
379         /temperature/0
380           /temperature/0/config
381             /temperature/0/config/temperature
382               <HDF5 dataset "sleep": shape (), type "<i4">
383                 1
384           /temperature/0/processed
385             <HDF5 dataset "data": shape (), type "<f8">
386               ...
387             <HDF5 dataset "units": shape (), type "|S1">
388               K
389           /temperature/0/raw
390             <HDF5 dataset "data": shape (), type "<f8">
391               ...
392             <HDF5 dataset "units": shape (), type "|S1">
393               K
394         /temperature/1
395         ...
396       /vibration
397         /vibration/0
398           /vibration/0/config
399             /vibration/0/config/deflection
400               ...
401             /vibration/0/config/vibration
402               <HDF5 dataset "chunk-size": shape (), type "<i4">
403                 2048
404               <HDF5 dataset "frequency": shape (), type "<f8">
405                 50000.0
406               ...
407           /vibration/0/processed
408             <HDF5 dataset "data": shape (), type "<f8">
409               ...
410             <HDF5 dataset "units": shape (), type "|S6">
411               V^2/Hz
412           /vibration/0/raw
413             <HDF5 dataset "data": shape (65536,), type "<u2">
414               [...]
415             <HDF5 dataset "units": shape (), type "|S4">
416               bits
417         ...
418
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
422     afm:
423       name: 1B3D9
424       main-axis: z
425       piezo:
426         name: 2253E
427     ...
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([...])}}
434
435     >>> pprint(raw_data)  # doctest: +ELLIPSIS, +REPORT_UDIFF
436     {'bump': [{'config': {'bump': <BumpConfig ...>},
437                'processed': ...,
438                'raw': {'deflection': array([...], dtype=uint16),
439                        'z': array([...], dtype=uint16)}},
440               {...},
441               ...],
442      'temperature': [{'config': {'temperature': <TemperatureConfig ...>},
443                       'processed': ...,
444                       'raw': ...},
445                      {...},
446                      ...],
447      'vibration': [{'config': {'vibration': <InputChannelConfig ...>},
448                     'processed': ...
449                     'raw': array([...], dtype=uint16)},
450                    {...},
451                    ...]}
452
453     Close the Comedi devices.
454
455     >>> for device in devices:
456     ...     device.close()
457
458     Cleanup our temporary config file.
459
460     >>> os.remove(filename)
461     """
462     def __init__(self, config, afm=None):
463         self.config = config
464         self.afm = afm
465
466     def load_from_config(self, devices):
467         if self.afm is None:
468             self.afm = _AFM(config=self.config['afm'])
469             self.afm.load_from_config(devices=devices)
470
471     def setup_config(self):
472         if self.afm:
473             self.afm.setup_config()
474             self.config['afm'] = self.afm.config
475
476     def calibrate(self, filename=None, group='/'):
477         """Main calibration method.
478
479         Outputs:
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
483         """
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]:
490             k,k_s = _analyze(
491                 bumps=bumps, temperatures=temperatures, vibrations=vibrations)
492         if filename is not None:
493             self.save_results(
494                 filename=filename, group='{}calibration/'.format(group),
495                 spring_constant=k, spring_constant_deviation=k_s, **data)
496         return (k, k_s, data)
497
498     def acquire(self, filename=None, group='/'):
499         """Acquire data for calibrating a cantilever in one function.
500
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`.
506
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.
510
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`.
515         """
516         if filename is not None:
517             assert group.endswith('/'), group
518             self.save(filename=filename, group='{}config/'.format(group))
519         data = {}
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'],
540                     filename=filename,
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'],
550                     filename=filename,
551                     group='{}vibration/{}/'.format(group, i))
552             _LOG.debug('vibrations: {}'.format(data['vibration']))
553         return data
554
555     def save(self, filename=None, group='/'):
556         storage = _HDF5_Storage(filename=filename, group=group)
557         storage.save(config=self.config)
558
559     @staticmethod
560     def load_results(filename, group='/'):
561         """Load results saved with `.save_results()`."""
562         specs = [
563             _SaveSpec(key=('raw', 'bump'),
564                       relpath='raw/photodiode-sensitivity',
565                       array=True, units='V/m'),
566             _SaveSpec(key=('raw', 'temperature'), relpath='raw/temperature',
567                       array=True, units='K'),
568             _SaveSpec(key=('raw', 'vibration'),
569                       relpath='raw/vibration',
570                       array=True, units='V^2/Hz'),
571             _SaveSpec(key=('processed', 'spring_constant'),
572                       relpath='processed/spring-constant',
573                       units='N/m', deviation='spring_constant_deviation'),
574             ]
575         return _load(filename=filename, group=group, specs=specs)
576
577     def load_raw(self, filename=None, group='/'):
578         """Load results saved during `.aquire()` by bumps, etc."""
579         data = {}
580         with _h5py.File(filename, 'r') as f:
581             for name,loader in [('bump',_bump_load),
582                                 ('temperature', _temperature_load),
583                                 ('vibration', _vibration_load),
584                                 ]:
585                 n = self.config['num-{}s'.format(name)]
586                 if n > 0:
587                     data[name] = []
588                     for i in range(n):
589                         try:
590                             cwg = f['{}{}/{}/'.format(group, name, i)]
591                         except KeyError:
592                             pass
593                         else:
594                             data[name].append(loader(group=cwg))
595         return data
596
597 Calibrator.save_results = _save_results