Added update_copyright.py to automate copyright blurb maintenance
[calibcant.git] / calibcant / bump_analyze.py
index 748287967cf8d38d29801e353dd217f162bef608..8291b0eed667cdb9f9aa61e1f229279715f6a974 100755 (executable)
@@ -47,22 +47,46 @@ slope Vphoto/Vout is converted to photoSensitivity with bump_analyze().
 
 import numpy
 import scipy.optimize
-import common # common module for the calibcant package
-import config # config module for the calibcant package
+
 import data_logger
 from splittable_kwargs import splittableKwargsFunction, \
     make_splittable_kwargs_function
 
+import .common
+import .config
+
 
+@splittableKwargsFunction()
+def Vzp_bits2nm(data_bits, zpGain=config.zpGain,
+                zpSensitivity=config.zpSensitivity,
+                Vzp_out2V=config.Vzp_out2V):
+    scale_Vzp_bits2V = Vzp_out2V(1) - Vzp_out2V(0)
+    data_V = data_bits / scale_Vzp_bits2V
+    #             bits / (bits/V) = V
+    data_nm = data_V * zpGain * zpSensitivity
+    return data_nm
+
+@splittableKwargsFunction()
+def Vphoto_bits2V(data_bits, Vphoto_in2V=config.Vphoto_in2V):
+    scale_Vphoto_bits2V = Vphoto_in2V(1) - Vphoto_in2V(0)
+    Vphoto_V = data_bits / scale_Vphoto_bits2V
+    #               bits / (bits/V) = V
+    return Vphoto_V
+
+@splittableKwargsFunction((Vzp_bits2nm, 'data_bits'),
+                          (Vphoto_bits2V, 'data_bits'))
+def slope_bitspbit2Vpnm(slope_bitspbit, **kwargs):
+    zp_kwargs,photo_kwargs = slope_bitspbit2Vpnm._splitargs(slope_bitspbit2Vpnm, kwargs)
+    Vzp_bits = 1.0
+    Vphoto_bits = slope_bitspbit * Vzp_bits
+    return Vphoto_bits2V(Vphoto_bits, **photo_kwargs)/Vzp_bits2nm(Vzp_bits, **zp_kwargs)
+    
 #@splittableKwargsFunction((bump_fit, 'zpiezo_output_bits',
-#                           'deflection_input_bits'))
+#                           'deflection_input_bits'),
+#                          (slope_bitspbit2Vpnm, 'slope_bitspbit'))
 # Some of the child functions aren't yet defined, so postpone
 # make-splittable until later in the module.
-def bump_analyze(data, zpGain=config.zpGain,
-                 zpSensitivity=config.zpSensitivity,
-                 Vzp_out2V=config.Vzp_out2V,
-                 Vphoto_in2V=config.Vphoto_in2V,
-                 **kwargs) :
+def bump_analyze(data, **kwargs) :
     """
     Return the slope of the bump ;).
     Inputs:
@@ -77,40 +101,35 @@ def bump_analyze(data, zpGain=config.zpGain,
     
     With the current implementation, the data is regressed in DAC/ADC bits
     and THEN converted, so we're assuming that both conversions are LINEAR.
-    if they aren't, rewrite to convert before the regression.
+    If they aren't, rewrite to convert before the regression.
     """
-    bump_fit_kwargs, = bump_analyze._splitargs(bump_analyze, kwargs)
+    bump_fit_kwargs,slope_bitspbit2Vpnm_kwargs = \
+        bump_analyze._splitargs(bump_analyze, kwargs)
     Vphoto2Vzp_out_bit = bump_fit(data['Z piezo output'],
                                   data['Deflection input'],
                                   **bump_fit_kwargs)
-    scale_Vzp_bits2V = Vzp_out2V(1) - Vzp_out2V(0)
-    scale_Vphoto_bits2V = Vphoto_in2V(1) - Vphoto_in2V(0)
-    Vphoto2Vzp_out = Vphoto2Vzp_out_bit *scale_Vphoto_bits2V / scale_Vzp_bits2V
-    #               1 / (Vzp/Vzp_out  *     Zp/Vzp       * Zcant/Zp )
-    Vzp_out2Zcant = 1.0/ (zpGain      * zpSensitivity) # * 1
-    return Vphoto2Vzp_out * Vzp_out2Zcant
+    return slope_bitspbit2Vpnm(Vphoto2Vzp_out_bit, **slope_bitspbit2Vpnm_kwargs)
 
-def limited_quadratic(x, params):
+def limited_linear(x, params):
     """
     Model the bump as:
       flat region (off-surface)
-      quadratic region (in-contact)
+      linear region (in-contact)
       flat region (high-voltage-rail)
     Parameters:
       x_contact (x value for the surface-contact kink)
       y_contact (y value for the surface-contact kink)
       slope (dy/dx at the surface-contact kink)
-      quad (d**2 y / dx**2, allow decreasing sensitivity with increased x)
     """
     high_voltage_rail = 2**16 - 1 # bits
-    x_contact,y_contact,slope,quad = params
-    y = slope*(x-x_contact) + quad*(x-x_contact)**2+ y_contact
+    x_contact,y_contact,slope = params
+    y = slope*(x-x_contact) + y_contact
     y = numpy.clip(y, y_contact, high_voltage_rail)
     return y
 
-def limited_quadratic_param_guess(x, y) :
+def limited_linear_param_guess(x, y) :
     """
-    Guess rough parameters for a limited_quadratic model.  Assumes the
+    Guess rough parameters for a limited_linear model.  Assumes the
     bump approaches (raising the deflection as it does so) first.
     Retracting after the approach is optional.  Approximates the contact
     position and an on-surface (high) position by finding first crossings
@@ -131,6 +150,44 @@ def limited_quadratic_param_guess(x, y) :
     x_contact = float(x[i_low])
     x_high = float(x[i_high])
     slope = (y_high - y_contact) / (x_high - x_contact)
+    return (x_contact, y_contact, slope)
+
+def limited_linear_sensitivity(params):
+    """
+    Return the estimated sensitivity to small deflections according to
+    limited_linear fit parameters.
+    """
+    slope = params[2]
+    return slope
+
+def limited_quadratic(x, params):
+    """
+    Model the bump as:
+      flat region (off-surface)
+      quadratic region (in-contact)
+      flat region (high-voltage-rail)
+    Parameters:
+      x_contact (x value for the surface-contact kink)
+      y_contact (y value for the surface-contact kink)
+      slope (dy/dx at the surface-contact kink)
+      quad (d**2 y / dx**2, allow decreasing sensitivity with increased x)
+    """
+    high_voltage_rail = 2**16 - 1 # bits
+    x_contact,y_contact,slope,quad = params
+    y = slope*(x-x_contact) + quad*(x-x_contact)**2+ y_contact
+    y = numpy.clip(y, y_contact, high_voltage_rail)
+    return y
+
+def limited_quadratic_param_guess(x, y) :
+    """
+    Guess rough parameters for a limited_quadratic model.  Assumes the
+    bump approaches (raising the deflection as it does so) first.
+    Retracting after the approach is optional.  Approximates the contact
+    position and an on-surface (high) position by finding first crossings
+    of thresholds 0.3 and 0.7 of the y value's total range.  Not the
+    most efficient algorithm, but it seems fairly robust.
+    """
+    x_contact,y_contact,slope = limited_linear_param_guess(x,y)
     quad = 0
     return (x_contact, y_contact, slope, quad)
 
@@ -144,7 +201,7 @@ def limited_quadratic_sensitivity(params):
 
 @splittableKwargsFunction()
 def bump_fit(zpiezo_output_bits, deflection_input_bits,
-             paramGuesser=limited_quadratic_param_guess,
+             param_guesser=limited_quadratic_param_guess,
              model=limited_quadratic,
              sensitivity_from_fit_params=limited_quadratic_sensitivity,
              plotVerbose=False) :
@@ -152,9 +209,9 @@ def bump_fit(zpiezo_output_bits, deflection_input_bits,
     y = deflection_input_bits
     def residual(p, y, x) :
         return model(x, p) - y
-    paramGuess = paramGuesser(x, y)
+    param_guess = param_guesser(x, y)
     p,cov,info,mesg,ier = \
-        scipy.optimize.leastsq(residual, paramGuess, args=(y, x),
+        scipy.optimize.leastsq(residual, param_guess, args=(y, x),
                                full_output=True, maxfev=int(10e3))
     if config.TEXT_VERBOSE :
         print "Fitted params:",p
@@ -166,7 +223,7 @@ def bump_fit(zpiezo_output_bits, deflection_input_bits,
         else :
             print "Solution did not converge"
     if plotVerbose or config.PYLAB_VERBOSE :
-        yguess = model(x, paramGuess)
+        yguess = model(x, param_guess)
         #yguess = None # Don't print the guess, since I'm convinced it's ok ;).
         yfit = model(x, p)
         bump_plot(data={"Z piezo output":x, "Deflection input":y},
@@ -223,7 +280,8 @@ def bump_plot(data, yguess=None, yfit=None, plotVerbose=False) :
 
 make_splittable_kwargs_function(bump_analyze,
                                 (bump_fit, 'zpiezo_output_bits',
-                                 'deflection_input_bits'))
+                                 'deflection_input_bits'),
+                                (slope_bitspbit2Vpnm, 'slope_bitspbit'))
 
 @splittableKwargsFunction((bump_analyze, 'data'))
 def bump_load_analyze_tweaked(tweak_file, **kwargs):
@@ -291,8 +349,9 @@ if __name__ == '__main__' :
                     'and one to analyze tweak files.\n'
                     '\n'
                     'Single file mode (the default) :\n'
-                    'Scales raw DAC/ADC bit data and fits a straight line.\n'
-                    'Returns photodiode sensitivity Vphotodiode/Zcantilever in V/nm.\n'
+                    'Scales raw DAC/ADC bit data and fits a bounded quadratic.\n'
+                    'Returns photodiode sensitivity Vphotodiode/Zcantilever in V/nm, determined by.\n'
+                    'the slope at the kink between the non-contact region and the contact region.\n'
                     '<input-file> should be whitespace-delimited, 2 column ASCII\n'
                     'without a header line.  e.g: "<zp_DAC>\\t<deflection_ADC>\\n"\n'
                     '\n'
@@ -322,6 +381,9 @@ if __name__ == '__main__' :
     parser.add_option('-d', '--datalogger-mode', dest='datalogger_mode', action='store_true',
                       help='Run input files with datalogger.read_dict_of_arrays().  This is useful, for example, to test a single line from a tweakfile.',
                       default=False)
+    parser.add_option('-q', '--disable-quadratic', dest='quadratic', action='store_false',
+                      help='Disable quadratic term in fitting (i.e. use bounded linear fits).',
+                      default=True)
     parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
                       help='Print lots of debugging information',
                       default=False)
@@ -340,17 +402,31 @@ if __name__ == '__main__' :
     config.PYLAB_INTERACTIVE = False
     config.PYLAB_VERBOSE = options.pylab
     config.GNUPLOT_VERBOSE = False
+    if options.quadratic == True:
+        param_guesser = limited_quadratic_param_guess
+        model = limited_quadratic
+        sensitivity_from_fit_params = limited_quadratic_sensitivity
+    else:
+        param_guesser = limited_linear_param_guess
+        model = limited_linear
+        sensitivity_from_fit_params = limited_linear_sensitivity
     
     if options.tweakmode == False :
         if options.datalogger_mode:
             data = bump_load(ifilename)
         else:
             data = read_data(ifilename)
-        photoSensitivity = bump_analyze(data)
+        photoSensitivity = bump_analyze(data,
+                                        param_guesser=param_guesser,
+                                        model=model,
+                                        sensitivity_from_fit_params=sensitivity_from_fit_params)
         
         print >> ofile, photoSensitivity
     else : # tweak file mode
-        slopes = bump_load_analyze_tweaked(ifilename)
+        slopes = bump_load_analyze_tweaked(ifilename,
+                                           param_guesser=param_guesser,
+                                           model=model,
+                                           sensitivity_from_fit_params=sensitivity_from_fit_params)
         if options.comma_out :
             sep = ','
         else :