New Marisa/me calibration difference bug 327f4db8-3119-4ec1-a762-a3115254608a
[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 import FFT_tools
88
89 import common
90 import config
91 import bump_analyze
92 import T_analyze
93 import vib_analyze
94 import analyze
95
96 # bump family
97
98 @splittableKwargsFunction()
99 def bump_aquire(zpiezo, push_depth=200, npoints=1024, freq=100e3) :
100     """
101     Ramps closer push_depth and returns to the original position.
102     Inputs:
103      zpiezo     an opened zpiezo.zpiezo instance
104      push_depth distance to approach, in nm
105      npoints    number points during the approach and during the retreat
106      freq       rate at which data is aquired
107     Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
108     """
109     # generate the bump output
110     start_pos = zpiezo.curPos()
111     pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
112     close_pos = start_pos + pos_dist
113     appr = numpy.linspace(start_pos, close_pos, npoints)
114     retr = numpy.linspace(close_pos, start_pos, npoints)
115     out = numpy.concatenate((appr, retr))
116     # run the bump, and measure deflection
117     if config.TEXT_VERBOSE :
118         print "Bump %g nm" % push_depth
119     data = zpiezo.ramp(out, freq)
120     return data
121
122 @splittableKwargsFunction(bump_aquire,
123                           (bump_analyze.bump_save, 'data'),
124                           (bump_analyze.bump_analyze, 'data'))
125 def bump(**kwargs):
126     """
127     Wrapper around bump_aquire(), bump_save(), bump_analyze()
128     """
129     bump_aquire_kwargs,bump_save_kwargs,bump_analyze_kwargs = \
130         bump._splitargs(bump, kwargs)
131     data = bump_aquire(**bump_aquire_kwargs)
132     bump_analyze.bump_save(data, **bump_save_kwargs)
133     photoSensitivity = bump_analyze.bump_analyze(data, **bump_analyze_kwargs)
134     return photoSensitivity
135
136 # T family.
137 # Fairly stubby, since a one shot Temp measurement is a common thing.
138 # We just wrap that to provide a consistent interface.
139
140 @splittableKwargsFunction()
141 def T_aquire(get_T=None) :
142     """
143     Measure the current temperature of the sample, 
144     or, if get_T == None, fake it by returning config.DEFAULT_TEMP
145     """
146     if get_T == None :
147         if config.TEXT_VERBOSE :
148             print "Fake temperature %g" % config.DEFAULT_TEMP
149         return config.DEFAULT_TEMP
150     else :
151         if config.TEXT_VERBOSE :
152             print "Measure temperature"
153         return get_T()
154
155 @splittableKwargsFunction(T_aquire,
156                           (T_analyze.T_save, 'T'),
157                           (T_analyze.T_analyze, 'T'))
158 def T(**kwargs):
159     """
160     Wrapper around T_aquire(), T_save(), T_analyze(), T_plot()
161     """
162     T_aquire_kwargs,T_save_kwargs,T_analyze_kwargs = \
163         T._splitargs(T, kwargs)
164     T_raw = T_aquire(**T_aquire_kwargs)
165     T_analyze.T_save(T_raw, **T_save_kwargs)
166     T_ret = T_analyze.T_analyze(T_raw, **T_analyze_kwargs) # returns array
167     return T_ret[0]
168
169 # vib family
170
171 @splittableKwargsFunction()
172 def vib_aquire(zpiezo, time=1, freq=50e3) :
173     """
174     Record data for TIME seconds at FREQ Hz from ZPIEZO at it's current position.
175     """
176     # round up to the nearest power of two, for efficient FFT-ing
177     nsamps = FFT_tools.ceil_pow_of_two(time*freq)
178     time = nsamps / freq
179     # take some data, keeping the position voltage at it's current value
180     out = numpy.ones((nsamps,), dtype=numpy.uint16) * zpiezo.curPos()
181     if config.TEXT_VERBOSE :
182         print "get %g seconds of data" % time
183     data = zpiezo.ramp(out, freq)
184     data['sample frequency Hz'] = numpy.array([freq])
185     return data
186
187 @splittableKwargsFunction(vib_aquire,
188                           (vib_analyze.vib_save, 'data'),
189                           (vib_analyze.vib_analyze, 'deflection_bits', 'freq'))
190 def vib(**kwargs) :
191     """
192     Wrapper around vib_aquire(), vib_save(), vib_analyze()
193     """
194     vib_aquire_kwargs,vib_save_kwargs,vib_analyze_kwargs = \
195         vib._splitargs(vib, kwargs)
196     data = vib_aquire(**vib_aquire_kwargs)
197     vib_analyze.vib_save(data, **vib_save_kwargs)
198     freq = data['sample frequency Hz']
199     deflection_bits = data['Deflection input']
200     Vphoto_var = vib_analyze.vib_analyze(deflection_bits=deflection_bits,
201                                          freq=freq, **vib_analyze_kwargs)
202     return Vphoto_var
203
204 # A few positioning functions, so we can run bump_aquire() and vib_aquire()
205 # with proper spacing relative to the surface.
206
207 @splittableKwargsFunction()
208 def move_just_onto_surface(stepper, zpiezo, Depth_nm=100, setpoint=2) :
209     """
210     Uses z_piezo_utils.getSurfPos() to pinpoint the position of the surface.
211     Adjusts the stepper position as required to get within stepper_tol nm
212     of the surface.
213     Then set Vzp to place the cantilever Depth_nm onto the surface.
214
215     If getSurfPos() fails to find the surface, backs off (for safety)
216     and steps in (without moving the zpiezo) until Vphoto > setpoint.
217     """
218     stepper_tol = 250 # nm, generous estimate of the fullstep stepsize
219
220     if config.TEXT_VERBOSE :
221         print "moving just onto surface"
222     # Zero the piezo
223     if config.TEXT_VERBOSE :
224         print "zero the z piezo output"
225     zpiezo.jumpToPos(zpiezo.pos_nm2out(0))
226     # See if we're near the surface already
227     if config.TEXT_VERBOSE :
228         print "See if we're starting near the surface"
229     try :
230         dist = zpiezo.pos_out2nm( \
231             z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint))
232                                 )
233     except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
234         if config.TEXT_VERBOSE :
235             print "distance failed with: ", string
236             print "Back off 200 half steps"
237         # Back away 200 steps
238         stepper.step_rel(-400)
239         stepper.step_rel(200)
240         sp = zpiezo.def_V2in(setpoint) # sp = setpoint in bits
241         zpiezo.updateInputs()
242         cd = zpiezo.curDef()           # cd = current deflection in bits
243         if config.TEXT_VERBOSE :
244             print "Single stepping approach"
245         while cd < sp :
246             if config.TEXT_VERBOSE :
247                 print "deflection %g < setpoint %g.  step closer" % (cd, sp)
248             stepper.step_rel(2) # Full step in
249             zpiezo.updateInputs()
250             cd = zpiezo.curDef()
251         # Back off two steps (protecting against backlash)
252         if config.TEXT_VERBOSE :
253             print "Step back 4 half steps to get off the setpoint"
254         stepper.step_rel(-200)
255         stepper.step_rel(196)
256         # get the distance to the surface
257         zpiezo.updateInputs()
258         if config.TEXT_VERBOSE :
259             print "get surf pos, with setpoint %g (%d)" % (setpoint, zpiezo.def_V2in(setpoint))
260         for i in range(20) : # HACK, keep stepping back until we get a distance
261             try :
262                 dist = zpiezo.pos_out2nm( \
263                     z_piezo_utils.getSurfPos(zpiezo,zpiezo.def_V2in(setpoint)))
264             except (z_piezo_utils.tooClose, z_piezo_utils.poorFit), string :
265                 stepper.step_rel(-200)
266                 stepper.step_rel(198)
267                 continue
268             break
269         if i >= 19 :
270             print "tried %d times, still too close! bailing" % i
271             print "probably an invalid setpoint."
272             raise Exception, "weirdness"
273     if config.TEXT_VERBOSE :
274         print 'distance to surface ', dist, ' nm'
275     # fine tune the stepper position
276     while dist < -stepper_tol : # step back if we need to
277         stepper.step_rel(-200)
278         stepper.step_rel(198)
279         dist = zpiezo.pos_out2nm( \
280             z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
281         if config.TEXT_VERBOSE :
282             print 'distance to surface ', dist, ' nm, step back'
283     while dist > stepper_tol : # and step forward if we need to
284         stepper.step_rel(2)
285         dist = zpiezo.pos_out2nm( \
286             z_piezo_utils.getSurfPos(zpiezo, zpiezo.def_V2in(setpoint)))
287         if config.TEXT_VERBOSE :
288             print 'distance to surface ', dist, ' nm, step closer'
289     # now adjust the zpiezo to place us just onto the surface
290     target = dist + Depth_nm
291     zpiezo.jumpToPos(zpiezo.pos_nm2out(target))
292     # and we're there :)
293     if config.TEXT_VERBOSE :
294         print "We're %g nm into the surface" % Depth_nm
295
296 @splittableKwargsFunction()
297 def move_far_from_surface(stepper, um_back=50) :
298     """
299     Step back a specified number of microns.
300     (uses very rough estimate of step distance at the moment)
301     """
302     step_nm = 100
303     steps = int(um_back*1000/step_nm)
304     print "step back %d steps" % steps
305     stepper.step_rel(-steps)
306
307
308 # and finally, the calib family
309
310 @splittableKwargsFunction((move_just_onto_surface, 'stepper', 'zpiezo'),
311                           (bump, 'zpiezo', 'freq', 'log_dir', 'Vphoto_in2V'),
312                           (move_far_from_surface, 'stepper'),
313                           (T, 'log_dir'),
314                           (vib, 'zpiezo', 'log_dir', 'Vphoto_in2V'),
315                           (analyze.calib_save, 'bumps','Ts','vibs','log_dir'))
316 def calib_aquire(stepper, zpiezo, num_bumps=10, num_Ts=10, num_vibs=20,
317                  bump_freq=100e3,
318                  log_dir=config.LOG_DIR, Vphoto_in2V=config.Vphoto_in2V,
319                  **kwargs):
320     """
321     Aquire data for calibrating a cantilever in one function.
322     return (bump, T, vib), each of which is an array.
323     Inputs :
324      stepper       a stepper.stepper_obj for coarse Z positioning
325      zpiezo        a z_piezo.z_piezo for fine positioning and deflection readin
326      setpoint      maximum allowed deflection (in Volts) during approaches
327      num_bumps     number of 'a's (see Outputs)
328      push_depth_nm depth of each push when generating a
329      num_temps     number of 'T's (see Outputs)
330      num_vibs      number of 'vib's (see Outputs)
331      log_dir       directory to log data to.  Default 'None' disables logging.
332     Outputs (all are arrays of recorded data) :
333      bumps measured (V_photodiode / nm_tip) proportionality constant
334      Ts    measured temperature (K)
335      vibs  measured V_photodiode variance in free solution
336     """
337     move_just_onto_surface_kwargs,bump_kwargs,move_far_from_surface_kwargs, \
338         T_kwargs,vib_kwargs,calib_save_kwargs = \
339         calib_aquire._splitargs(calib_aquire, kwargs)
340     # get bumps
341     move_just_onto_surface(stepper, zpiezo, **move_just_onto_surface_kwargs)
342     bumps = numpy.zeros((num_bumps,), dtype=numpy.float)
343     for i in range(num_bumps) :
344         bumps[i] = bump(zpiezo=zpiezo, freq=bump_freq, log_dir=log_dir,
345                         Vphoto_in2V=Vphoto_in2V, **bump_kwargs)
346     if config.TEXT_VERBOSE :
347         print bumps
348
349     move_far_from_surface(stepper, **move_far_from_surface_kwargs)
350
351     # get Ts
352     Ts = numpy.zeros((num_Ts,), dtype=numpy.float)
353     for i in range(num_Ts) :
354         Ts[i] = T(**T_kwargs)
355         time.sleep(1) # wait a bit to get an independent temperature measure
356     print Ts
357
358     # get vibs
359     vibs = numpy.zeros((num_vibs,), dtype=numpy.float)
360     for i in range(num_vibs) :
361         vibs[i] = vib(zpiezo=zpiezo, log_dir=log_dir, Vphoto_in2V=Vphoto_in2V,
362                       **vib_kwargs)
363     print vibs
364     
365     analyze.calib_save(bumps, Ts, vibs, log_dir, **calib_save_kwargs)
366     
367     return (bumps, Ts, vibs)
368
369
370 @splittableKwargsFunction( \
371     (calib_aquire, 'log_dir'),
372     (analyze.calib_analyze, 'bumps','Ts','vibs'))
373 def calib(log_dir=config.LOG_DIR, **kwargs) :
374     """
375     Calibrate a cantilever in one function.
376     The I-don't-care-about-the-details black box version :p.
377     return (k, k_s)
378     Inputs:
379      (see calib_aquire()) 
380     Outputs :
381      k    cantilever spring constant (in N/m, or equivalently nN/nm)
382      k_s  standard deviation in our estimate of k
383     Notes :
384      See get_calibration_data() for the data aquisition code
385      See analyze_calibration_data() for the analysis code
386     """
387     calib_aquire_kwargs,calib_analyze_kwargs = \
388         calib._splitargs(calib, kwargs)
389     a, T, vib = calib_aquire(**calib_aquire_kwargs)
390     k,k_s,ps2_m, ps2_s,T_m,T_s,one_o_Vp2_m,one_o_Vp2_s = \
391         analyze.calib_analyze(a, T, vib, **calib_analyze_kwargs)
392     analyze.calib_save_analysis(k, k_s, ps2_m, ps2_s, T_m, T_s,
393                                 one_o_Vp2_m, one_o_Vp2_s, log_dir)
394     return (k, k_s)
395
396     
397