* wpylib.math.fitting: Imported fit_func_base from Cr2 project.
* Added documentation on fitting methods.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user