Get calibcant working with the new load_from_config-based pyafm.
[calibcant.git] / calibcant / bump.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, save, and load cantilever calibration bump data.
20
21 For measuring photodiode sensitivity.
22
23 The relevent physical quantities are:
24   Vzp_out  Output z-piezo voltage (what we generate)
25   Vzp      Applied z-piezo voltage (after external ZPGAIN)
26   Zp       The z-piezo position
27   Zcant    The cantilever vertical deflection
28   Vphoto   The photodiode vertical deflection voltage (what we measure)
29
30 Which are related by the parameters:
31   zp_gain           Vzp_out / Vzp
32   zp_sensitivity    Zp / Vzp
33   photo_sensitivity Vphoto / Zcant
34
35 Cantilever calibration assumes a pre-calibrated z-piezo
36 (zp_sensitivity) and amplifier (zp_gain).  In our lab, the z-piezo is
37 calibrated by imaging a calibration sample, which has features with
38 well defined sizes, and the gain is set with a knob on our modified
39 NanoScope.
40
41 Photo-sensitivity is measured by bumping the cantilever against the
42 surface, where `Zp = Zcant`.  The measured slope Vphoto/Vout is
43 converted to photo-sensitivity via::
44
45   Vphoto/Vzp_out * Vzp_out/Vzp  * Vzp/Zp   *    Zp/Zcant =    Vphoto/Zcant
46    (measured)      (1/zp_gain) (1/zp_sensitivity)  (1)    (photo_sensitivity)
47
48 We do all these measurements a few times to estimate statistical
49 errors.
50 """
51
52 import numpy as _numpy
53
54 from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits
55 from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
56
57 from . import LOG as _LOG
58 from .bump_analyze import analyze as _analyze
59 from .bump_analyze import save as _save
60
61
62 def acquire(afm, config):
63     """Ramps `push_depth` closer and returns to the original position.
64
65     Inputs:
66       afm     a pyafm.AFM instance
67       config  a .config._BumpConfig instance
68
69     Returns the acquired ramp data dictionary, with data in DAC/ADC bits.
70     """
71     afm.move_just_onto_surface(
72         depth=config['initial-position'], far=config['far-steps'],
73         setpoint=config['setpoint'],
74         min_slope_ratio=config['min-slope-ratio'])
75     #afm.piezo.jump('z', 32000)
76
77     _LOG.info(
78         'bump the surface to a depth of {} m with a setpoint of {} V'.format(
79             config['push-depth'], config['setpoint']))
80
81     axis = afm.piezo.axis_by_name(afm.config['main-axis'])
82
83     start_pos = afm.piezo.last_output[afm.config['main-axis']]
84     start_pos_m = _convert_bits_to_meters(axis.config, start_pos)
85     close_pos_m = start_pos_m + config['push-depth']
86     close_pos = _convert_meters_to_bits(axis.config, close_pos_m)
87
88     dtype = afm.piezo.channel_dtype(
89         afm.config['main-axis'], direction='output')
90     appr = _numpy.linspace(
91         start_pos, close_pos, config['samples']).astype(dtype)
92     # switch numpy.append to numpy.concatenate with version 2.0+
93     out = _numpy.append(appr, appr[::-1])
94     out = out.reshape((len(out), 1))
95
96     # (samples) / (meters) * (meters/second) = (samples/second)
97     freq = (config['samples'] / config['push-depth']
98             * config['push-speed'])
99
100     data = afm.piezo.ramp(out, freq, output_names=[afm.config['main-axis']],
101                           input_names=['deflection'])
102
103     out = out.reshape((len(out),))
104     data = data.reshape((data.size,))
105     return {afm.config['main-axis']: out, 'deflection': data}
106
107 def run(afm, config, filename, group='/'):
108     """Wrapper around acquire(), analyze(), save().
109
110     >>> import os
111     >>> import tempfile
112     >>> from h5config.storage.hdf5 import pprint_HDF5
113     >>> from pyafm.storage import load_afm
114     >>> from .config import BumpConfig
115
116     >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
117     >>> os.close(fd)
118
119     >>> devices = []
120     >>> afm = load_afm()
121     >>> afm.load_from_config(devices=devices)
122
123     Test a bump:
124
125     >>> config = BumpConfig()
126     >>> output = run(afm=afm, config=config, filename=filename, group='/')
127     >>> output  # doctest: +SKIP
128     23265462.3047795
129     >>> pprint_HDF5(filename)  # doctest: +ELLIPSIS, +REPORT_UDIFF
130     /
131       /config
132         /config/bump
133           <HDF5 dataset "far-steps": shape (), type "<i4">
134             200
135           <HDF5 dataset "initial-position": shape (), type "<f8">
136             -5e-08
137           <HDF5 dataset "min-slope-ratio": shape (), type "<f8">
138             10.0
139           <HDF5 dataset "model": shape (), type "|S9">
140             quadratic
141           <HDF5 dataset "push-depth": shape (), type "<f8">
142             2e-07
143           <HDF5 dataset "push-speed": shape (), type "<f8">
144             1e-06
145           <HDF5 dataset "samples": shape (), type "<i4">
146             1024
147           <HDF5 dataset "setpoint": shape (), type "<f8">
148             2.0
149       /processed
150         <HDF5 dataset "data": shape (), type "<f8">
151           ...
152         <HDF5 dataset "units": shape (), type "|S3">
153           V/m
154       /raw
155         /raw/deflection
156           <HDF5 dataset "data": shape (2048,), type "<u2">
157             [...]
158           <HDF5 dataset "units": shape (), type "|S4">
159             bits
160         /raw/z
161           <HDF5 dataset "data": shape (2048,), type "<u2">
162             [...]
163           <HDF5 dataset "units": shape (), type "|S4">
164             bits
165
166     Close the Comedi devices.
167
168     >>> for device in devices:
169     ...     device.close()
170
171     Cleanup our temporary config file.
172
173     >>> os.remove(filename)
174     """
175     deflection_channel = afm.piezo.input_channel_by_name('deflection')
176     axis = afm.piezo.axis_by_name(afm.config['main-axis'])
177
178     raw = acquire(afm, config)
179     photo_sensitivity = _analyze(
180         config=config, data=raw, z_axis_config=axis.config,
181         deflection_channel_config=deflection_channel.config)
182     _save(filename=filename, group=group, config=config,
183           raw=raw, processed=photo_sensitivity)
184     return photo_sensitivity