54279a5652e0238e115c058f5ebc9c90c409c655
[calibcant.git] / calibcant / calibrate.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 """
22 Aquire and analyze cantilever calibration data.
23
24 W. Trevor King Dec. 2007-Jan. 2008
25
26 GPL BOILERPLATE
27
28
29 The relevent physical quantities are :
30  Vzp_out  Output z-piezo voltage (what we generate)
31  Vzp      Applied z-piezo voltage (after external ZPGAIN)
32  Zp       The z-piezo position
33  Zcant    The cantilever vertical deflection
34  Vphoto   The photodiode vertical deflection voltage (what we measure)
35  Fcant    The force on the cantilever
36  T        The temperature of the cantilever and surrounding solution
37           (another thing we measure or guess)
38  k_b      Boltzmann's constant
39
40 Which are related by the parameters :
41  zpGain           Vzp_out / Vzp
42  zpSensitivity    Zp / Vzp
43  photoSensitivity Vphoto / Zcant
44  k_cant           Fcant / Zcant
45
46 Cantilever calibration assumes a pre-calibrated z-piezo
47 (zpSensitivity) and a amplifier (zpGain).  In our lab, the z-piezo is
48 calibrated by imaging a calibration sample, which has features with
49 well defined sizes, and the gain is set with a knob on the Nanoscope.
50
51 photoSensitivity is measured by bumping the cantilever against the
52 surface, where Zp = Zcant (see bump_aquire() and the bump_analyze
53 submodule).
54
55 k_cant is measured by watching the cantilever vibrate in free solution
56 (see the vib_aquire() and the vib_analyze submodule).  The average
57 energy of the cantilever in the vertical direction is given by the
58 equipartition theorem.
59     1/2 k_b T   =   1/2 k_cant <Zcant**2>
60  so     k_cant  = k_b T / Zcant**2
61  but    Zcant   = Vphoto / photoSensitivity
62  so     k_cant  = k_b T * photoSensitivty**2 / <Vphoto**2>
63
64 We measured photoSensitivity with the surface bumps.  We can either
65 measure T using an external function (see temperature.py), or just
66 estimate it (see T_aquire() and the T_analyze submodule).  Guessing
67 room temp ~22 deg C is actually fairly reasonable.  Assuming the
68 actual fluid temperature is within +/- 5 deg, the error in the spring
69 constant k_cant is within 5/273.15 ~= 2%.  A time series of Vphoto
70 while we're far from the surface and not changing Vzp_out will give us
71 the average variance <Vphoto**2>.
72
73 We do all these measurements a few times to estimate statistical
74 errors.
75
76 The functions are layed out in the families:
77  bump_*(), vib_*(), T_*(), and calib_*()
78 where calib_{save|load|analyze}() deal with derived data, not
79 real-world data.
80
81 For each family, * can be any of :
82  aquire       get real-world data
83  save         store real-world data to disk
84  load         get real-world data from disk
85  analyze      interperate the real-world data.
86  plot         show a nice graphic to convince people we're working :p
87  load_analyze_tweaked
88               read a file with a list of paths to previously saved
89               real world data load each file using *_load(), analyze
90               using *_analyze(), and optionally plot using *_plot().
91               Intended for re-processing old data.
92 A family name without any _* extension (e.g. bump()), runs *_aquire(),
93  *_save(), *_analyze(), *_plot().
94
95 We also define the two positioning functions:
96  move_just_onto_surface() and move_far_from_surface()
97 which make automating the calibration procedure more straightforward.
98 """
99
100 import numpy
101 import time 
102
103 import FFT_tools
104 import piezo.z_piezo_utils as z_piezo_utils
105 from splittable_kwargs import splittableKwargsFunction, \
106     make_splittable_kwargs_function
107
108 from . import common
109 from . import config
110 from . import bump_analyze
111 from . import T_analyze
112 from . import vib_analyze
113 from . import analyze
114
115
116 # bump family
117
118 @splittableKwargsFunction()
119 def bump_aquire(zpiezo, push_depth=200, npoints=1024, push_speed=1000) :
120     """
121     Ramps closer push_depth and returns to the original position.
122     Inputs:
123      zpiezo     an opened zpiezo.zpiezo instance
124      push_depth distance to approach, in nm
125      npoints    number points during the approach and during the retreat
126      push_speed piezo speed during approach and retreat, in nm/s
127     Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
128     """
129     # generate the bump output
130     nm_per_step = float(push_depth) / npoints
131     freq = push_speed / nm_per_step # freq is sample frequency in Hz
132     start_pos = zpiezo.curPos()
133     pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
134     close_pos = start_pos + pos_dist
135     appr = numpy.linspace(start_pos, close_pos, npoints)
136     retr = numpy.linspace(close_pos, start_pos, npoints)
137     out = numpy.concatenate((appr, retr))
138     # run the bump, and measure deflection
139     if config.TEXT_VERBOSE :
140         print "Bump %g nm at %g nm/s" % (push_depth, push_speed)
141     data = zpiezo.ramp(out, freq)
142     return data
143
144 @splittableKwargsFunction(bump_aquire,
145                           (bump_analyze.bump_save, 'data'),
146                           (bump_analyze.bump_analyze, 'data'))
147 def bump(**kwargs):
148     """
149     Wrapper around bump_aquire(), bump_save(), bump_analyze()
150     """
151     bump_aquire_kwargs,bump_save_kwargs,bump_analyze_kwargs = \
152         bump._splitargs(bump, kwargs)
153     data = bump_aquire(**bump_aquire_kwargs)
154     bump_analyze.bump_save(data, **bump_save_kwargs)
155     photoSensitivity = bump_analyze.bump_analyze(data, **bump_analyze_kwargs)
156     return photoSensitivity
157
158 # T family.
159 # Fairly stubby, since a one shot Temp measurement is a common thing.
160 # We just wrap that to provide a consistent interface.
161
162 @splittableKwargsFunction()
163 def T_aquire(get_T=None) :
164     """
165     Measure the current temperature of the sample, 
166     or, if get_T == None, fake it by returning config.DEFAULT_TEMP
167     """
168     if get_T == None :
169         if config.TEXT_VERBOSE :
170             print "Fake temperature %g" % config.DEFAULT_TEMP
171         return config.DEFAULT_TEMP
172     else :
173         if config.TEXT_VERBOSE :
174             print "Measure temperature"
175         return get_T()
176
177 @splittableKwargsFunction(T_aquire,
178                           (T_analyze.T_save, 'T'),
179                           (T_analyze.T_analyze, 'T'))
180 def T(**kwargs):
181     """
182     Wrapper around T_aquire(), T_save(), T_analyze(), T_plot()
183     """
184     T_aquire_kwargs,T_save_kwargs,T_analyze_kwargs = \
185         T._splitargs(T, kwargs)
186     T_raw = T_aquire(**T_aquire_kwargs)
187     T_analyze.T_save(T_raw, **T_save_kwargs)
188     T_ret = T_analyze.T_analyze(T_raw, **T_analyze_kwargs) # returns array
189     return T_ret[0]
190
191 # vib family
192
193 @splittableKwargsFunction()
194 def vib_aquire(zpiezo, time=1, freq=50e3) :
195     """
196     Record data for TIME seconds at FREQ Hz from ZPIEZO at it's current position.
197     """
198     # round up to the nearest power of two, for efficient FFT-ing
199     nsamps = FFT_tools.ceil_pow_of_two(time*freq)
200     time = nsamps / freq
201     # take some data, keeping the position voltage at it's current value
202     out = numpy.ones((nsamps,), dtype=numpy.uint16) * zpiezo.curPos()
203     if config.TEXT_VERBOSE :
204         print "get %g seconds of data" % time
205     data = zpiezo.ramp(out, freq)
206     data['sample frequency Hz'] = numpy.array([freq])
207     return data
208
209 @splittableKwargsFunction(vib_aquire,
210                           (vib_analyze.vib_save, 'data'),
211                           (vib_analyze.vib_analyze, 'deflection_bits', 'freq'))
212 def vib(**kwargs) :
213     """
214     Wrapper around vib_aquire(), vib_save(), vib_analyze()
215     """
216     vib_aquire_kwargs,vib_save_kwargs,vib_analyze_kwargs = \
217         vib._splitargs(vib, kwargs)
218     data = vib_aquire(**vib_aquire_kwargs)
219     vib_analyze.vib_save(data, **vib_save_kwargs)
220     freq = data['sample frequency Hz']
221     deflection_bits = data['Deflection input']
222     Vphoto_var = vib_analyze.vib_analyze(deflection_bits=deflection_bits,
223                                          freq=freq, **vib_analyze_kwargs)
224     return Vphoto_var
225
226 # A few positioning functions, so we can run bump_aquire() and vib_aquire()
227 # with proper spacing relative to the surface.
228
229 @splittableKwargsFunction()
230 def move_just_onto_surface(stepper, zpiezo, Depth_nm=-50, setpoint=2) :
231     """
232     Uses z_piezo_utils.getSurfPos() to pinpoint the position of the
233     surface.  Adjusts the stepper position as required to get within
234     stepper_tol nm of the surface.  Then set Vzp to place the
235     cantilever Depth_nm onto the surface.  Negative Depth_nm values
236     will place the cantilever that many nm _off_ the surface.
237     
238     If getSurfPos() fails to find the surface, backs off (for safety)
239     and steps in (without moving the zpiezo) until Vphoto > setpoint.
240     """
241     stepper_tol = 250 # nm, generous estimate of the fullstep stepsize
242
243     if config.TEXT_VERBOSE :
244         print "moving just onto surface"
245     # Zero the piezo
246     if config.TEXT_VERBOSE :
247         print "zero the z piezo output"
248     zpiezo.jumpToPos(zpiezo.pos_nm2out(0))
249     # See if we're near the surface already
250     if config.TEXT_VERBOSE :
251         print "See if we're starting near the surface"
252     try :
253         dist = zpiezo.pos_out2nm( \
254             z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))
255                                 )
256     except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
257         if config.TEXT_VERBOSE :
258             print "distance failed with: ", string
259             print "Back off 200 half steps"
260         # Back away 200 steps
261         stepper.step_rel(-400)
262         stepper.step_rel(200)
263         sp = zpiezo.def_V2in(setpoint) # sp = setpoint in bits
264         zpiezo.updateInputs()
265         cd = zpiezo.curDef()           # cd = current deflection in bits
266         if config.TEXT_VERBOSE :
267             print "Single stepping approach"
268         while cd < sp :
269             if config.TEXT_VERBOSE :
270                 print "deflection %g < setpoint %g.  step closer" % (cd, sp)
271             stepper.step_rel(2) # Full step in
272             zpiezo.updateInputs()
273             cd = zpiezo.curDef()
274         # Back off two steps (protecting against backlash)
275         if config.TEXT_VERBOSE :
276             print "Step back 4 half steps to get off the setpoint"
277         stepper.step_rel(-200)
278         stepper.step_rel(196)
279         # get the distance to the surface
280         zpiezo.updateInputs()
281         if config.TEXT_VERBOSE :
282             print "get surf pos, with setpoint %g (%d)" % (setpoint, zpiezo.def_V2in(setpoint))
283         for i in range(20) : # HACK, keep stepping back until we get a distance
284             try :
285                 dist = zpiezo.pos_out2nm( \
286                     z_piezo_utils.getSurfPos(zpiezo,zpiezo.def_V2in(setpoint)))
287             except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
288                 stepper.step_rel(-200)
289                 stepper.step_rel(198)
290                 continue
291             break
292         if i >= 19 :
293             print "tried %d times, still too close! bailing" % i
294             print "probably an invalid setpoint."
295             raise Exception, "weirdness"
296     if config.TEXT_VERBOSE :
297         print 'distance to surface ', dist, ' nm'
298     # fine tune the stepper position
299     while dist < -stepper_tol : # step back if we need to
300         stepper.step_rel(-200)
301         stepper.step_rel(198)
302         dist = zpiezo.pos_out2nm( \
303             z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
304         if config.TEXT_VERBOSE :
305             print 'distance to surface ', dist, ' nm, step back'
306     while dist > stepper_tol : # and step forward if we need to
307         stepper.step_rel(2)
308         dist = zpiezo.pos_out2nm( \
309             z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
310         if config.TEXT_VERBOSE :
311             print 'distance to surface ', dist, ' nm, step closer'
312     # now adjust the zpiezo to place us just onto the surface
313     target = dist + Depth_nm
314     zpiezo.jumpToPos(zpiezo.pos_nm2out(target))
315     # and we're there :)
316     if config.TEXT_VERBOSE :
317         print "We're %g nm into the surface" % Depth_nm
318
319 @splittableKwargsFunction()
320 def move_far_from_surface(stepper, um_back=50) :
321     """
322     Step back a specified number of microns.
323     (uses very rough estimate of step distance at the moment)
324     """
325     step_nm = 100
326     steps = int(um_back*1000/step_nm)
327     print "step back %d steps" % steps
328     stepper.step_rel(-steps)
329
330
331 # and finally, the calib family
332
333 @splittableKwargsFunction((move_just_onto_surface, 'stepper', 'zpiezo'),
334                           (bump, 'zpiezo', 'log_dir', 'Vphoto_in2V'),
335                           (move_far_from_surface, 'stepper'),
336                           (T, 'log_dir'),
337                           (vib, 'zpiezo', 'log_dir', 'Vphoto_in2V'),
338                           (analyze.calib_save, 'bumps','Ts','vibs','log_dir'))
339 def calib_aquire(stepper, zpiezo, num_bumps=10, num_Ts=10, num_vibs=20,
340                  log_dir=config.LOG_DIR, Vphoto_in2V=config.Vphoto_in2V,
341                  **kwargs):
342     """
343     Aquire data for calibrating a cantilever in one function.
344     return (bump, T, vib), each of which is an array.
345     Inputs :
346      stepper       a stepper.stepper_obj for coarse Z positioning
347      zpiezo        a z_piezo.z_piezo for fine positioning and deflection readin
348      num_bumps     number of 'bumps' (see Outputs)
349      num_temps     number of 'Ts' (see Outputs)
350      num_vibs      number of 'vib's (see Outputs)
351      log_dir       directory to log data to.  Default 'None' disables logging.
352      Vphoto_in2V   function to convert photodiode input bits to Volts
353
354      + other kwargs.  Run calib_aquire._kwargs(calib_aquire) to see
355      all options.  Run calib_aquire._childSplittables to see a list
356      of kwarg functions that this function calls.
357
358     Outputs (all are arrays of recorded data) :
359      bumps measured (V_photodiode / nm_tip) proportionality constant
360      Ts    measured temperature (K)
361      vibs  measured V_photodiode variance in free solution
362     """
363     move_just_onto_surface_kwargs,bump_kwargs,move_far_from_surface_kwargs, \
364         T_kwargs,vib_kwargs,calib_save_kwargs = \
365         calib_aquire._splitargs(calib_aquire, kwargs)
366     # get bumps
367     bumps = numpy.zeros((num_bumps,), dtype=numpy.float)
368     for i in range(num_bumps) :
369         move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
370         bumps[i] = bump(zpiezo=zpiezo, log_dir=log_dir,
371                         Vphoto_in2V=Vphoto_in2V, **bump_kwargs)
372     if config.TEXT_VERBOSE :
373         print bumps
374
375     move_far_from_surface(stepper, **move_far_from_surface_kwargs)
376
377     # get Ts
378     Ts = numpy.zeros((num_Ts,), dtype=numpy.float)
379     for i in range(num_Ts) :
380         Ts[i] = T(**T_kwargs)
381         time.sleep(1) # wait a bit to get an independent temperature measure
382     print Ts
383
384     # get vibs
385     vibs = numpy.zeros((num_vibs,), dtype=numpy.float)
386     for i in range(num_vibs) :
387         vibs[i] = vib(zpiezo=zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V,
388                       **vib_kwargs)
389     print vibs
390     
391     analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs)
392     
393     return (bumps, Ts, vibs)
394
395
396 @splittableKwargsFunction( \
397     (calib_aquire, 'log_dir'),
398     (analyze.calib_analyze, 'bumps','Ts','vibs'))
399 def calib(log_dir=config.LOG_DIR, **kwargs) :
400     """
401     Calibrate a cantilever in one function.
402     The I-don't-care-about-the-details black box version :p.
403     return (k, k_s)
404     Inputs:
405      (see calib_aquire()) 
406     Outputs :
407      k    cantilever spring constant (in N/m, or equivalently nN/nm)
408      k_s  standard deviation in our estimate of k
409     Notes :
410      See get_calibration_data() for the data aquisition code
411      See analyze_calibration_data() for the analysis code
412     """
413     calib_aquire_kwargs,calib_analyze_kwargs = \
414         calib._splitargs(calib, kwargs)
415     a, T, vib = calib_aquire(**calib_aquire_kwargs)
416     k,k_s,ps2_m, ps2_s,T_m,T_s,one_o_Vp2_m,one_o_Vp2_s = \
417         analyze.calib_analyze(a, T, vib, **calib_analyze_kwargs)
418     analyze.calib_save_analysis(k, k_s, ps2_m, ps2_s, T_m, T_s,
419                                 one_o_Vp2_m, one_o_Vp2_s, log_dir)
420     return (k, k_s)
421
422     
423