The @fit_input decorator¶
The @fit_input
decorator makes it easy to define bounds for the fit function in a more readable way. It also lets users include a fixed_params
argument in any fit function it decorates, even if that function doesn’t normally support fixed_params
.
As the name suggests fixed_params
allows users to define which parameters in their initial guess should not be modified during the optimization. This is achieved by setting tight bounds around the fixed parameters, +/- param_value / tolerance. The tolerance is by default 1e-6, but it can be set using the fixed_bound_factor
argument.
IMPORTANT: This decorator requires the x and y input vectors to be named x_data
and y_data
. The initial guess must be called guess
and the bounds bounds
.
IMPORTANT: Note that scipy doesn't allow bounds = None
. If you're writing a fit function with the @fit_input
decorator and don't want to force the user to pass bounds every time, you should be mindful of that. To solve this issue you can write your own logic to handle invalid bounds, or set the bounds input argument to have a default of bounds = (-np.inf, np.inf)
, which will leave ALL parameters free.
Lorentzian fit example¶
Let's define the lorentzian function and create some synthetic data for our example
import numpy as np
# Define the Lorentzian function
def lorentzian(x, A, x0, w):
return (A / np.pi) * (w / ((x - x0)**2 + w**2))
# Generate synthetic data
true_params = [1, 0, 2] # A=1, x0=0, w=2
np.random.seed(11)
x_data = np.linspace(-10, 10, 100)
noise = 0.01 * np.random.normal(size=len(x_data))
y_data = lorentzian(x_data, *true_params) + noise # Add noise
Then we write a fit function that supports bounds and apply the fit_input
decorator, remembering to give the correct names to the input arguments.
from scipy.optimize import curve_fit
from sqil_core.fit import fit_input
@fit_input
def fit_lorentzian(x_data, y_data, guess=None, bounds=(-np.inf, np.inf)):
return curve_fit(lorentzian, x_data, y_data, p0=guess, bounds=bounds)
Fitting without initial guess and without bounds¶
# No guess and no bounds
res = fit_lorentzian(x_data, y_data)
print("Optimized parameters\tNo guess/bounds\t", res[0])
Optimized parameters No guess/bounds [1.00732229 0.04351166 2.01522901]
Fitting with an inital guess¶
The initial guess is just an array of parameters, so you need to be careful about the order. The order in which you put the parameters in the initial guess must be the same as the one used by your model function.
In our example, the parameter order must be the same as the one used by lorentzian(x, A, x0, w)
, so [A, x0, w]
.
# Guess A x0 w
guess = [0.5, 0.5, 1]
# Only guess
res = fit_lorentzian(x_data, y_data, guess=guess)
print("Optimized parameters\tOnly guess\t",res[0])
Optimized parameters Only guess [1.0073222 0.04351161 2.01522867]
The parameter order might not at all be obvious. If you're writing a fitting function fit_lorentzian
the users cannot see your lorentzian
model function. So it's HIGHLY recommended that you write down the parameter order in the fit function's docstring, like so
@fit_input
def fit_lorentzian(x_data, y_data, guess=None, bounds=(-np.inf, np.inf)):
"""Function to fit lorentzians :)
Parameters
----------
x_data : np.ndarray
The independent variable
y_data : np.ndarray
The dependent variable
guess : list, optional
The initial guess for the parameters [A, x0, w], by default None
bounds : list[tuple] | tuple, optional
The bounds for the optimization in the form [(min, max), ...], by default (-np.inf, np.inf)
Returns
-------
tuple
popt, pcov
"""
return curve_fit(lorentzian, x_data, y_data, p0=guess, bounds=bounds)
Note how in the guess line the parameter order is specified. To write the docstring template automatically you can download the autoDosctring VS Code extension, then type """ (right below your function's definition) followed by TAB.
Fitting with bounds¶
Bounds must be given in an array of tuples, following the same order as the guess array.
# Bounds A x0 w
bounds = [(0,2), (-3, 1), (0.7, 2.1)]
# Only bounds
res = fit_lorentzian(x_data, y_data, bounds=bounds)
print("Optimized parameters\tOnly bounds\t",res[0])
Optimized parameters Only bounds [1.0073222 0.0435117 2.01522867]
Fixing parameters¶
The fit_lorentzian
function doesn't allow for a fixed_params
argument directly, but it inherits it from the @fit_input
decorator. So, even if it's not present in the function definition, you can still pass it.
fixed_params
allows users to define which parameters should not be optimized. To be able to fix the values an initial guess must be provided.
It must be passed as an array of indices. These indices are relative to the initial guess. So, for example, if we wanted to fix the amplitude A of the lorentzian, we would set fixed_params=[0]
# Guess A x0 w
guess = [0.5, 0.5, 1]
# Fix A to its initial value
fixed_params = [0]
# Fit with fixed A
res = fit_lorentzian(x_data, y_data, guess=guess, fixed_params=fixed_params)
print("Optimized parameters\tFixed A = 0.5\t",res[0])
Optimized parameters Fixed A = 0.5 [0.5000005 0.08076683 1.09458635]
See how the value of A remained fixed, up to a relative precision of 1e-6.
Important notes¶
See how in the definition of fit_lorentzian
the default value for bounds
is (-np.inf, np.inf)
. If bounds
was None
by default and the user forgot to set the bounds manually, we would get a scipy error, since None
bounds are not allowed.
@fit_input
def fit_lorentzian(x_data, y_data, guess=None, bounds=None):
return curve_fit(lorentzian, x_data, y_data, p0=guess, bounds=bounds)
try:
fit_lorentzian(x_data, y_data)
except Exception as err:
print(f"ERROR: {err}")
ERROR: 'NoneType' object is not iterable