00001 """Hessian and Levenberg-Marquardt Curve Fitting Package with resampling capability.
00002
00003 This is loosely derived from the information in 'Numerical Recipes' 2nd Ed. by Press, Flannery, Teukolsky and Vetterling.
00004 Implementation by Marcus H. Mendenhall, Vanderbilt University Free Electron Laser Center, Nashville, TN, USA
00005 Implemented around 3 December, 2002.
00006 This implementation is in the public domain, although some of its algorithms
00007 may be at least in part owned by the 'Numerical Recipes' people.
00008 Check with them before using this in commercial software.
00009
00010 To use it, subclass the fit class and, at minimum, define function(self, params, x) as below
00011
00012 from fitting_toolkit import fit
00013 class myfit(fit):
00014 "a really terrible way to fit a polynomial to data!"
00015 def function(self, params, x):
00016 return params[0]+params[1]*x+params[2]*x*x
00017
00018 This function takes a Numeric array of parameters 'params', and a Numeric array of independent variable values 'x'. If the
00019 independent variable is a scalar, the function will be passed a 1-d array. If the independent variable is a vector,
00020 the function will be passed a 2-d array with each _row_ holding one component of the vector, so x[0] would be x, x[1] would be y, etc.
00021
00022 Then, instantiate this class, give it initial parameter estimates, set the autoderivative step size if numerical derivatives
00023 are being used (the default value is 0.001*parameter values), add points, and repeatedly call hessian_compute_fit until
00024 fitting has converged (which, with Hessian fitting, it often won't!)
00025
00026 x=myfit()
00027 x.set_initial_params([1.,1.,1.])
00028 x.auto_deriv_steps((1,1,1))
00029
00030 x.add_point(0, 2)
00031 x.add_point(1, 5)
00032 x.add_point(2, 10)
00033 x.add_point(3, 17)
00034 x.add_point(4, 26.1)
00035
00036 print "***Start test fit***"
00037 for i in range(5):
00038 x.hessian_compute_fit()
00039 print x.fitmat, x.fitvector, x.funcparams, x.funcvals[:x.pointcount], sqrt(x.chi2/x.pointcount)
00040
00041 To add more than one point at a time, do
00042
00043 x.add_points(<array of independent variables>, <array of dependent variables>, x_transpose=<0 | 1>)
00044
00045 If the independent variable is a vector and the independent array has the components of the vector in columns,
00046 so each column is one vector, the default x_transpose=0 is correct.
00047 If the components are along rows, and each row is a vector, use x_transpose=1.
00048
00049 For a more robust fitter, use the Levenberg-Marquardt stepping as follows e.g. (this example shows one convergence checker):
00050
00051 savechi2=1e30
00052 acceptcount=0
00053 for i in range(maxloops):
00054 reject=x.lm_compute_fit(trace=1)
00055 chi2=sqrt(x.chi2/x.pointcount)
00056 if not reject:
00057 if chi2/savechi2 >0.99 and acceptcount > 5: break
00058 else:
00059 savechi2=chi2
00060 acceptcount += 1
00061 elif chi2/savechi2 > 1.01: #only penalize for really bad steps, not noise!
00062 acceptcount=0
00063
00064 print x.funcparams, chi2
00065
00066 For more advanced fitting (variable weight, analytic derivatives, etc.) other methods can be overridden. Note that
00067 except for the function() override, all other function use raw access to the internal variables of the fitter. Since the data are
00068 stored in arrays which are larger than the number of points, one must always calculate based on
00069 self.xarray[:,self.pointcount] and self.yarray[:self.pointcount] to return data of the right length.
00070
00071 If weighted fitting is desired, include the function weight_func(self) in the subclass, \e e.g.
00072 def weight_func(self): #throw out points with < 5 a/d counts in the bin.
00073 #This is a bad way to do this if lots of points are being tossed,
00074 #but doesn't cost much for the expected case that it doesn't happen often
00075 return Numeric.greater(self.yarray[:self.pointcount], 5.0)
00076
00077 If analytic derivatives are desired, do, \e e.g.
00078 class gauss_fit_2d(fit):
00079 def function(self, p, r):
00080 #print p
00081 z0, a, xmu, xsigma, ymu, ysigma = p
00082 xsigi=-1.0/(2.0*xsigma**2)
00083 ysigi=-1.0/(2.0*ysigma**2)
00084 return z0+a*Numeric.exp(xsigi*(r[0]-xmu)**2+ysigi*(r[1]-ymu)**2)
00085
00086 def derivs(self):
00087 #analytic derivatives for a 2-d gaussian
00088 #z0+a*exp( -(x-xmu)**2/(2*xsig**2) -(y-ymu)**2/(2.0*ysig**2))
00089 z0, a, xmu, xsigma, ymu, ysigma = self.funcparams
00090 n=self.pointcount
00091 x=self.xarray[0,:n]
00092 y=self.xarray[1,:n]
00093 xsigi=-1.0/(2.0*xsigma**2)
00094 ysigi=-1.0/(2.0*ysigma**2)
00095 dx=x-xmu
00096 dx2=dx*dx
00097 dy=y-ymu
00098 dy2=dy*dy
00099 expfact=Numeric.exp(xsigi*dx2+ysigi*dy2)
00100 z=a*expfact
00101
00102 dd = zeros((n, 6), self.atype)
00103 dd[:,0]=1.0
00104 dd[:,1]=expfact
00105 dd[:,2]=(-2.0*xsigi)*(dx*z)
00106 dd[:,3]=(-2.0*xsigi/xsigma)*(dx2*z)
00107 dd[:,4]=(-2.0*ysigi)*(dy*z)
00108 dd[:,5]= (-2.0*ysigi/ysigma)*(dy2*z)
00109
00110 return dd
00111
00112 """
00113
00114 _rcsid="$Id: fitting_toolkit.py,v 1.18 2006/08/08 02:00:19 mendenhall Exp $"
00115
00116 try:
00117 import numpy as Numeric
00118 import numpy
00119 numeric_float=Numeric.float64
00120 from numpy import linalg
00121 solve_linear_equations=linalg.solve
00122 def singular_value_decomposition(mat):
00123 return linalg.svd(mat, full_matrices=0, compute_uv=1)
00124
00125 matinverse=linalg.inv
00126 from numpy import dot, zeros, transpose, array, array_str
00127 except:
00128 import Numeric
00129 numeric_float=Numeric.Float64
00130 from LinearAlgebra import solve_linear_equations, inverse as matinverse, singular_value_decomposition
00131 from Numeric import dot, zeros, transpose, array, array_str
00132
00133 import random
00134
00135
00136 import math
00137 from math import sqrt
00138 import copy
00139 import operator
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248
00249
00250
00251
00252
00253
00254
00255
00256
00257 class fit:
00258
00259 def __init__(self, pointhint=1000):
00260 "create the fitter, and give it a hint as to the size blocks to allocate for data arrays"
00261 self.pointhint=pointhint
00262 self.pointcount=0
00263 self.arraysexist=0
00264 self.firstpass=1
00265 self.atype=self.DefaultArrayType()
00266
00267
00268
00269
00270 def DefaultArrayType(self):
00271 """override this function if you want to fit in single precision, \e e.g.
00272 Default is numeric_float i.e. double precision"""
00273 return numeric_float
00274
00275
00276
00277
00278
00279 def set_initial_params(self, params):
00280 "set up initial values for function parameters to get the fit off to a good start"
00281 self.funcparams=array(params, self.atype)
00282 self.firstpass=1
00283 self.param_count=len(params)
00284 if not hasattr(self, "deriv_step"):
00285 self.deriv_step=self.funcparams*0.001
00286
00287
00288
00289
00290
00291 def auto_deriv_steps(self, deriv_step):
00292 "set the step sizes used for each parameter if numerical derivatives are to be used"
00293 self.deriv_step=array(deriv_step, self.atype)
00294
00295
00296
00297
00298
00299 def check_arrays(self, sample_x, points_to_add=1):
00300 "make sure arrays are big enough to add the specified number of points"
00301 if not self.arraysexist:
00302 if operator.isSequenceType(sample_x):
00303 self.arg_count=len(sample_x)
00304 else:
00305 self.arg_count=1
00306 self.frozen=zeros(self.param_count)
00307 self.xarray=zeros((self.arg_count, self.pointhint), self.atype)
00308 self.yarray=zeros(self.pointhint, self.atype )
00309 self.currentlen=self.pointhint
00310 self.arraysexist=1
00311
00312 if self.pointcount+points_to_add >= self.currentlen:
00313
00314 realmax = max(2*self.currentlen, self.currentlen+2*points_to_add)
00315 xarray=zeros((self.arg_count, realmax ), self.atype)
00316 yarray=zeros(realmax, self.atype)
00317 xarray[:, :self.pointcount]=self.xarray[:,:self.pointcount]
00318 yarray[:self.pointcount]=self.yarray[:self.pointcount]
00319 self.xarray=xarray
00320 self.yarray=yarray
00321 self.currentlen=realmax
00322
00323
00324
00325
00326
00327
00328
00329 def freeze_parameter(self, param_index):
00330 self.frozen[param_index]=1
00331
00332
00333
00334
00335 def unfreeze_parameter(self, param_index):
00336 self.frozen[param_index]=0
00337
00338
00339
00340
00341
00342
00343 def add_point(self, xvector, yval):
00344 self.check_arrays(xvector)
00345 n=self.pointcount
00346 if self.arg_count > 1:
00347 self.xarray[:,n]=xvector
00348 else:
00349 self.xarray[0,n]=xvector
00350 self.yarray[n]=yval
00351 self.pointcount=self.pointcount+1
00352
00353
00354
00355
00356
00357
00358 def add_points(self, xvector, yval, x_transpose=0):
00359 n=self.pointcount
00360
00361 if x_transpose:
00362 xv=Numeric.transpose(array(xvector))
00363 else:
00364 xv=array(xvector)
00365
00366 n1=xv.shape[-1]
00367 if len(xv.shape) == 1:
00368 self.check_arrays(1.0, n1)
00369 else:
00370 self.check_arrays(xv[:,0], n1)
00371
00372 if self.arg_count > 1:
00373 self.xarray[:,n:n+n1]=xv
00374 else:
00375 self.xarray[0,n:n+n1]=xv
00376 self.yarray[n:n+n1]=yval
00377 self.pointcount=self.pointcount+n1
00378
00379
00380
00381
00382
00383
00384 def compute_funcvals(self, xvals=None, params=None, x_transpose=0):
00385 "evaluate the fitter's function, providing some convenient glue for passing in data arrays of various shapes"
00386 if params is None:
00387 params=self.funcparams
00388
00389 if xvals is None:
00390 n=self.pointcount
00391 if self.arg_count > 1:
00392 xvals=self.xarray[:,:n]
00393 else:
00394 xvals=self.xarray[0, :n]
00395 elif x_transpose:
00396 xvals = Numeric.transpose(xvals)
00397
00398 return self.function(params, xvals)
00399
00400
00401
00402
00403
00404 def numeric_deriv(self, param_index, delta_x):
00405 """numerically differentiate the fitter's function with respect
00406 to the indexed parameter, using the specified step size"""
00407 delta=zeros(self.param_count, self.atype)
00408 delta[param_index]=delta_x/2.0
00409 newrow=((self.compute_funcvals(params=self.funcparams+delta)-
00410 self.compute_funcvals(params=self.funcparams-delta))/delta_x)
00411 return newrow
00412
00413
00414
00415
00416
00417 def derivs(self):
00418 "default deriv is automatic numeric derivatives, override this for analytic derivatives"
00419 n=self.pointcount
00420 fxarray=zeros((n, self.param_count), self.atype)
00421 for i in range(self.param_count):
00422 if not self.frozen[i]:
00423 fxarray[:,i]=self.numeric_deriv(i, self.deriv_step[i])
00424 return fxarray
00425
00426
00427
00428
00429
00430
00431 def weight_func(self):
00432 "default weight is 1 or, if explicit_weightlist exists, that is returned"
00433 if not hasattr(self, "explicit_weightlist") or self.explicit_weightlist is None:
00434 return 1.0
00435 else:
00436 return self.explicit_weightlist
00437
00438
00439
00440
00441 def setup_resampling(self):
00442 "setup_resampling() caches the 'real' arrays of x and y, so they can be resampled for bootstrapping, and seeds a random generator"
00443 assert not hasattr(self, "saved_xarray"), "Don't even think of initializing the resampling more than once!"
00444 self.saved_xarray=array(self.xarray[:,:self.pointcount])
00445 self.saved_yarray=array(self.yarray[:self.pointcount])
00446 self.initialize_random_generator()
00447
00448
00449
00450
00451 def clear_resampling(self):
00452 "clear_resampling() removes resampling machinery"
00453 self.xarray=self.saved_xarray
00454 self.yarray=self.saved_yarray
00455 self.firstpass=1
00456 del self.saved_xarray, self.saved_yarray
00457
00458
00459
00460 def initialize_random_generator(self):
00461 "initialize the random number generator to be used in resampling. Overide to use other than random.Random. r250_521 is much faster and better!"
00462 self.randoms=random.Random()
00463
00464
00465
00466 def get_random_list(self, count):
00467 "return a list of count randoms on [0,1) for resampling. Override to pick a different random generator"
00468 r=self.randoms.random
00469 return array([r() for i in range(count)])
00470
00471
00472
00473
00474 def resample(self):
00475 "resample() randomly draws a set of points equal in size to the original set from the cached data for bootstrapping"
00476 assert hasattr(self, "saved_xarray"), "resampling not set up yet. Call setup_resampling() first."
00477 ranlist=Numeric.floor(self.get_random_list(self.pointcount)*self.pointcount).astype(Numeric.Int)
00478 self.xarray=Numeric.take(self.saved_xarray, ranlist, -1)
00479 self.yarray=Numeric.take(self.saved_yarray, ranlist)
00480 self.firstpass=1
00481
00482
00483
00484
00485 def set_weights(self):
00486 self.weights=self.weight_func()
00487 self.scalar_weights = (type(self.weights) is type(1.0) or len(self.weights.shape)==1)
00488
00489
00490
00491 def weights_multiply(self, right_array):
00492 "left multiply vector or matrix by weights... override if weights are sparse matrix, etc."
00493 if self.scalar_weights:
00494 if len(right_array.shape)==1:
00495 return right_array*self.weights
00496 else:
00497 r=copy.copy(right_array)
00498 for k in range(self.param_count):
00499 r[k]*=self.weights
00500 return r
00501 else:
00502 return dot(self.weights, right_array)
00503
00504
00505
00506 def compute_chi2(self):
00507 self.residuals=self.yarray[:self.pointcount]-self.funcvals
00508 self.chi2=float(dot(self.residuals,self.weights_multiply(self.residuals)))
00509 self.reduced_chi2=float(self.chi2/(self.pointcount-self.param_count))
00510
00511
00512
00513
00514 def hessian_compute_fit(self, lm_lambda=None):
00515 "take one Hessian fitting step if lm_lambda undefined, otherwise make Levenberg-Marquardt adjustment"
00516
00517 n=self.pointcount
00518 fxarray=self.derivs()
00519
00520 self.set_weights()
00521 fxwarray=self.weights_multiply(Numeric.transpose(fxarray))
00522 self.fitmat=dot(fxwarray, fxarray)
00523
00524 if lm_lambda is not None:
00525 for i in range(self.param_count):
00526 self.fitmat[i,i]*=(1.0+lm_lambda)
00527
00528 for i in range(self.param_count):
00529 if self.frozen[i]:
00530 self.fitmat[i,:]=0.0
00531 self.fitmat[:,i]=0.0
00532 self.fitmat[i,i]=1.0
00533
00534 if(self.firstpass):
00535 self.funcvals=self.compute_funcvals()
00536 self.firstpass=0
00537
00538 self.fitvector=solve_linear_equations(self.fitmat, dot(fxwarray, self.yarray[:n]-self.funcvals) )
00539 self.funcparams=self.funcparams+self.fitvector*(1-self.frozen)
00540 self.funcvals=self.compute_funcvals()
00541 self.compute_chi2()
00542
00543
00544
00545
00546 def svd_hessian_compute_fit(self, damping=None):
00547 "take one Hessian fitting step using singular-value-decomposition"
00548
00549 try:
00550 n=self.pointcount
00551 self.set_weights()
00552
00553 if not self.scalar_weights:
00554 raise exceptions.AssertionError, "SVD solutions require, for now, scalar weights"
00555
00556 sigi=Numeric.sqrt(self.weights)
00557 if not operator.isSequenceType(sigi):
00558 design=self.derivs()*float(sigi)
00559 else:
00560 design=self.derivs()*sigi[:,Numeric.NewAxis]
00561
00562 if(self.firstpass):
00563 self.funcvals=self.compute_funcvals()
00564 self.firstpass=0
00565
00566 u, w, v = singular_value_decomposition(design)
00567 w=self.singular_value_edit(w)
00568 for i in range(self.param_count):
00569 if w[i] != 0:
00570 w[i] = 1.0/w[i]
00571 b=(self.yarray[:n]-self.funcvals)*sigi
00572 self.fitvector=Numeric.sum(dot(Numeric.transpose(u*w),b)*Numeric.transpose(v), -1)
00573 self.svd_u=u
00574 self.svd_v=v
00575 self.svd_winv=w
00576 except:
00577 import traceback
00578 traceback.print_exc()
00579 raise
00580
00581 if damping is None:
00582 self.funcparams=self.funcparams+self.fitvector*(1-self.frozen)
00583 else:
00584 self.funcparams=self.funcparams+self.fitvector*(1-self.frozen)*damping
00585
00586 self.funcvals=self.compute_funcvals()
00587 self.compute_chi2()
00588
00589
00590
00591 def singular_value_edit(self, values):
00592 "zap unreasonably small singular values based on svd_tolerance"
00593 if hasattr(self, "svd_tolerance"):
00594 tol=self.svd_tolerance
00595 else:
00596 tol=1e-10
00597 clip=max(values)*tol
00598 for i in range(len(values)):
00599 if values[i]<clip: values[i]=0.0
00600 return values
00601
00602
00603
00604
00605 def compute_fit(self, lm_lambda=None):
00606 "override this to select which fitting kernel is being used"
00607 self.hessian_compute_fit(lm_lambda)
00608
00609
00610
00611 def covariance_matrix(self):
00612 "return the variance-covariance matrix resulting from this fit"
00613 return matinverse(self.fitmat)
00614
00615
00616
00617 def svd_covariance_matrix(self):
00618 "return the variance-covariance matrix resulting from this fit when svd stepping was used"
00619 return dot(self.svd_v, Numeric.transpose(self.svd_winv**2*self.svd_v))
00620
00621
00622
00623 def lm_lambda_recipe(self, old_chi2=None, new_chi2=None):
00624 "adjust the Levenberg-marquardt lambda parameter based on the old and new chi^2 values"
00625 if old_chi2 is None:
00626 self.lm_lambda=0.001
00627 elif old_chi2 < new_chi2:
00628 if new_chi2/old_chi2 < 1.1:
00629 self.lm_lambda *=10.0
00630 else:
00631 self.lm_lambda = max(self.lm_lambda*10.0, 1.0)
00632 else:
00633 self.lm_lambda *= 0.1
00634
00635
00636
00637
00638 def lm_fit_step(self, trace=0):
00639 "take one Levenberg-Marquardt step and adjust lambda based on chi^2"
00640 n=self.pointcount
00641 if self.firstpass:
00642 self.lm_lambda_recipe()
00643 self.funcvals=self.compute_funcvals()
00644 self.set_weights()
00645 self.compute_chi2()
00646 self.firstpass=0
00647
00648 save_chi2=self.chi2
00649 save_params=self.funcparams
00650 save_vals=self.funcvals
00651 self.hessian_compute_fit(self.lm_lambda)
00652 self.lm_lambda_recipe(save_chi2, self.chi2)
00653 if save_chi2 < self.chi2:
00654 if trace:
00655 print "rejected step: old chi2=%.3e, new chi2=%.3e, lambda=%.3e" % (save_chi2, self.chi2, self.lm_lambda)
00656 print "params =", self.funcparams
00657 self.funcparams=save_params
00658 self.funcvals=save_vals
00659 self.chi2=save_chi2
00660 return 1
00661 else:
00662 if trace:
00663 print "accepted step: old chi2=%.3e, new chi2=%.3e, lambda=%.3e" % (save_chi2, self.chi2, self.lm_lambda)
00664 print "params =", self.funcparams
00665 return 0
00666
00667
00668
00669
00670
00671 def svd_damping_recipe(self, old_chi2=None, new_chi2=None):
00672 "adjust the damping parameter based on the old and new chi^2 values"
00673 if old_chi2 is None:
00674 self.svd_damping=1.0
00675 elif old_chi2 < new_chi2:
00676 if new_chi2/old_chi2 < 1.1:
00677 self.svd_damping *=0.8
00678 else:
00679 self.svd_damping *= 0.25
00680 else:
00681 self.svd_damping = min(self.svd_damping *2.0, 1.0)
00682
00683
00684
00685 def damped_svd_fit_step(self, trace=0):
00686 "take one svd-hessian step and adjust damping based on chi^2"
00687 n=self.pointcount
00688 if self.firstpass:
00689 self.svd_damping_recipe()
00690 self.funcvals=self.compute_funcvals()
00691 self.set_weights()
00692 self.compute_chi2()
00693 self.firstpass=0
00694
00695 save_chi2=self.chi2
00696 save_params=self.funcparams
00697 save_vals=self.funcvals
00698 try:
00699 self.svd_hessian_compute_fit(self.svd_damping)
00700 except KeyboardInterrupt:
00701 raise
00702 except:
00703 self.chi2=2.0*save_chi2
00704 self.svd_damping_recipe(save_chi2, self.chi2)
00705 if save_chi2 < self.chi2:
00706 if trace:
00707 print "rejected step: old chi2=%.3e, new chi2=%.3e, lambda=%.3e" % (save_chi2, self.chi2, self.svd_damping)
00708 print "params =", self.funcparams
00709 self.funcparams=save_params
00710 self.chi2=save_chi2
00711 self.funcvals=save_vals
00712 return 1
00713 else:
00714 if trace:
00715 print "accepted step: old chi2=%.3e, new chi2=%.3e, lambda=%.3e" % (save_chi2, self.chi2, self.svd_damping)
00716 print "params =", self.funcparams
00717 return 0
00718
00719
00720
00721 class linear_combination_fit(fit):
00722 "fit a linear combination of basis functions"
00723
00724
00725
00726
00727
00728
00729 def __init__(self, funclist, pointhint=1000):
00730 fit.__init__(self, pointhint)
00731 self.basis=funclist
00732 self.param_count=len(funclist)
00733
00734
00735
00736 def function(self, p, x):
00737 if self.firstpass:
00738 return 0.0
00739
00740 sumarray=zeros(x.shape[-1], self.atype)
00741 for i in range(self.param_count):
00742 sumarray+=p[i]*self.basis[i](i, x)
00743 return sumarray
00744
00745
00746
00747 def derivs(self):
00748 if self.firstpass:
00749 n=self.pointcount
00750 self.funcparams=zeros(self.param_count, self.atype)
00751 dd = zeros((n, self.param_count), self.atype)
00752 if self.arg_count==1:
00753 x=self.xarray[0,:n]
00754 else:
00755 x=self.xarray[:,:n]
00756 for i in range(self.param_count):
00757 dd[:,i]=self.basis[i](i,x)
00758 self.saved_derivs=dd
00759 return self.saved_derivs
00760
00761
00762
00763 def fit_data(self, xvector, yvector, weightlist=None):
00764 "compute the fit of the supplied data, all in one call"
00765 self.pointcount=0
00766 self.firstpass=1
00767 self.add_points(xvector, yvector)
00768 self.explicit_weightlist=weightlist
00769 self.compute_fit()
00770
00771
00772
00773 class polynomial_fit(fit):
00774 "fit a polynomial of selected degree, with x shifted by xcenter"
00775
00776
00777
00778
00779
00780
00781 def __init__(self, degree, pointhint=1000, xcenter=0.0):
00782 fit.__init__(self, pointhint)
00783 self.xcenter=xcenter
00784 self.param_count=degree+1
00785
00786
00787
00788 def function(self, coefs, xlist):
00789 if self.firstpass:
00790 return 0.0
00791
00792 xc=xlist-self.xcenter
00793 y=xc*coefs[-1]
00794 for i in coefs[-2:0:-1]:
00795 y=xc*(i+y)
00796 return y+coefs[0]
00797
00798
00799
00800 def derivs(self):
00801 if self.firstpass:
00802 self.funcparams=zeros(self.param_count, self.atype)
00803 n=self.pointcount
00804 dd = zeros((n, self.param_count), self.atype)
00805 dd[:,0]=1.0
00806 xc=self.xarray[0,:n]-self.xcenter
00807 for i in range(1,self.param_count):
00808 dd[:,i]=dd[:,i-1]*xc
00809 self.saved_derivs=dd
00810 return self.saved_derivs
00811
00812
00813
00814 def fit_data(self, xvector, yvector, weightlist=None, xcenter=0.0):
00815 "compute the fit of the supplied data, all in one call"
00816 self.pointcount=0
00817 self.firstpass=1
00818 self.add_points(xvector, yvector)
00819 self.explicit_weightlist=weightlist
00820 self.xcenter=xcenter
00821 self.compute_fit()
00822
00823
00824
00825 find_peak_fitter=polynomial_fit(2, pointhint=100)
00826
00827
00828
00829
00830
00831
00832
00833
00834
00835 def find_peak(data):
00836 """find a positive peak in data, assuming the background is 0.
00837 Data can either be a list of (x,y) or just a list of y, in which case integer x is assumed.
00838 find_peak returns xcenter, hwhm, height.
00839 Algorithm is parabolic fit to data between half-height points around max of data.
00840 This makes sense if hwhm > 1 channel, so at least 3 points
00841 get included in the fit. It breaks for peaks narrower than this,
00842 but in that case, using the highest point is about the best one can do, anyway.
00843 """
00844 da=array(data, numeric_float)
00845 if type(data[0]) is type(1.0):
00846 x=range(len(data))
00847 y=data
00848 else:
00849 x=data[:,0]
00850 y=data[:,1]
00851
00852 topchan=Numeric.argmax(y)
00853 topy=y[topchan]
00854
00855 startx=topchan
00856 while(y[startx] >= 0.5*topy): startx -= 1
00857 endx=topchan
00858 while(y[endx] >= 0.5*topy): endx += 1
00859
00860
00861 f=find_peak_fitter
00862 f.fit_data(x[startx:endx+1], y[startx:endx+1], xcenter=x[topchan])
00863 c,b,a=f.funcparams
00864 x0=-b/(2.0*a)+x[topchan]
00865 hwhm=math.sqrt(-c/(2.0*a))
00866 return x0, hwhm, c
00867
00868
00869
00870 class gauss_fit(fit):
00871 "fit a constant baseline + gaussian y=y0+a*exp( -(x-xmu)**2/(2*xsig**2) )"
00872 def function(self, p, r):
00873 z0, a, xmu, xsigma = p
00874 xsigi=-1.0/(2.0*xsigma**2)
00875 return z0+a*Numeric.exp(xsigi*(r-xmu)**2)
00876
00877 def derivs(self):
00878
00879
00880 z0, a, xmu, xsigma = self.funcparams
00881 n=self.pointcount
00882 x=self.xarray[0,:n]
00883 xsigi=-1.0/(2.0*xsigma**2)
00884 dx=x-xmu
00885 dx2=dx*dx
00886 expfact=Numeric.exp(xsigi*dx2)
00887 z=a*expfact
00888
00889 dd = zeros((n, 4), self.atype)
00890 dd[:,0]=1.0
00891 dd[:,1]=expfact
00892 dd[:,2]=(-2.0*xsigi)*(dx*z)
00893 dd[:,3]=(-2.0*xsigi/xsigma)*(dx2*z)
00894
00895 return dd
00896
00897
00898
00899 class gauss_fit_2d(fit):
00900 "fit a constant baseline + gaussian z=z0+a*exp( -(x-xmu)**2/(2*xsig**2) -(y-ymu)**2/(2.0*ysig**2))"
00901 def function(self, p, r):
00902 z0, a, xmu, xsigma, ymu, ysigma = p
00903 xsigi=-1.0/(2.0*xsigma**2)
00904 ysigi=-1.0/(2.0*ysigma**2)
00905 return z0+a*Numeric.exp(xsigi*(r[0]-xmu)**2+ysigi*(r[1]-ymu)**2)
00906
00907 def derivs(self):
00908
00909
00910 z0, a, xmu, xsigma, ymu, ysigma = self.funcparams
00911 n=self.pointcount
00912 x=self.xarray[0,:n]
00913 y=self.xarray[1,:n]
00914 xsigi=-1.0/(2.0*xsigma**2)
00915 ysigi=-1.0/(2.0*ysigma**2)
00916 dx=x-xmu
00917 dx2=dx*dx
00918 dy=y-ymu
00919 dy2=dy*dy
00920 expfact=Numeric.exp(xsigi*dx2+ysigi*dy2)
00921 z=a*expfact
00922
00923 dd = zeros((n, 6), self.atype)
00924 dd[:,0]=1.0
00925 dd[:,1]=expfact
00926 dd[:,2]=(-2.0*xsigi)*(dx*z)
00927 dd[:,3]=(-2.0*xsigi/xsigma)*(dx2*z)
00928 dd[:,4]=(-2.0*ysigi)*(dy*z)
00929 dd[:,5]= (-2.0*ysigi/ysigma)*(dy2*z)
00930
00931 return dd
00932
00933
00934
00935
00936 class cauchy_fit(fit):
00937 "Since class cauchy_fit provides no analytic derivatives, automatic numeric differentiation will be used. This is a minimal fitter example"
00938 def function(self, p, x):
00939 z0, a, xmu, hwhm = p
00940 return z0+(a*hwhm**2)/((x-xmu)**2+hwhm**2)
00941
00942
00943
00944 class generalized_cauchy_fit(fit):
00945 "generalized_cauchy_fit fits a cauchy-like distribution with a variable exponent in the tails. exponent=2 is true cauchy."
00946 def function(self, p, x):
00947 z0, a, xmu, hwhm, exponent = p
00948 return z0+a*(hwhm**2/((x-xmu)**2+hwhm**2))**(0.5*exponent)
00949
00950 if __name__=="__main__":
00951
00952
00953 class myfit(fit):
00954 def function(self, params, x):
00955 return params[0]+params[1]*x+params[2]*x**params[3]
00956
00957 x=myfit()
00958 x.set_initial_params([1.,1.,1., 3])
00959 x.auto_deriv_steps((.1,.1,.1, 0.01))
00960
00961 x.add_point(0, 2)
00962 x.add_point(1, 5)
00963 x.add_point(2, 10)
00964 x.add_point(3, 17)
00965 x.add_point(4, 26.1)
00966 x.add_point(5, 36.9)
00967
00968 print "\n\n***Start nonlinear test fit***"
00969 for i in range(10):
00970 x.lm_fit_step()
00971 print array_str(x.funcparams, precision=5), array_str(x.funcvals, precision=3), sqrt(x.reduced_chi2)
00972
00973 print "\n\n***Start polynomial test fit***"
00974 x=polynomial_fit(2)
00975 x.add_point(0, 2)
00976 x.add_point(1, 5)
00977 x.add_point(2, 10)
00978 x.add_point(3, 17)
00979 x.add_point(4, 26.1)
00980 x.add_point(5, 36.9)
00981 x.compute_fit()
00982 print x.funcparams, x.funcvals, sqrt(x.reduced_chi2)
00983
00984 print "\n\n***Start svd polynomial test fit with funny weights ***"
00985
00986
00987 class my_svd_fit(polynomial_fit):
00988 def compute_fit(self, lm_lambda=None):
00989 self.svd_hessian_compute_fit(lm_lambda)
00990 def weight_func(self):
00991 return array(range(self.pointcount), self.atype)**2+2.0
00992
00993 x=my_svd_fit(2)
00994 x.add_point(0, 2)
00995 x.add_point(1, 5)
00996 x.add_point(2, 10)
00997 x.add_point(3, 17)
00998 x.add_point(4, 26.1)
00999 x.add_point(5, 36.9)
01000 x.compute_fit()
01001 print array_str(x.funcparams, precision=5), array_str(x.funcvals, precision=3), sqrt(x.reduced_chi2)
01002
01003 print "\n\n***Start svd degenerate function nonlinear test fit ***"
01004
01005
01006 class my_svd_bad_fit(fit):
01007 def function(self, p, x):
01008 return p[0]*Numeric.exp(-p[1]+p[2]*x)
01009
01010 x=my_svd_bad_fit()
01011 x.set_initial_params((1.,1.,2.2))
01012 x.add_point(0, 2)
01013 x.add_point(1, 5)
01014 x.add_point(2, 10)
01015 x.add_point(3, 17)
01016 x.add_point(4, 26.1)
01017 x.add_point(5, 36.9)
01018 for i in range(25):
01019 x.damped_svd_fit_step(trace=0)
01020 print array_str(x.funcparams, precision=5), array_str(x.funcvals, precision=3), sqrt(x.reduced_chi2)
01021 print x.svd_covariance_matrix()
01022
01023