@ -7,10 +7,58 @@
# Imported 20100120 from $PWQMC77/expt/Hybrid-proj/analyze-Eh.py
# (dated 20090323).
#
# fit_func_base was imported 20150520 from Cr2_analysis_cbs.py
# (dated 20141017, CVS rev 1.143).
#
# Some references on fitting:
# * http://stackoverflow.com/questions/529184/simple-multidimensional-curve-fitting
# * http://www.scipy.org/Cookbook/OptimizationDemo1 (not as thorough, but maybe useful)
"""
wpylib . math . fitting
Basic tools for two - dimensional curve fitting
ABOUT THE FITTING METHODS
We depend on module scipy . optimize and ( optionally ) lmfit to provide the
minimization routines for us .
The following methods are currently supported for scipy . optimize :
* ` fmin `
The Nelder - Mead Simplex algorithm .
* ` fmin_bfgs ` or ` bfgs `
The Broyden - Fletcher - Goldfarb - Shanno ( BFGS ) algorithm
* ` anneal `
Similated annealing algorithm
* ` leastsq `
The Levenberg - Marquardt nonlinear least square ( NLLS ) method
See the documentation of ` scipy . optimize ` for more details .
The ` fmin ` algorithm is the slowest although it is fairly foor proof to
converge it ( it may take many iterations ) .
The leastsq ` algorithm is the best but it requires parameter guess that is
reasonable .
I don ' t have much success with `anneal`--it seems to behave erratically in
my limited experience . YMMV .
The lmfit package is supported if it can be found at runtime .
This package provides richer set of features , including constraints on
parameters and parameter interdependency .
Various minimization methods under this package are available .
To use lmfit , use keyword ` lmfit : < method > ` as the fit method name .
Example : ` lmfit : leastsq ` .
See the documentation here :
http : / / cars9 . uchicago . edu / software / python / lmfit / fitting . html #fit-methods-label
"""
import numpy
import scipy . optimize
from wpylib . db . result_base import result_base
@ -379,3 +427,147 @@ def fit_func(Funct, Data=None, Guess=None, Params=None,
raise ValueError , " Invalid `outfmt ' argument = " + x
# Imported 20150520 from Cr2_analysis_cbs.py .
class fit_func_base ( object ) :
""" Base class for function 2-D fitting object.
This is an enhanced OO interface to fit_func .
In the derived class , a __call__ method must be implemented with
this prototype :
def __call__ ( self , C , x )
where
- ` C ' is the parameters which we sought through the fitting
procedure , and
- ` x ' is the x values of the data samples against which we want
to do the curve fitting .
A few user - adjustable parameters need to be attached as attributes
to this object :
- fit_method
- fit_opts ( a dict or multi_fit_opts object )
- debug
- dbg_params
- Params
` fit_method ' is a string containing the name of the fitting method to use,
see this module document .
Additional attributes are required to support lmfit - based fitting :
- param_names : a list / tuple of parameter names , in the same order as in
the legacy ' C ' __call__ argument above .
The input - data - based automatic parameter guess is specified via Guess parameter .
See wpylib . math . fitting . fit_func for detail .
- if Guess == None ( default ) , then it attempts to use self . Guess_xy ( ) method
( better , new default ) or old self . Guess ( ) method .
- if Guess == False ( only for lmfit case ) , existing values from Params object
will be used .
- TODO : dict - like Guess should be made possible .
- otherwise , the guess values will be used as the initial values .
"""
class multi_fit_opts ( dict ) :
""" A class for defining default control parameters for different fit methods.
The fit method name is the dict key , and the value , which is also a dict ,
is the default set of fitting control parameters for that particular fit method .
"""
pass
# Some reasonable parameters are set:
fit_default_opts = multi_fit_opts (
fmin = dict ( xtol = 1e-5 , maxfun = 100000 , maxiter = 10000 , disp = 0 ) ,
fmin_bfgs = dict ( gtol = 1e-6 , disp = 0 ) ,
leastsq = dict ( xtol = 1e-8 , epsfcn = 1e-6 ) ,
)
fit_default_opts [ " lmfit:leastsq " ] = dict ( xtol = 1e-8 , epsfcn = 1e-6 )
debug = 1
dbg_params = 1
fit_method = ' fmin '
fit_opts = fit_default_opts
#fit_opts = dict(xtol=1e-5, maxfun=100000, maxiter=10000, disp=0)
def fit ( self , x , y , dy = None , fit_opts = None , Funct_hook = None , Guess = None ) :
""" Main entry function for fitting. """
x = numpy . asarray ( x )
if len ( x . shape ) == 1 :
# fix common "mistake" for 1-D domain: make it 2-D
x = x . reshape ( ( 1 , x . shape [ 0 ] ) )
if fit_opts == None :
# Use class default if it is available
fit_opts = getattr ( self , " fit_opts " , { } )
if isinstance ( fit_opts , self . multi_fit_opts ) : # multiple choice :-)
fit_opts = fit_opts . get ( self . fit_method , { } )
if Guess == None :
Guess = getattr ( self , " Guess " , None )
if self . dbg_params :
self . dbg_params_log = [ ]
if self . debug > = 5 :
print " fit: Input Params = " , getattr ( self , " Params " , None )
self . last_fit = fit_func (
Funct = self ,
Funct_hook = Funct_hook ,
x = x , y = y , dy = dy ,
Guess = Guess ,
Params = getattr ( self , " Params " , None ) ,
method = self . fit_method ,
opts = fit_opts ,
debug = self . debug ,
outfmt = 0 , # yield full result
)
if self . use_lmfit_method :
if not hasattr ( self , " Params " ) :
self . Params = self . last_fit . params
return self . last_fit [ ' xopt ' ]
def func_call_hook ( self , C , x , y ) :
""" Common hook function called when calling ' THE '
function , e . g . for debugging purposes . """
from copy import copy
if self . dbg_params :
if not hasattr ( self , " dbg_params_log " ) :
self . dbg_params_log = [ ]
self . dbg_params_log . append ( copy ( C ) )
#print "Call morse2_fit_func(%s, %s) -> %s" % (C, x, y)
def get_params ( self , C , * names ) :
""" Special support function to extract the values (or
representative objects ) of the parameters contained in ' C ' ,
the list of parameters .
In the legacy case , C is simply a tuple / list of numbers .
In the lmfit case , C is a Parameters object .
"""
try :
from lmfit import Parameters
# new way: using lmfit.Parameters:
if isinstance ( C , Parameters ) :
return tuple ( C [ k ] . value for k in names )
except :
pass
# old way: using positional parameters
return tuple ( C )
@property
def use_lmfit_method ( self ) :
return self . fit_method . startswith ( " lmfit: " )
@staticmethod
def domain_array ( x ) :
""" Creates a domain array (x) for nonlinear fitting.
Also accomodates a common " mistake " for 1 - D domain by making it
correctly 2 - D in shape .
"""
x = numpy . asarray ( x )
if len ( x . shape ) == 1 :
# fix common "mistake" for 1-D domain: make it 2-D
x = x . reshape ( ( 1 , x . shape [ 0 ] ) )
return x