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