4d0db8439fe8bc01de3d0ee5b878d1c3d2275c7b
[calibcant.git] / calibcant / bump.py
1 # calibcant - tools for thermally calibrating AFM cantilevers
2 #
3 # Copyright (C) 2008-2011 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
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.
11 #
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.
16 #
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/>.
20
21 """Acquire, save, and load cantilever calibration bump data.
22
23 For measuring photodiode sensitivity.
24
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)
31
32 Which are related by the parameters:
33   zp_gain           Vzp_out / Vzp
34   zp_sensitivity    Zp / Vzp
35   photo_sensitivity Vphoto / Zcant
36
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
41 NanoScope.
42
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
46
47   Vphoto/Vzp_out * Vzp_out/Vzp  * Vzp/Zp   *    Zp/Zcant =    Vphoto/Zcant
48    (measured)      (1/zp_gain) (1/zp_sensitivity)  (1)    (photo_sensitivity)
49
50 We do all these measurements a few times to estimate statistical
51 errors.
52
53 The functions are layed out in the families:
54   bump_*()
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
61
62 A family name without any _* extension (e.g. `bump()`), runs `*_acquire()`,
63 `*_save()`, `*_analyze()`.
64
65 If `package_config['matplotlib']` is `True`, `*_analyze()` will call
66 `*_plot()` internally.
67 """
68
69 import numpy as _numpy
70
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
73
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
77
78
79 def bump_acquire(afm, bump_config):
80     """Ramps `push_depth` closer and returns to the original position.
81
82     Inputs:
83       afm          a pyafm.AFM instance
84       bump_config  a .config._BumpConfig instance
85
86     Returns the acquired ramp data dictionary, with data in DAC/ADC bits.
87     """
88     afm.move_just_onto_surface(
89         depth=bump_config['initial-position'], far=bump_config['far-steps'],
90         setpoint=bump_config['setpoint'],
91         min_slope_ratio=bump_config['min-slope-ratio'])
92     #afm.piezo.jump('z', 32000)
93
94     _LOG.info('bump the surface to a depth of %g m with a setpoint of %g V'
95               % (bump_config['push-depth'], bump_config['setpoint']))
96
97     axis = afm.piezo.axis_by_name(afm.axis_name)
98
99     start_pos = afm.piezo.last_output[afm.axis_name]
100     start_pos_m = _convert_bits_to_meters(axis.config, start_pos)
101     close_pos_m = start_pos_m + bump_config['push-depth']
102     close_pos = _convert_meters_to_bits(axis.config, close_pos_m)
103
104     dtype = afm.piezo.channel_dtype(afm.axis_name, direction='output')
105     appr = _numpy.linspace(
106         start_pos, close_pos, bump_config['samples']).astype(dtype)
107     # switch numpy.append to numpy.concatenate with version 2.0+
108     out = _numpy.append(appr, appr[::-1])
109     out = out.reshape((len(out), 1))
110
111     # (samples) / (meters) * (meters/second) = (samples/second)
112     freq = (bump_config['samples'] / bump_config['push-depth']
113             * bump_config['push-speed'])
114
115     data = afm.piezo.ramp(out, freq, output_names=[afm.axis_name],
116                           input_names=['deflection'])
117
118     out = out.reshape((len(out),))
119     data = data.reshape((data.size,))
120     return {afm.axis_name: out, 'deflection': data}
121
122 def bump(afm, bump_config, filename, group='/'):
123     """Wrapper around bump_acquire(), bump_analyze(), bump_save().
124
125     >>> import os
126     >>> import tempfile
127     >>> from h5config.storage.hdf5 import pprint_HDF5
128     >>> from pycomedi.device import Device
129     >>> from pycomedi.subdevice import StreamingSubdevice
130     >>> from pycomedi.channel import AnalogChannel, DigitalChannel
131     >>> from pycomedi.constant import AREF, IO_DIRECTION, SUBDEVICE_TYPE, UNIT
132     >>> from pypiezo.afm import AFMPiezo
133     >>> from pypiezo.base import PiezoAxis, InputChannel
134     >>> from pypiezo.config import ChannelConfig, AxisConfig
135     >>> from stepper import Stepper
136     >>> from pyafm.afm import AFM
137     >>> from .config import BumpConfig
138
139     >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
140     >>> os.close(fd)
141
142     >>> d = Device('/dev/comedi0')
143     >>> d.open()
144
145     Setup an `AFMPiezo` instance.
146
147     >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
148     ...     factory=StreamingSubdevice)
149     >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
150     ...     factory=StreamingSubdevice)
151
152     >>> axis_channel = s_out.channel(
153     ...     0, factory=AnalogChannel, aref=AREF.ground)
154     >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
155     >>> for chan in [axis_channel, input_channel]:
156     ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
157
158     We set the minimum voltage for the `z` axis to -9 (a volt above
159     the minimum possible voltage) to help with testing
160     `.get_surface_position`.  Without this minimum voltage, small
161     calibration errors could lead to a railed -10 V input for the
162     first few surface approaching steps, which could lead to an
163     `EdgeKink` error instead of a `FlatFit` error.
164
165     >>> axis_config = AxisConfig()
166     >>> axis_config.update(
167     ...     {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
168     >>> axis_channel_config = ChannelConfig()
169     >>> axis_channel_config['name'] = 'z'
170     >>> axis_config['channel'] = axis_channel_config
171     >>> input_channel_config = ChannelConfig()
172     >>> input_channel_config['name'] = 'deflection'
173
174     >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel)
175     >>> a.setup_config()
176
177     >>> c = InputChannel(config=input_channel_config, channel=input_channel)
178     >>> c.setup_config()
179
180     >>> piezo = AFMPiezo(axes=[a], inputs=[c])
181
182     Setup a `stepper` instance.
183
184     >>> s_d = d.find_subdevice_by_type(SUBDEVICE_TYPE.dio)
185     >>> d_channels = [s_d.channel(i, factory=DigitalChannel)
186     ...             for i in (0, 1, 2, 3)]
187     >>> for chan in d_channels:
188     ...     chan.dio_config(IO_DIRECTION.output)
189
190     >>> def write(value):
191     ...     s_d.dio_bitfield(bits=value, write_mask=2**4-1)
192
193     >>> stepper = Stepper(write=write)
194
195     Setup an `AFM` instance.
196
197     >>> afm = AFM(piezo, stepper)
198
199     Test a bump:
200
201     >>> bump_config = BumpConfig()
202     >>> bump(afm, bump_config, filename, group='/bump')
203     TODO: replace skipped example data with real-world values
204     >>> pprint_HDF5(filename)  # doctest: +ELLIPSIS, +REPORT_UDIFF
205     /
206       /bump
207         /bump/config
208           /bump/config/bump
209             <HDF5 dataset "far-steps": shape (), type "<i4">
210               200
211             <HDF5 dataset "initial-position": shape (), type "<f8">
212               -5e-08
213             <HDF5 dataset "model": shape (), type "|S9">
214               quadratic
215             <HDF5 dataset "push-depth": shape (), type "<f8">
216               2e-07
217             <HDF5 dataset "push-speed": shape (), type "<f8">
218               1e-06
219             <HDF5 dataset "samples": shape (), type "<i4">
220               1024
221             <HDF5 dataset "setpoint": shape (), type "<f8">
222               2.0
223           /bump/config/deflection
224             /bump/config/deflection/channel
225               <HDF5 dataset "channel": shape (), type "<i4">
226                 0
227               <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
228                 [ -1.00000000e+01   3.05180438e-04]
229               <HDF5 dataset "conversion-origin": shape (), type "<f8">
230                 0.0
231               <HDF5 dataset "device": shape (), type "|S12">
232                 /dev/comedi0
233               <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
234                 [    0.    3276.75]
235               <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
236                 -10.0
237               <HDF5 dataset "maxdata": shape (), type "<i8">
238                 65535
239               <HDF5 dataset "name": shape (), type "|S10">
240                 deflection
241               <HDF5 dataset "range": shape (), type "<i4">
242                 0
243               <HDF5 dataset "subdevice": shape (), type "<i4">
244                 0
245           /bump/config/z
246             /bump/config/z/axis
247               /bump/config/z/axis/channel
248                 <HDF5 dataset "channel": shape (), type "<i4">
249                   0
250                 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
251                   [ -1.00000000e+01   3.05180438e-04]
252                 <HDF5 dataset "conversion-origin": shape (), type "<f8">
253                   0.0
254                 <HDF5 dataset "device": shape (), type "|S12">
255                   /dev/comedi0
256                 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
257                   [    0.    3276.75]
258                 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
259                   -10.0
260                 <HDF5 dataset "maxdata": shape (), type "<i8">
261                   65535
262                 <HDF5 dataset "name": shape (), type "|S1">
263                   z
264                 <HDF5 dataset "range": shape (), type "<i4">
265                   0
266                 <HDF5 dataset "subdevice": shape (), type "<i4">
267                   1
268               <HDF5 dataset "gain": shape (), type "<i4">
269                 20
270               <HDF5 dataset "maximum": shape (), type "<f8">
271                 10.0
272               <HDF5 dataset "minimum": shape (), type "<i4">
273                 -9
274               <HDF5 dataset "monitor": shape (), type "|S1">
275     <BLANKLINE>
276               <HDF5 dataset "sensitivity": shape (), type "<f8">
277                 8e-09
278         <HDF5 dataset "processed": shape (), type "<f8">
279           ...
280         /bump/raw
281           <HDF5 dataset "deflection": shape (2048,), type "<u2">
282             [...]
283           <HDF5 dataset "z": shape (2048,), type "<u2">
284             [...]
285
286     Close the Comedi device.
287
288     >>> d.close()
289
290     Cleanup our temporary config file.
291
292     >>> os.remove(filename)
293     """
294     deflection_channel = afm.piezo.input_channel_by_name('deflection')
295     axis = afm.piezo.axis_by_name(afm.axis_name)
296
297     data = bump_acquire(afm, bump_config)
298     photo_sensitivity = _bump_analyze(
299         data, bump_config, z_axis_config=axis.config,
300         deflection_channel_config=deflection_channel.config)
301     _bump_save(
302         filename, group, data, bump_config,
303         z_axis_config=axis.config,
304         deflection_channel_config=deflection_channel.config,
305         processed_bump=photo_sensitivity)
306     return photo_sensitivity