10622f49b8a3df44035e1ff489cb65bdbf3c40a0
[calibcant.git] / calibcant / calibrate.py
1 #!/usr/bin/python
2 #
3 # calibcant - tools for thermally calibrating AFM cantilevers
4 #
5 # Copyright (C) 2007,2008, William Trevor King
6 #
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License as
9 # published by the Free Software Foundation; either version 3 of the
10 # License, or (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15 # See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
20 # 02111-1307, USA.
21 #
22 # The author may be contacted at <wking@drexel.edu> on the Internet, or
23 # write to Trevor King, Drexel University, Physics Dept., 3141 Chestnut St.,
24 # Philadelphia PA 19104, USA.
25
26 """
27 Aquire and analyze cantilever calibration data.
28
29 W. Trevor King Dec. 2007-Jan. 2008
30
31 GPL BOILERPLATE
32
33
34 The relevent physical quantities are :
35  Vzp_out  Output z-piezo voltage (what we generate)
36  Vzp      Applied z-piezo voltage (after external ZPGAIN)
37  Zp       The z-piezo position
38  Zcant    The cantilever vertical deflection
39  Vphoto   The photodiode vertical deflection voltage (what we measure)
40  Fcant    The force on the cantilever
41  T        The temperature of the cantilever and surrounding solution
42           (another thing we measure or guess)
43  k_b      Boltzmann's constant
44
45 Which are related by the parameters :
46  zpGain           Vzp_out / Vzp
47  zpSensitivity    Zp / Vzp
48  photoSensitivity Vphoto / Zcant
49  k_cant           Fcant / Zcant
50
51 Cantilever calibration assumes a pre-calibrated z-piezo
52 (zpSensitivity) and a amplifier (zpGain).  In our lab, the z-piezo is
53 calibrated by imaging a calibration sample, which has features with
54 well defined sizes, and the gain is set with a knob on the Nanoscope.
55
56 photoSensitivity is measured by bumping the cantilever against the
57 surface, where Zp = Zcant (see bump_aquire() and the bump_analyze
58 submodule).
59
60 k_cant is measured by watching the cantilever vibrate in free solution
61 (see the vib_aquire() and the vib_analyze submodule).  The average
62 energy of the cantilever in the vertical direction is given by the
63 equipartition theorem.
64     1/2 k_b T   =   1/2 k_cant <Zcant**2>
65  so     k_cant  = k_b T / Zcant**2
66  but    Zcant   = Vphoto / photoSensitivity
67  so     k_cant  = k_b T * photoSensitivty**2 / <Vphoto**2>
68
69 We measured photoSensitivity with the surface bumps.  We can either
70 measure T using an external function (see temperature.py), or just
71 estimate it (see T_aquire() and the T_analyze submodule).  Guessing
72 room temp ~22 deg C is actually fairly reasonable.  Assuming the
73 actual fluid temperature is within +/- 5 deg, the error in the spring
74 constant k_cant is within 5/273.15 ~= 2%.  A time series of Vphoto
75 while we're far from the surface and not changing Vzp_out will give us
76 the average variance <Vphoto**2>.
77
78 We do all these measurements a few times to estimate statistical
79 errors.
80
81 The functions are layed out in the families:
82  bump_*(), vib_*(), T_*(), and calib_*()
83 where calib_{save|load|analyze}() deal with derived data, not
84 real-world data.
85
86 For each family, * can be any of :
87  aquire       get real-world data
88  save         store real-world data to disk
89  load         get real-world data from disk
90  analyze      interperate the real-world data.
91  plot         show a nice graphic to convince people we're working :p
92  load_analyze_tweaked
93               read a file with a list of paths to previously saved
94               real world data load each file using *_load(), analyze
95               using *_analyze(), and optionally plot using *_plot().
96               Intended for re-processing old data.
97 A family name without any _* extension (e.g. bump()), runs *_aquire(),
98  *_save(), *_analyze(), *_plot().
99
100 We also define the two positioning functions:
101  move_just_onto_surface() and move_far_from_surface()
102 which make automating the calibration procedure more straightforward.
103 """
104
105 import numpy
106 import time 
107 import z_piezo_utils
108 from splittable_kwargs import splittableKwargsFunction, \
109     make_splittable_kwargs_function
110 import FFT_tools
111
112 import common
113 import config
114 import bump_analyze
115 import T_analyze
116 import vib_analyze
117 import analyze
118
119 # bump family
120
121 @splittableKwargsFunction()
122 def bump_aquire(zpiezo, push_depth=200, npoints=1024, freq=100e3) :
123     """
124     Ramps closer push_depth and returns to the original position.
125     Inputs:
126      zpiezo     an opened zpiezo.zpiezo instance
127      push_depth distance to approach, in nm
128      npoints    number points during the approach and during the retreat
129      freq       rate at which data is aquired
130     Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
131     """
132     # generate the bump output
133     start_pos = zpiezo.curPos()
134     pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
135     close_pos = start_pos + pos_dist
136     appr = numpy.linspace(start_pos, close_pos, npoints)
137     retr = numpy.linspace(close_pos, start_pos, npoints)
138     out = numpy.concatenate((appr, retr))
139     # run the bump, and measure deflection
140     if config.TEXT_VERBOSE :
141         print "Bump %g nm" % push_depth
142     data = zpiezo.ramp(out, freq)
143     return data
144
145 @splittableKwargsFunction(bump_aquire,
146                           (bump_analyze.bump_save, 'data'),
147                           (bump_analyze.bump_analyze, 'data'))
148 def bump(**kwargs):
149     """
150     Wrapper around bump_aquire(), bump_save(), bump_analyze()
151     """
152     bump_aquire_kwargs,bump_save_kwargs,bump_analyze_kwargs = \
153         bump._splitargs(bump, kwargs)
154     data = bump_aquire(**bump_aquire_kwargs)
155     bump_analyze.bump_save(data, **bump_save_kwargs)
156     photoSensitivity = bump_analyze.bump_analyze(data, **bump_analyze_kwargs)
157     return photoSensitivity
158
159 # T family.
160 # Fairly stubby, since a one shot Temp measurement is a common thing.
161 # We just wrap that to provide a consistent interface.
162
163 @splittableKwargsFunction()
164 def T_aquire(get_T=None) :
165     """
166     Measure the current temperature of the sample, 
167     or, if get_T == None, fake it by returning config.DEFAULT_TEMP
168     """
169     if get_T == None :
170         if config.TEXT_VERBOSE :
171             print "Fake temperature %g" % config.DEFAULT_TEMP
172         return config.DEFAULT_TEMP
173     else :
174         if config.TEXT_VERBOSE :
175             print "Measure temperature"
176         return get_T()
177
178 @splittableKwargsFunction(T_aquire,
179                           (T_analyze.T_save, 'T'),
180                           (T_analyze.T_analyze, 'T'))
181 def T(**kwargs):
182     """
183     Wrapper around T_aquire(), T_save(), T_analyze(), T_plot()
184     """
185     T_aquire_kwargs,T_save_kwargs,T_analyze_kwargs = \
186         T._splitargs(T, kwargs)
187     T_raw = T_aquire(**T_aquire_kwargs)
188     T_analyze.T_save(T_raw, **T_save_kwargs)
189     T_ret = T_analyze.T_analyze(T_raw, **T_analyze_kwargs) # returns array
190     return T_ret[0]
191
192 # vib family
193
194 @splittableKwargsFunction()
195 def vib_aquire(zpiezo, time=1, freq=50e3) :
196     """
197     Record data for TIME seconds at FREQ Hz from ZPIEZO at it's current position.
198     """
199     # round up to the nearest power of two, for efficient FFT-ing
200     nsamps = FFT_tools.ceil_pow_of_two(time*freq)
201     time = nsamps / freq
202     # take some data, keeping the position voltage at it's current value
203     out = numpy.ones((nsamps,), dtype=numpy.uint16) * zpiezo.curPos()
204     if config.TEXT_VERBOSE :
205         print "get %g seconds of data" % time
206     data = zpiezo.ramp(out, freq)
207     data['sample frequency Hz'] = numpy.array([freq])
208     return data
209
210 @splittableKwargsFunction(vib_aquire,
211                           (vib_analyze.vib_save, 'data'),
212                           (vib_analyze.vib_analyze, 'deflection_bits', 'freq'))
213 def vib(**kwargs) :
214     """
215     Wrapper around vib_aquire(), vib_save(), vib_analyze()
216     """
217     vib_aquire_kwargs,vib_save_kwargs,vib_analyze_kwargs = \
218         vib._splitargs(vib, kwargs)
219     data = vib_aquire(**vib_aquire_kwargs)
220     vib_analyze.vib_save(data, **vib_save_kwargs)
221     freq = data['sample frequency Hz']
222     deflection_bits = data['Deflection input']
223     Vphoto_var = vib_analyze.vib_analyze(deflection_bits=deflection_bits,
224                                          freq=freq, **vib_analyze_kwargs)
225     return Vphoto_var
226
227 # A few positioning functions, so we can run bump_aquire() and vib_aquire()
228 # with proper spacing relative to the surface.
229
230 @splittableKwargsFunction()
231 def move_just_onto_surface(stepper, zpiezo, Depth_nm=100, setpoint=2) :
232     """
233     Uses z_piezo_utils.getSurfPos() to pinpoint the position of the surface.
234     Adjusts the stepper position as required to get within stepper_tol nm
235     of the surface.
236     Then set Vzp to place the cantilever Depth_nm onto 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', 'freq', '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                  bump_freq=100e3,
341                  log_dir=config.LOG_DIR, Vphoto_in2V=config.Vphoto_in2V,
342                  **kwargs):
343     """
344     Aquire data for calibrating a cantilever in one function.
345     return (bump, T, vib), each of which is an array.
346     Inputs :
347      stepper       a stepper.stepper_obj for coarse Z positioning
348      zpiezo        a z_piezo.z_piezo for fine positioning and deflection readin
349      setpoint      maximum allowed deflection (in Volts) during approaches
350      num_bumps     number of 'a's (see Outputs)
351      push_depth_nm depth of each push when generating a
352      num_temps     number of 'T's (see Outputs)
353      num_vibs      number of 'vib's (see Outputs)
354      log_dir       directory to log data to.  Default 'None' disables logging.
355     Outputs (all are arrays of recorded data) :
356      bumps measured (V_photodiode / nm_tip) proportionality constant
357      Ts    measured temperature (K)
358      vibs  measured V_photodiode variance in free solution
359     """
360     move_just_onto_surface_kwargs,bump_kwargs,move_far_from_surface_kwargs, \
361         T_kwargs,vib_kwargs,calib_save_kwargs = \
362         calib_aquire._splitargs(calib_aquire, kwargs)
363     # get bumps
364     move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
365     bumps = numpy.zeros((num_bumps,), dtype=numpy.float)
366     for i in range(num_bumps) :
367         bumps[i] = bump(zpiezo=zpiezo, freq=bump_freq, log_dir=log_dir,
368                         Vphoto_in2V=Vphoto_in2V, **bump_kwargs)
369     if config.TEXT_VERBOSE :
370         print bumps
371
372     move_far_from_surface(stepper, **move_far_from_surface_kwargs)
373
374     # get Ts
375     Ts = numpy.zeros((num_Ts,), dtype=numpy.float)
376     for i in range(num_Ts) :
377         Ts[i] = T(**T_kwargs)
378         time.sleep(1) # wait a bit to get an independent temperature measure
379     print Ts
380
381     # get vibs
382     vibs = numpy.zeros((num_vibs,), dtype=numpy.float)
383     for i in range(num_vibs) :
384         vibs[i] = vib(zpiezo=zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V,
385                       **vib_kwargs)
386     print vibs
387     
388     analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs)
389     
390     return (bumps, Ts, vibs)
391
392
393 @splittableKwargsFunction( \
394     (calib_aquire, 'log_dir'),
395     (analyze.calib_analyze, 'bumps','Ts','vibs'))
396 def calib(log_dir=config.LOG_DIR, **kwargs) :
397     """
398     Calibrate a cantilever in one function.
399     The I-don't-care-about-the-details black box version :p.
400     return (k, k_s)
401     Inputs:
402      (see calib_aquire()) 
403     Outputs :
404      k    cantilever spring constant (in N/m, or equivalently nN/nm)
405      k_s  standard deviation in our estimate of k
406     Notes :
407      See get_calibration_data() for the data aquisition code
408      See analyze_calibration_data() for the analysis code
409     """
410     calib_aquire_kwargs,calib_analyze_kwargs = \
411         calib._splitargs(calib, kwargs)
412     a, T, vib = calib_aquire(**calib_aquire_kwargs)
413     k,k_s,ps2_m, ps2_s,T_m,T_s,one_o_Vp2_m,one_o_Vp2_s = \
414         analyze.calib_analyze(a, T, vib, **calib_analyze_kwargs)
415     analyze.calib_save_analysis(k, k_s, ps2_m, ps2_s, T_m, T_s,
416                                 one_o_Vp2_m, one_o_Vp2_s, log_dir)
417     return (k, k_s)
418
419     
420