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, save, and load cantilever calibration bump data.
23 For measuring photodiode sensitivity.
25 The relevent physical quantities are:
26 Vzp_out Output z-piezo voltage (what we generate)
27 Vzp Applied z-piezo voltage (after external ZPGAIN)
28 Zp The z-piezo position
29 Zcant The cantilever vertical deflection
30 Vphoto The photodiode vertical deflection voltage (what we measure)
32 Which are related by the parameters:
34 zp_sensitivity Zp / Vzp
35 photo_sensitivity Vphoto / Zcant
37 Cantilever calibration assumes a pre-calibrated z-piezo
38 (zp_sensitivity) and amplifier (zp_gain). In our lab, the z-piezo is
39 calibrated by imaging a calibration sample, which has features with
40 well defined sizes, and the gain is set with a knob on our modified
43 Photo-sensitivity is measured by bumping the cantilever against the
44 surface, where `Zp = Zcant` (see the `bump_*()` family of functions).
45 The measured slope Vphoto/Vout is converted to photo-sensitivity via
47 Vphoto/Vzp_out * Vzp_out/Vzp * Vzp/Zp * Zp/Zcant = Vphoto/Zcant
48 (measured) (1/zp_gain) (1/zp_sensitivity) (1) (photo_sensitivity)
50 We do all these measurements a few times to estimate statistical
53 The functions are layed out in the families:
55 For each family, * can be any of:
56 acquire get real-world data
57 save store real-world data to disk
58 load get real-world data from disk
59 analyze interperate the real-world data.
60 plot show a nice graphic to convince people we're working :p
62 A family name without any _* extension (e.g. `bump()`), runs `*_acquire()`,
63 `*_save()`, `*_analyze()`.
65 If `package_config['matplotlib']` is `True`, `*_analyze()` will call
66 `*_plot()` internally.
69 import numpy as _numpy
71 from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits
72 from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
74 from . import LOG as _LOG
75 from .bump_analyze import bump_analyze as _bump_analyze
76 from .bump_analyze import bump_save as _bump_save
79 def bump_acquire(afm, bump_config):
80 """Ramps `push_depth` closer and returns to the original position.
83 afm a pyafm.AFM instance
84 bump_config a .config._BumpConfig instance
86 Returns the acquired ramp data dictionary, with data in DAC/ADC bits.
88 afm.move_just_onto_surface(
89 depth=bump_config['initial-position'], far=bump_config['far-steps'])
90 #afm.piezo.jump('z', 32000)
92 _LOG.info('bump the surface to a depth of %g m'
93 % bump_config['push-depth'])
95 axis = afm.piezo.axis_by_name(afm.axis_name)
97 start_pos = afm.piezo.last_output[afm.axis_name]
98 start_pos_m = _convert_bits_to_meters(axis.config, start_pos)
99 close_pos_m = start_pos_m + bump_config['push-depth']
100 close_pos = _convert_meters_to_bits(axis.config, close_pos_m)
102 dtype = afm.piezo.channel_dtype(afm.axis_name, direction='output')
103 appr = _numpy.linspace(
104 start_pos, close_pos, bump_config['samples']).astype(dtype)
105 # switch numpy.append to numpy.concatenate with version 2.0+
106 out = _numpy.append(appr, appr[::-1])
107 out = out.reshape((len(out), 1))
109 # (samples) / (meters) * (meters/second) = (samples/second)
110 freq = (bump_config['samples'] / bump_config['push-depth']
111 * bump_config['push-speed'])
113 data = afm.piezo.ramp(out, freq, output_names=[afm.axis_name],
114 input_names=['deflection'])
116 out = out.reshape((len(out),))
117 data = data.reshape((data.size,))
118 return {afm.axis_name: out, 'deflection': data}
120 def bump(afm, bump_config, filename, group='/'):
121 """Wrapper around bump_acquire(), bump_analyze(), bump_save().
125 >>> from h5config.storage.hdf5 import pprint_HDF5
126 >>> from pycomedi.device import Device
127 >>> from pycomedi.subdevice import StreamingSubdevice
128 >>> from pycomedi.channel import AnalogChannel, DigitalChannel
129 >>> from pycomedi.constant import AREF, IO_DIRECTION, SUBDEVICE_TYPE, UNIT
130 >>> from pypiezo.afm import AFMPiezo
131 >>> from pypiezo.base import PiezoAxis, InputChannel
132 >>> from pypiezo.config import ChannelConfig, AxisConfig
133 >>> from stepper import Stepper
134 >>> from pyafm.afm import AFM
135 >>> from .config import BumpConfig
137 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
140 >>> d = Device('/dev/comedi0')
143 Setup an `AFMPiezo` instance.
145 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
146 ... factory=StreamingSubdevice)
147 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
148 ... factory=StreamingSubdevice)
150 >>> axis_channel = s_out.channel(
151 ... 0, factory=AnalogChannel, aref=AREF.ground)
152 >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
153 >>> for chan in [axis_channel, input_channel]:
154 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
156 We set the minimum voltage for the `z` axis to -9 (a volt above
157 the minimum possible voltage) to help with testing
158 `.get_surface_position`. Without this minimum voltage, small
159 calibration errors could lead to a railed -10 V input for the
160 first few surface approaching steps, which could lead to an
161 `EdgeKink` error instead of a `FlatFit` error.
163 >>> axis_config = AxisConfig()
164 >>> axis_config.update(
165 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
166 >>> axis_channel_config = ChannelConfig()
167 >>> axis_channel_config['name'] = 'z'
168 >>> axis_config['channel'] = axis_channel_config
169 >>> input_channel_config = ChannelConfig()
170 >>> input_channel_config['name'] = 'deflection'
172 >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel)
175 >>> c = InputChannel(config=input_channel_config, channel=input_channel)
178 >>> piezo = AFMPiezo(axes=[a], inputs=[c])
180 Setup a `stepper` instance.
182 >>> s_d = d.find_subdevice_by_type(SUBDEVICE_TYPE.dio)
183 >>> d_channels = [s_d.channel(i, factory=DigitalChannel)
184 ... for i in (0, 1, 2, 3)]
185 >>> for chan in d_channels:
186 ... chan.dio_config(IO_DIRECTION.output)
188 >>> def write(value):
189 ... s_d.dio_bitfield(bits=value, write_mask=2**4-1)
191 >>> stepper = Stepper(write=write)
193 Setup an `AFM` instance.
195 >>> afm = AFM(piezo, stepper)
199 >>> bump_config = BumpConfig()
200 >>> bump(afm, bump_config, filename, group='/bump')
201 TODO: replace skipped example data with real-world values
202 >>> pprint_HDF5(filename) # doctest: +ELLIPSIS, +REPORT_UDIFF
207 <HDF5 dataset "far-steps": shape (), type "<i4">
209 <HDF5 dataset "initial-position": shape (), type "<f8">
211 <HDF5 dataset "model": shape (), type "|S9">
213 <HDF5 dataset "push-depth": shape (), type "<f8">
215 <HDF5 dataset "push-speed": shape (), type "<f8">
217 <HDF5 dataset "samples": shape (), type "<i4">
219 <HDF5 dataset "setpoint": shape (), type "<f8">
221 /bump/config/deflection
222 /bump/config/deflection/channel
223 <HDF5 dataset "channel": shape (), type "<i4">
225 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
226 [ -1.00000000e+01 3.05180438e-04]
227 <HDF5 dataset "conversion-origin": shape (), type "<f8">
229 <HDF5 dataset "device": shape (), type "|S12">
231 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
233 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
235 <HDF5 dataset "maxdata": shape (), type "<i8">
237 <HDF5 dataset "name": shape (), type "|S10">
239 <HDF5 dataset "range": shape (), type "<i4">
241 <HDF5 dataset "subdevice": shape (), type "<i4">
245 /bump/config/z/axis/channel
246 <HDF5 dataset "channel": shape (), type "<i4">
248 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
249 [ -1.00000000e+01 3.05180438e-04]
250 <HDF5 dataset "conversion-origin": shape (), type "<f8">
252 <HDF5 dataset "device": shape (), type "|S12">
254 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
256 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
258 <HDF5 dataset "maxdata": shape (), type "<i8">
260 <HDF5 dataset "name": shape (), type "|S1">
262 <HDF5 dataset "range": shape (), type "<i4">
264 <HDF5 dataset "subdevice": shape (), type "<i4">
266 <HDF5 dataset "gain": shape (), type "<i4">
268 <HDF5 dataset "maximum": shape (), type "<f8">
270 <HDF5 dataset "minimum": shape (), type "<i4">
272 <HDF5 dataset "monitor": shape (), type "|S1">
274 <HDF5 dataset "sensitivity": shape (), type "<f8">
276 <HDF5 dataset "processed": shape (), type "<f8">
279 <HDF5 dataset "deflection": shape (2048,), type "<u2">
281 <HDF5 dataset "z": shape (2048,), type "<u2">
284 Close the Comedi device.
288 Cleanup our temporary config file.
290 >>> os.remove(filename)
292 deflection_channel = afm.piezo.input_channel_by_name('deflection')
293 axis = afm.piezo.axis_by_name(afm.axis_name)
295 data = bump_acquire(afm, bump_config)
296 photo_sensitivity = _bump_analyze(
297 data, bump_config, z_axis_config=axis.config,
298 deflection_channel_config=deflection_channel.config)
300 filename, group, data, bump_config,
301 z_axis_config=axis.config,
302 deflection_channel_config=deflection_channel.config,
303 processed_bump=photo_sensitivity)
304 return photo_sensitivity