I've got a little project for my college and I need to write a method which fits an array to some function, here's it's part:
def Linear(self,x,a,b):
return a*x+b
def Quadratic(self, x, a,b,c):
return a*(x**2)+b*x+c
def Sinusoid(self,t, a, gam, omega, phi, offset):
return np.e ** (gam * t) * a * np.sin((2 * np.pi * omega * t) + phi) + offset
def Fit(self, name):
func= getattr(App, name)
self.fit_params, self.covariance_matrix = curve_fit(func, self.t, self.a, maxfev= 100000)
But it returns absolutely wrong values and also doesn't even work for Sinusoid function (ptimizeWarning: Covariance of the parameters could not be estimated warnings.warn('Covariance of the parameters could not be estimated'). I've already checked if it's not an issue with getattr function but it works correctly.
I'm running out of ideas where the issue is.
There are several problems here.
Linear, Quadratic and Sinusoid do not need self, you can define those as staticmethod:
#This is the decorator use to define staticmethod
#staticmethod
def Linear(x,a,b):
return a*x+b
The same applies to other methods (except Fit).
It will help when they are called in the future. When you call them with curve_fit(func,...), for example, if func is Sinusoid, what you are doing is
curve_fit(Sinusoid, ...) and not curve_fit(self.Sinusoid,...). When curve_fit use Sinusoid, the first argument may be understood as the self, therefore, you get and error.
If you define
#staticmethod
def Sinusoid(t, a, gam, omega, phi, offset):
return np.e ** (gam * t) * a * np.sin((2 * np.pi * omega * t) + phi) + offset
a, gam, omega, phi and offset are constants to fit. Therefore, when you call curve_fit, you need to pass this constants as params:
scipy.optimize.curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False, check_finite=True, bounds=- inf, inf, method=None, jac=None, **kwargs)
To clarify: The first argument is the func, the second is your experimental data for x, and the third, is the data for y. And after that all the others kwargs, like maxfev. I don't know if self.a is the data for y, or if you want to set an initial value for a. I'm going to guess you want to set the initial value of a. You need to do it like this: p0=[x1,x2,x3,x4,..., xn] where xi is the initial value of the i-th constant. If you don't pass p0 as an argument to curve_fit, the default value for each constant is going to be 1. But here it comes the last problem:
The diferent functions use a diferent number arguments, so for the first one, Linear, you need to do p0 = [a,1]. For the second, p0 = [a, 1, 1]. And for the last one, the Sinusoid one, p0 = [a, 1, 1, 1, 1]
I don't really know if you can pass p0 = [a] to all of the functions and curve_fit will magically understand. But try so and if curve_fit doesn't let you, use condictionals:
def Fit(self, name):
if func = Linear:
p0 = [a,1]
elif func = Quadratic:
p0 = [a,1,1]
elif func = Sinusoid:
p0 = [a,1,1,1,1]
self.fit_params, self.covariance_matrix = curve_fit(func, self.t, self.y_values, p0=p0, maxfev= 100000)
Where self.y_values is the experimental data as an array-like element.
If self.a is the data for y you can cut all of this mambojambo and let Fit define as it currently is. But don't forguet the #staticmethod in the other functions.
Hope this solves your problem.
Related
I'm trying to fit a curve with a Gaussian plus a Lorentzian function, using the curve_fit function from scipy.
def gaussian(x, a, x0, sig):
return a * np.exp(-1/2 * (x - x0)**2 / sig**2)
def lorentzian(x, a, b, c):
return a*c**2/((x-b)**2+c**2)
def decompose(x, z, n, b, *par):
hb_n = gaussian(x, par[0], 4861.3*(1+z), n)
hb_b = lorentzian(x, par[1], 4861.3*(1+z), b)
return hb_b + hb_n
And when I set the p0 parameter, I can get a reasonable result, which fits the curve well.
guess = [0.0001, 2, 10, 3e-16, 3e-16]
p, c = curve_fit(decompose, wave, residual, guess)
fitting parameters
the fitting model and data figure when I set the p0 parameter
But if I set the p0 and bounds parameters simultaneously, the curve_fit function gives the initial guess as the final fitting result, which is rather deviated from the data.
guess = [0.0001, 2, 10, 3e-16, 3e-16]
p, c = curve_fit(decompose, wave, residual, guess, bounds=([-0.001, 0, 0, 0, 0], [0.001, 10, 100, 1e-15, 1e-15]))
fitting parameters
the fitting model and data figure when I set the p0 and bounds parameters simultaneously
I have tried many different combinations of boundaries for the parameters, but the fitting results invariably return the initial guess values. I've been stuck in this problem for a long time. I would be very grateful if anyone can give me some advice to solve this problem.
This happens due to a combination of the optimization algorithm and its parameters.
From the official documentation:
method{‘lm’, ‘trf’, ‘dogbox’}, optional
Method to use for optimization. See least_squares for more details.
Default is ‘lm’ for unconstrained problems and ‘trf’ if bounds are
provided. The method ‘lm’ won’t work when the number of observations
is less than the number of variables, use ‘trf’ or ‘dogbox’ in this
case.
So when you add bound constraints curve_fit will use different optimization algorithm (trust region instead of Levenberg-Marquardt).
To debug the problem you can try to set full_output=True as Warren Weckesser noted in the comments.
In the case of the fit with bounds you will see something similar to:
'nfev': 1
`gtol` termination condition is satisfied.
So the optimization stopped after the first iteration. That's why founed parameters similar to the initial guess.
To fix this you can specify lower gtol parameter. Full list of available parameters you can find here: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.least_squares.html#scipy.optimize.least_squares
Example:
Code:
import numpy as np
from matplotlib import pyplot as plt
from scipy.optimize import curve_fit
def gaussian(x, a, x0, sig):
return a * np.exp(-1 / 2 * (x - x0) ** 2 / sig**2)
def lorentzian(x, a, b, c):
return a * c**2 / ((x - b) ** 2 + c**2)
def decompose(x, z, n, b, *par):
hb_n = gaussian(x, par[0], 4861.3 * (1 + z), n)
hb_b = lorentzian(x, par[1], 4861.3 * (1 + z), b)
return hb_b + hb_n
gt_parameters = [-2.42688295e-4, 2.3477827, 1.56977708e1, 4.47455820e-16, 2.2193466e-16]
wave = np.linspace(4750, 5000, num=400)
gt_curve = decompose(wave, *gt_parameters)
noisy_curve = gt_curve + np.random.normal(0, 2e-17, size=len(wave))
guess = [0.0001, 2, 10, 3e-16, 3e-16]
bounds = ([-0.001, 0, 0, 0, 0], [0.001, 10, 100, 1e-15, 1e-15])
options = [
("Levenberg-Marquardt without bounds", dict(method="lm")),
("Trust Region without bounds", dict(method="trf")),
("Trust Region with bounds", dict(method="trf", bounds=bounds)),
(
"Trust Region with bounds + fixed tolerance",
dict(method="trf", bounds=bounds, gtol=1e-36),
),
]
fig, axs = plt.subplots(len(options))
for (title, fit_params), ax in zip(options, axs):
ax.set_title(title)
p, c = curve_fit(decompose, wave, noisy_curve, guess, **fit_params)
fitted_curve = decompose(wave, *p)
ax.plot(wave, gt_curve, label="gt_curve")
ax.plot(wave, noisy_curve, label="noisy")
ax.plot(wave, fitted_curve, label="fitted_curve")
handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels)
plt.show()
I am trying to fit a Gaussian model onto gaussian distributed data (x,y) , using scipy's curve_fit. I am trying to tweak the parameters of the fitting, in order to get better fitting. I saw that curve_fit calls scipy.optimize.least_sq with the method LM (Levenberg-Marquardt method). It seems to me that it constructs a function that evaluates the least square criterion at each data point. In my example, I have 8 data points. In my comprehension and according to scipy's documentation gtol is "Orthogonality desired between the function vector and the columns of the Jacobian."
popt, pcov = optimize.curve_fit(parametrized_gaussian, patch_indexes * pixel_size, sub_sig,
p0=p0, jac=gaussian_derivative_wrt_param, maxfev=max_fev, gtol=1e-11, ftol=1e-11, xtol=1e-11)
parametrized_gaussian is simply :
def parametrized_gaussian(x, a, x0, sigma) :
res = a * np.exp(-(x - x0) ** 2 / (2 * sigma ** 2))
return res.astype('float64')
and gaussian_derivative_wrt_param is
def gaussian_derivative_wrt_param(x, a, x0, sigma):
return np.array([parametrized_gaussian(x, a, x0, sigma) / a,
2 * (x - x0) / (sigma ** 2) * parametrized_gaussian(x, a, x0, sigma),
(x - x0) ** 2 / (sigma ** 3) * parametrized_gaussian(x, a, x0, sigma)]). swapaxes(0, -1).astype('float64')
I wanted to check that the value of the jacobian at the resulting optimal parameters. I do not understand the values that I get. When curve_fit calls leastsq, it then uses :
retval = _minpack._lmder(func, Dfun, x0, args, full_output,
col_deriv, ftol, xtol, gtol, maxfev,
factor, diag)
I print Dfun(retval[0]), because retval[0] is the values of optimal parameters. This is what I get.
0.18634,-6175.62246,5660.31995
0.50737, -10685.47212, 6223.84575
0.88394, -7937.93400, 1971.45501
0.98540, 3054.98273, 261.93803
0.70291, 10670.53623, 4479.93075
0.32083, 8746.05579, 6594.01140
0.09370, 3686.25245, 4010.79420
0.01751, 900.40686, 1280.50557
How does this respect gtol ??
Results for Dfun(optimal parameters on the grid of 8 points
That is why I think I do not understand how gtol works.
From scipy/optimize/minpack/lmder.f, we find a more detailed description
c gtol is a nonnegative input variable. termination
c occurs when the cosine of the angle between fvec and
c any column of the jacobian is at most gtol in absolute
c value. therefore, gtol measures the orthogonality
c desired between the function vector and the columns
c of the jacobian.
This just means that if gtol=0, then f(x_optimal) and columns of the jacobian are perpendicular on convergence. If this is the case, then f'(x_optimal).T # f(x_optimal) is a zero matrix. Since this product is used as part of the iteration, it makes sense to stop when this is 0, because no more progress can made.
I'm trying to fit 3 model parameters to curve_fit with 2 known parameters. Here's an example (that doesn't work but I think this is pretty much what I should be doing)
def richards(t, beta, l_f, nu, k, t_m):
denom = 1 + nu * np.exp(-k * (t - t_m))
return beta + l_f / np.power(denom, 1/nu)
for i, (r, c) in enumerate(coords):
params[i, :] = curve_fit(
f = richards,
xdata = ts,
ydata = plates[0, r, c]
)[0]
So the question is how do I incorporate the known variables so that only 3 variables need to be fitted?
Say you know that beta = 2 and nu = 3. Then you can define an auxiliary function
def f(t, l_f, k, t_m):
return richards(t=t, beta=2, l_f=l_f, nu=3, k=k, t_m=t_m)
and pass this function to the modelling function:
... curve_fit(f=f, ...)
I would like to perform some tests on my optimisation routine using scipy.optimize.minimize, in particular graphing the convergence (or the rather objective function) each iteration, over multiple tests.
Suppose I have the following linearly constrained quadratic optimisation problem:
minimise: x_i Q_ij x_j + a|x_i|
subject to: sum(x_i) = 1
I can code this as:
def _fun(x, Q, a):
c = np.einsum('i,ij,j->', x, Q, x)
p = np.sum(a * np.abs(x))
return c + p
def _constr(x):
return np.sum(x) - 1
And I will implement the optimisation in scipy as:
x_0 = # some initial vector
x_soln = scipy.optimise.minimize(_fun, x_0, args=(Q, a), method='SLSQP',
constraints={'type': 'eq', 'fun': _constr})
I see that there is a callback argument but which only accepts a single argument of the parameter values at each iteration. How can I utilise this in a more esoteric case where I might have other arguments that need to be supplied to my callback function?
The way I solved this was use a generic callback cache object referenced each time from my callback function. Let's say you want to do 20 tests and plot the objective function after each iteration in the same chart. You will need an outer loop to run 20 tests, but we'll create that later.
First lets create a class that will store all iteration objective function values for us, and a couple extra bits and pieces:
class OpObj(object):
def __init__(self, Q, a):
self.Q, self.a = Q, a
rv = np.random.rand()
self.x_0 = np.array([rv, (1-rv)/2, (1-rv)/2])
self.f = np.full(shape=(500,), fill_value=np.NaN)
self.count = 0
def _fun(self, x):
return _fun(x, self.Q, self.a)
Also lets add a callback function that manipulates that class obj. Don't worry that it has more than one argument for now since we'll fix this later. Just make sure the first parameter is the solution variables.
def cb(xk, obj=None):
obj.f[obj.count] = obj._fun(xk)
obj.count += 1
All this does is use the object's functions and values to update itself, counting the number of iterations each time. This function will be called after each iteration.
Putting this all together all we need is two more things: 1) some matplotlib-ing to do the plot, and fixing the callback to have only one argument. We can do that with a decorator which is exactly what functools partial does. It returns a function with less arguments than the original. So the final code looks like this:
import matplotlib.pyplot as plt
import scipy.optimize as op
import numpy as np
from functools import partial
Q = np.array([[1.0, 0.75, 0.45], [0.75, 1.0, 0.60], [0.45, 0.60, 1.0]])
a = 1.0
def _fun(x, Q, a):
c = np.einsum('i,ij,j->', x, Q, x)
p = np.sum(a * np.abs(x))
return c + p
def _constr(x):
return np.sum(x) - 1
class OpObj(object):
def __init__(self, Q, a):
self.Q, self.a = Q, a
rv = np.random.rand()
self.x_0 = np.array([rv, (1-rv)/2, (1-rv)/2])
self.f = np.full(shape=(500,), fill_value=np.NaN)
self.count = 0
def _fun(self, x):
return _fun(x, self.Q, self.a)
def cb(xk, obj=None):
obj.f[obj.count] = obj._fun(xk)
obj.count += 1
fig, ax = plt.subplots(1,1)
x = np.linspace(1,500,500)
for test in range(20):
op_obj = OpObj(Q, a)
x_soln = op.minimize(_fun, op_obj.x_0, args=(Q, a), method='SLSQP',
constraints={'type': 'eq', 'fun': _constr},
callback=partial(cb, obj=op_obj))
ax.plot(x, op_obj.f)
ax.set_ylim((1.71,1.76))
plt.show()
I am trying to write a curve fitting function which returns the optimal parameters a, b and c, here is a simplified example:
import numpy
import scipy
from scipy.optimize import curve_fit
def f(x, a, b, c):
return x * 2*a + 4*b - 5*c
xdata = numpy.array([1,3,6,8,10])
ydata = numpy.array([ 0.91589774, 4.91589774, 10.91589774, 14.91589774, 18.91589774])
popt, pcov = scipy.optimize.curve_fit(f, xdata, ydata)
This works fine, but I want to give the user a chance to supply some (or none) of the parameters a, b or c, in which case they should be treated as constants and not estimated. How can I write f so that it fits only the parameters not supplied by the user?
Basically, I need to define f dynamically with the correct arguments. For instance if a was known by the user, f becomes:
def f(x, b, c):
a = global_version_of_a
return x * 2*a + 4*b - 5*c
Taking a page from the collections.namedtuple playbook, you can use exec to "dynamically" define func:
import numpy as np
import scipy.optimize as optimize
import textwrap
funcstr=textwrap.dedent('''\
def func(x, {p}):
return x * 2*a + 4*b - 5*c
''')
def make_model(**kwargs):
params=set(('a','b','c')).difference(kwargs.keys())
exec funcstr.format(p=','.join(params)) in kwargs
return kwargs['func']
func=make_model(a=3, b=1)
xdata = np.array([1,3,6,8,10])
ydata = np.array([ 0.91589774, 4.91589774, 10.91589774, 14.91589774, 18.91589774])
popt, pcov = optimize.curve_fit(func, xdata, ydata)
print(popt)
# [ 5.49682045]
Note the line
func=make_model(a=3, b=1)
You can pass whatever parameters you like to make_model. The parameters you pass to make_model become fixed constants in func. Whatever parameters remain become free parameters that optimize.curve_fit will try to fit.
For example, above, a=3 and b=1 become fixed constants in func. Actually, the exec statement places them in func's global namespace. func is thus defined as a function of x and the single parameter c. Note the return value for popt is an array of length 1 corresponding to the remaining free parameter c.
Regarding textwrap.dedent: In the above example, the call to textwrap.dedent is unnecessary. But in a "real-life" script, where funcstr is defined inside a function or at a deeper indentation level, textwrap.dedent allows you to write
def foo():
funcstr=textwrap.dedent('''\
def func(x, {p}):
return x * 2*a + 4*b - 5*c
''')
instead of the visually unappealing
def foo():
funcstr='''\
def func(x, {p}):
return x * 2*a + 4*b - 5*c
'''
Some people prefer
def foo():
funcstr=(
'def func(x, {p}):\n'
' return x * 2*a + 4*b - 5*c'
)
but I find quoting each line separately and adding explicit EOL characters a bit onerous. It does save you a function call however.
I usually use a lambda for this purpose.
user_b, user_c = get_user_vals()
opt_fun = lambda x, a: f(x, a, user_b, user_c)
popt, pcov = scipy.optimize.curve_fit(opt_fun, xdata, ydata)
If you want a simple solution based on curve_fit, I'd suggest that you wrap your function in a class. Minimal example:
import numpy
from scipy.optimize import curve_fit
class FitModel(object):
def f(self, x, a, b, c):
return x * 2*a + 4*b - 5*c
def f_a(self, x, b, c):
return self.f(x, self.a, b, c)
# user supplies a = 1.0
fitModel = FitModel()
fitModel.a = 1.0
xdata = numpy.array([1,3,6,8,10])
ydata = numpy.array([ 0.91589774, 4.91589774, 10.91589774, 14.91589774, 18.91589774])
initial = (1.0,2.0)
popt, pconv = curve_fit(fitModel.f_a, xdata, ydata, initial)
There is already a package that does this:
https://lmfit.github.io/lmfit-py/index.html
From the README:
"LMfit-py provides a Least-Squares Minimization routine and class
with a simple, flexible approach to parameterizing a model for
fitting to data. Named Parameters can be held fixed or freely
adjusted in the fit, or held between lower and upper bounds. In
addition, parameters can be constrained as a simple mathematical
expression of other Parameters."
def f(x, a = 10, b = 15, c = 25):
return x * 2*a + 4*b - 5*c
If the user doesn't supply an argument for the parameter in question, whatever you specified on the right-hand side of the = sign will be used:
e.g.:
f(5, b = 20) will evaluate to return 5 * 2*10 + 4*20 - 5*25 and
f(7) will evaluate to return 7 * 2*10 + 4*15 - 5*25